from __future__ import annotations from collections import defaultdict from datetime import datetime, timedelta from typing import Any from .config import GeneratorConfig from .models import DatasetBundle, stable_id def generate_execution_quality(bundle: DatasetBundle, config: GeneratorConfig) -> DatasetBundle: """Populate execution and quality records, including finite-capacity rework ledgers.""" work_orders = sorted(bundle.rows("work-orders"), key=lambda row: str(row.get("workOrderCode") or row["workOrderId"])) operations = sorted( bundle.rows("operations"), key=lambda row: (str(row.get("workOrderId") or ""), int(row.get("sequence") or 0), str(row["operationId"])), ) slots = sorted(bundle.rows("schedule-slots"), key=lambda row: (str(row.get("start") or ""), str(row["scheduleSlotId"]))) materials = sorted(bundle.rows("materials"), key=lambda row: str(row.get("materialCode") or row["materialId"])) if not work_orders or not operations or not slots or not materials: raise ValueError("execution and quality generation requires orders, scheduled operations and materials") baseline_operation_count = len(operations) baseline_schedule_slot_count = len(slots) work_order_by_id = {str(row["workOrderId"]): row for row in work_orders} material_by_id = {str(row["materialId"]): row for row in materials} slot_by_operation = {str(row["operationId"]): row for row in slots} operations_by_work_order: dict[str, list[dict[str, Any]]] = defaultdict(list) for operation in operations: operations_by_work_order[str(operation["workOrderId"])].append(operation) requirements_by_package: dict[str, list[dict[str, Any]]] = defaultdict(list) for requirement in bundle.rows("material-requirements"): requirements_by_package[str(requirement.get("workPackageId") or "")].append(requirement) occupied_by_resource: dict[str, list[tuple[datetime, datetime]]] = defaultdict(list) occupied_by_team: dict[str, list[tuple[datetime, datetime]]] = defaultdict(list) for slot in slots: start = datetime.fromisoformat(str(slot["start"])) end = datetime.fromisoformat(str(slot["end"])) if end <= start: raise AssertionError(f"baseline schedule slot must have positive duration: {slot['scheduleSlotId']}") occupied_by_resource[str(slot["resourceId"])].append((start, end)) occupied_by_team[str(slot["teamId"])].append((start, end)) for intervals in (*occupied_by_resource.values(), *occupied_by_team.values()): intervals.sort() mes_orders: list[dict[str, Any]] = [] for work_order in work_orders: work_order_id = str(work_order["workOrderId"]) assigned_slots = [slot_by_operation[str(op["operationId"])] for op in operations_by_work_order[work_order_id]] first_slot = min(assigned_slots, key=lambda row: str(row["start"])) dispatch_at = datetime.fromisoformat(str(first_slot["start"])) - timedelta(days=1) frozen = any(bool(row.get("frozen")) for row in assigned_slots) mes_orders.append( { "mesOrderId": stable_id("mes-order", work_order_id, prefix="MES"), "workOrderId": work_order_id, "productionOrderId": work_order["productionOrderId"], "projectId": work_order["projectId"], "dispatchStatus": "DISPATCHED", "dispatchAt": dispatch_at.isoformat(), "frozen": frozen, "autoDeleteAllowed": False, "autoRescheduleAllowed": not frozen, "protectedReason": "MES_DISPATCHED_AND_FROZEN" if frozen else "MES_DISPATCHED", "evidenceRefs": [f"work-order:{work_order_id}", f"schedule-slot:{first_slot['scheduleSlotId']}"], } ) report_target = max(1, min(len(operations), config.profile.operation_count // 6)) reported_operations = operations[:report_target] operation_reports: list[dict[str, Any]] = [] for index, operation in enumerate(reported_operations, 1): slot = slot_by_operation[str(operation["operationId"])] operation_reports.append( { "operationReportId": stable_id("operation-report", operation["operationId"], index, prefix="RPT"), "operationId": operation["operationId"], "workOrderId": operation["workOrderId"], "reportStatus": "COMPLETED", "reportedQuantity": 1, "reportedHours": operation["durationHours"], "reportedAt": slot["end"], "resourceId": slot["resourceId"], "teamId": slot["teamId"], "evidenceRefs": [f"schedule-slot:{slot['scheduleSlotId']}", f"operation:{operation['operationId']}"], } ) issue_target = max(1, min(len(work_orders), config.profile.production_order_count // 5)) material_issues: list[dict[str, Any]] = [] for index, work_order in enumerate(work_orders[:issue_target], 1): package_requirements = requirements_by_package.get(str(work_order.get("workPackageId") or "")) or [] if package_requirements: requirement = package_requirements[(index - 1) % len(package_requirements)] material_id = requirement["materialId"] quantity = max(0.001, float(requirement.get("grossRequirement") or 1.0)) evidence = f"material-requirement:{requirement['requirementId']}" else: material = materials[(index - 1) % len(materials)] material_id = material["materialId"] quantity = 1.0 + index % 5 evidence = f"material:{material_id}" first_operation = operations_by_work_order[str(work_order["workOrderId"])][0] slot = slot_by_operation[str(first_operation["operationId"])] material_issues.append( { "materialIssueId": stable_id("material-issue", work_order["workOrderId"], material_id, prefix="ISS"), "workOrderId": work_order["workOrderId"], "materialId": material_id, "quantity": round(quantity, 3), "unit": material_by_id.get(str(material_id), {}).get("unit", "PCS"), "issueStatus": "ISSUED", "issuedAt": slot["start"], "negativeInventoryAllowed": False, "evidenceRefs": [evidence, f"operation:{first_operation['operationId']}"], } ) last_operation_ids = { str(work_order_operations[-1]["operationId"]) for work_order_operations in operations_by_work_order.values() if work_order_operations } hold_candidates = [row for row in operations if row.get("holdPoint") and row["operationId"] in slot_by_operation] if not hold_candidates: hold_candidates = [row for row in operations if row["operationId"] in slot_by_operation][:1] hold_candidates.sort( key=lambda row: ( 0 if str(row["operationId"]) in last_operation_ids else 1, str(row.get("workOrderId") or ""), int(row.get("sequence") or 0), str(row["operationId"]), ) ) inspection_target = max(2, min(len(hold_candidates), max(2, config.profile.production_order_count // 12))) quality_inspections: list[dict[str, Any]] = [] nonconformities: list[dict[str, Any]] = [] rework_orders: list[dict[str, Any]] = [] rework_operations: list[dict[str, Any]] = [] rework_schedule_slots: list[dict[str, Any]] = [] ncr_target = max(1, inspection_target // 4) for index, operation in enumerate(hold_candidates[:inspection_target], 1): slot = slot_by_operation[str(operation["operationId"])] inspection_id = stable_id("inspection", operation["operationId"], "initial", prefix="INS") inspection_held_at = datetime.fromisoformat(str(slot["start"])) inspection_inspected_at = datetime.fromisoformat(str(slot["end"])) has_ncr = index <= ncr_target if not has_ncr: inspection_released_at = inspection_inspected_at + timedelta(minutes=5) quality_inspections.append( { "inspectionId": inspection_id, "operationId": operation["operationId"], "inspectionType": "HOLD_POINT" if operation.get("holdPoint") else "PROCESS_INSPECTION", "holdPoint": True, "status": "RELEASED", "resultStatus": "PASSED", "stateHistory": ["HOLD", "INSPECTED", "PASSED", "RELEASED"], "heldAt": inspection_held_at.isoformat(), "inspectedAt": inspection_inspected_at.isoformat(), "releasedAt": inspection_released_at.isoformat(), "witnessType": "CLASS_AND_OWNER" if index % 3 == 0 else "INTERNAL_QA", "evidenceRefs": [f"operation:{operation['operationId']}", f"schedule-slot:{slot['scheduleSlotId']}"], } ) continue nonconformity_id = stable_id("nonconformity", inspection_id, prefix="NCR") rework_order_id = stable_id("rework-order", nonconformity_id, prefix="RWK") rework_operation_id = stable_id("rework-operation", rework_order_id, prefix="RWOP") rework_schedule_slot_id = stable_id("rework-schedule-slot", rework_operation_id, prefix="RWS") reinspection_id = stable_id("inspection", rework_operation_id, "reinspection", prefix="INS") planned_hours = round(max(1.0, float(operation["durationHours"]) * 0.35), 2) ncr_opened_at = inspection_inspected_at + timedelta(minutes=1) rework_start = ncr_opened_at + timedelta(minutes=5) rework_duration = timedelta(hours=planned_hours) resource_id = str(slot["resourceId"]) team_id = str(slot["teamId"]) while True: rework_end = rework_start + rework_duration conflict_ends = [ end for start, end in (*occupied_by_resource[resource_id], *occupied_by_team[team_id]) if start < rework_end and rework_start < end ] if not conflict_ends: break rework_start = max(conflict_ends) + timedelta(minutes=5) occupied_by_resource[resource_id].append((rework_start, rework_end)) occupied_by_team[team_id].append((rework_start, rework_end)) occupied_by_resource[resource_id].sort() occupied_by_team[team_id].sort() reinspection_held_at = rework_end reinspection_inspected_at = reinspection_held_at + timedelta(minutes=30) quality_released_at = reinspection_inspected_at + timedelta(minutes=5) ncr_closed_at = quality_released_at + timedelta(minutes=5) if not ( inspection_held_at <= inspection_inspected_at <= ncr_opened_at < rework_start < rework_end <= reinspection_inspected_at <= quality_released_at <= ncr_closed_at ): raise AssertionError(f"invalid NCR/rework timeline for {nonconformity_id}") source_work_order = work_order_by_id[str(operation["workOrderId"])] quality_inspections.append( { "inspectionId": inspection_id, "operationId": operation["operationId"], "inspectionType": "HOLD_POINT" if operation.get("holdPoint") else "PROCESS_INSPECTION", "holdPoint": True, "status": "RELEASED_AFTER_REWORK", "resultStatus": "FAILED", "stateHistory": [ "HOLD", "INSPECTED", "FAILED", "NCR_RAISED", "REWORK_COMPLETED", "REINSPECTION_PASSED", "RELEASED", ], "heldAt": inspection_held_at.isoformat(), "failedAt": inspection_inspected_at.isoformat(), "inspectedAt": inspection_inspected_at.isoformat(), "releasedAt": quality_released_at.isoformat(), "nonconformityId": nonconformity_id, "reworkOrderId": rework_order_id, "reinspectionId": reinspection_id, "witnessType": "CLASS_AND_OWNER" if index % 3 == 0 else "INTERNAL_QA", "evidenceRefs": [f"operation:{operation['operationId']}", f"schedule-slot:{slot['scheduleSlotId']}"], } ) nonconformities.append( { "nonconformityId": nonconformity_id, "inspectionId": inspection_id, "operationId": operation["operationId"], "status": "CLOSED", "severity": ("MAJOR", "MINOR")[index % 2], "disposition": "REWORK", "stateHistory": ["OPEN", "REWORK_REQUIRED", "REWORK_IN_PROGRESS", "REINSPECTION", "RELEASED", "CLOSED"], "stateTransitions": [ {"state": "OPEN", "at": ncr_opened_at.isoformat()}, {"state": "REWORK_IN_PROGRESS", "at": rework_start.isoformat()}, {"state": "REINSPECTION", "at": rework_end.isoformat()}, {"state": "RELEASED", "at": quality_released_at.isoformat()}, {"state": "CLOSED", "at": ncr_closed_at.isoformat()}, ], "reworkOrderId": rework_order_id, "reworkOperationId": rework_operation_id, "reworkScheduleSlotId": rework_schedule_slot_id, "reinspectionId": reinspection_id, "rootCause": "SYNTHETIC_PROCESS_VARIATION", "openedAt": ncr_opened_at.isoformat(), "reworkStartedAt": rework_start.isoformat(), "reworkCompletedAt": rework_end.isoformat(), "reinspectionInspectedAt": reinspection_inspected_at.isoformat(), "releasedAt": quality_released_at.isoformat(), "closedAt": ncr_closed_at.isoformat(), "evidenceRefs": [f"inspection:{inspection_id}", f"operation:{operation['operationId']}"], } ) rework_orders.append( { "reworkOrderId": rework_order_id, "orderType": "REWORK", "nonconformityId": nonconformity_id, "operationId": operation["operationId"], "sourceOperationId": operation["operationId"], "reworkOperationId": rework_operation_id, "workOrderId": operation["workOrderId"], "parentWorkOrderId": operation["workOrderId"], "productionOrderId": operation.get("productionOrderId") or source_work_order.get("productionOrderId"), "projectId": source_work_order.get("projectId"), "status": "CLOSED", "reinspectionRequired": True, "resourceId": resource_id, "teamId": team_id, "plannedHours": planned_hours, "scheduledHours": round((rework_end - rework_start).total_seconds() / 3600.0, 2), "capacityReservationRequired": True, "capacityReservationStatus": "RESERVED_AND_EXECUTED", "stateHistory": ["CREATED", "IN_PROGRESS", "COMPLETED", "REINSPECTION_PASSED", "CLOSED"], "stateTransitions": [ {"state": "CREATED", "at": ncr_opened_at.isoformat()}, {"state": "IN_PROGRESS", "at": rework_start.isoformat()}, {"state": "COMPLETED", "at": rework_end.isoformat()}, {"state": "REINSPECTION_PASSED", "at": reinspection_inspected_at.isoformat()}, {"state": "CLOSED", "at": ncr_closed_at.isoformat()}, ], "createdAt": ncr_opened_at.isoformat(), "startedAt": rework_start.isoformat(), "completedAt": rework_end.isoformat(), "reinspectionPassedAt": reinspection_inspected_at.isoformat(), "closedAt": ncr_closed_at.isoformat(), "sourceScheduleSlotId": slot["scheduleSlotId"], "sourceBaselineScheduleSlotId": slot["scheduleSlotId"], "scheduleSlotId": rework_schedule_slot_id, "reworkScheduleSlotId": rework_schedule_slot_id, "evidenceRefs": [ f"nonconformity:{nonconformity_id}", f"operation:{operation['operationId']}", f"resource:{resource_id}", f"team:{team_id}", ], } ) rework_operations.append( { "reworkOperationId": rework_operation_id, "reworkOrderId": rework_order_id, "sourceOperationId": operation["operationId"], "parentWorkOrderId": operation["workOrderId"], "operationCode": f"RW-{operation.get('operationCode') or operation['operationId']}", "operationName": f"返工-{operation.get('operationName') or operation.get('operationCode') or operation['operationId']}", "operationType": "REWORK", "sequence": 1, "durationHours": planned_hours, "resourceId": resource_id, "teamId": team_id, "scheduleSlotId": rework_schedule_slot_id, "status": "COMPLETED", "start": rework_start.isoformat(), "end": rework_end.isoformat(), } ) rework_schedule_slots.append( { "reworkScheduleSlotId": rework_schedule_slot_id, "scheduleSlotId": rework_schedule_slot_id, "slotType": "REWORK", "reworkOperationId": rework_operation_id, "reworkOrderId": rework_order_id, "sourceOperationId": operation["operationId"], "sourceBaselineScheduleSlotId": slot["scheduleSlotId"], "resourceId": resource_id, "teamId": team_id, "start": rework_start.isoformat(), "end": rework_end.isoformat(), "durationHours": planned_hours, "finiteCapacityReserved": True, "capacityReservationStatus": "RESERVED_AND_EXECUTED", "explanation": "NCR 返工独立占用原工序匹配资源与班组,并避让基准排产和其他返工槽位。", "evidenceRefs": [ f"nonconformity:{nonconformity_id}", f"operation:{operation['operationId']}", f"resource:{resource_id}", f"team:{team_id}", ], } ) quality_inspections.append( { "inspectionId": reinspection_id, "operationId": operation["operationId"], "reworkOperationId": rework_operation_id, "inspectionType": "REINSPECTION", "holdPoint": True, "status": "CLOSED", "resultStatus": "PASSED", "stateHistory": ["HOLD", "REINSPECTION", "PASSED", "RELEASED", "CLOSED"], "reinspectionOfId": inspection_id, "nonconformityId": nonconformity_id, "reworkOrderId": rework_order_id, "reworkScheduleSlotId": rework_schedule_slot_id, "heldAt": reinspection_held_at.isoformat(), "inspectedAt": reinspection_inspected_at.isoformat(), "releasedAt": quality_released_at.isoformat(), "closedAt": ncr_closed_at.isoformat(), "evidenceRefs": [f"rework-order:{rework_order_id}", f"nonconformity:{nonconformity_id}"], } ) tables = { "mes-orders": mes_orders, "operation-reports": operation_reports, "material-issues": material_issues, "quality-inspections": quality_inspections, "nonconformities": nonconformities, "rework-orders": rework_orders, } if any(not rows for rows in tables.values()): missing = [name for name, rows in tables.items() if not rows] raise AssertionError(f"execution/quality tables must all contain data: {missing}") for name, rows in tables.items(): bundle.set_rows(name, rows) bundle.set_rows("rework-operations", rework_operations) bundle.set_rows("rework-schedule-slots", rework_schedule_slots) if len(bundle.rows("operations")) != baseline_operation_count or len(bundle.rows("schedule-slots")) != baseline_schedule_slot_count: raise AssertionError("rework generation must not change baseline operation or schedule-slot counts") rework_total_planned_hours = round(sum(float(row["plannedHours"]) for row in rework_orders), 2) rework_total_scheduled_hours = round(sum(float(row["durationHours"]) for row in rework_schedule_slots), 2) if rework_total_planned_hours != rework_total_scheduled_hours: raise AssertionError("rework planned hours must equal independently scheduled finite-capacity hours") bundle.artifacts["execution-quality"] = { "tableCounts": {name: len(rows) for name, rows in tables.items()}, "supplementalTableCounts": { "rework-operations": len(rework_operations), "rework-schedule-slots": len(rework_schedule_slots), }, "baselineCountsPreserved": { "operations": baseline_operation_count, "schedule-slots": baseline_schedule_slot_count, }, "holdReleaseStateMachine": ["HOLD", "INSPECTED", "PASSED_OR_FAILED", "RELEASED"], "ncrReworkStateMachine": ["FAILED", "NCR_OPEN", "REWORK_SLOT", "REINSPECTION", "RELEASED", "CLOSED"], "timelineRule": "heldAt <= inspectedAt <= openedAt < rework.start < rework.end <= reinspection.inspectedAt <= releasedAt <= closedAt", "reworkUsesScheduledFiniteResources": all( row.get("resourceId") and row.get("teamId") and row.get("reworkScheduleSlotId") for row in rework_orders ), "reworkTotalPlannedHours": rework_total_planned_hours, "reworkTotalScheduledHours": rework_total_scheduled_hours, "strictTimelineSatisfied": len(nonconformities) == len(rework_orders) == len(rework_schedule_slots), "reworkOperations": rework_operations, "reworkScheduleSlots": rework_schedule_slots, "syntheticBusinessDate": config.planning_base_date.isoformat(), } return bundle