from __future__ import annotations from collections import defaultdict from datetime import date, timedelta from typing import Any from .config import GeneratorConfig from .models import DatasetBundle, stable_id def _as_date(value: Any, fallback: date) -> date: if isinstance(value, date): return value text = str(value or "")[:10] try: return date.fromisoformat(text) except ValueError: return fallback def _selected_indexes(total: int, target: int) -> set[int]: if target <= 0: return set() if target > total: raise ValueError("outsource suggestion target exceeds operation count") return {(offset * total) // target for offset in range(target)} def _eligible_routing_templates(bundle: DatasetBundle) -> list[tuple[dict[str, Any], list[dict[str, Any]]]]: resources = bundle.rows("resources") teams = bundle.rows("teams") resource_types = {str(row.get("resourceType") or "") for row in resources if row.get("status", "ACTIVE") == "ACTIVE"} active_teams = [row for row in teams if row.get("status", "ACTIVE") == "ACTIVE"] grouped: dict[str, list[dict[str, Any]]] = defaultdict(list) for operation in bundle.rows("routing-operations"): if operation.get("status", "ACTIVE") != "ACTIVE": continue required_type = str(operation.get("resourceType") or operation.get("requiredResourceType") or "WORKSTATION") required_skills = {str(code) for code in operation.get("requiredSkillCodes") or []} crew_size = int(operation.get("crewSize") or 1) team_exists = any( required_skills.issubset({str(code) for code in team.get("skillCodes") or []}) and int(team.get("crewSize") or 0) >= crew_size for team in active_teams ) if required_type in resource_types and team_exists: grouped[str(operation["routingId"])].append(operation) routings = {str(row["routingId"]): row for row in bundle.rows("routings") if row.get("status", "ACTIVE") == "ACTIVE"} eligible: list[tuple[dict[str, Any], list[dict[str, Any]]]] = [] for routing_id, templates in grouped.items(): if routing_id not in routings or len(templates) < 5: continue ordered = sorted(templates, key=lambda row: (int(row.get("sequence") or 0), str(row.get("routingOperationId") or ""))) eligible.append((routings[routing_id], ordered[:5])) return sorted(eligible, key=lambda item: str(item[0].get("routingCode") or item[0]["routingId"])) def _material_ready_dates(bundle: DatasetBundle, fallback: date) -> dict[str, date]: return { str(row["workPackageId"]): _as_date(row.get("readyDate"), fallback) for row in bundle.rows("kit-readiness") if row.get("workPackageId") } def generate_orders(bundle: DatasetBundle, config: GeneratorConfig) -> DatasetBundle: """Generate exact production orders plus non-zero shipyard hard-constraint samples.""" target_orders = config.profile.production_order_count target_operations = config.profile.operation_count if target_operations != target_orders * 5: raise ValueError("W69 contract requires exactly five operations per production order") projects = sorted( bundle.rows("ship-projects"), key=lambda row: str(row.get("projectCode") or row.get("projectId") or ""), ) work_packages = sorted( bundle.rows("work-packages"), key=lambda row: str(row.get("workPackageCode") or row.get("workPackageId") or ""), ) routing_templates = _eligible_routing_templates(bundle) resources = sorted( [row for row in bundle.rows("resources") if row.get("status", "ACTIVE") == "ACTIVE"], key=lambda row: str(row.get("resourceCode") or row.get("resourceId") or ""), ) teams = sorted( [row for row in bundle.rows("teams") if row.get("status", "ACTIVE") == "ACTIVE"], key=lambda row: str(row.get("teamCode") or row.get("teamId") or ""), ) suppliers = sorted( [ row for row in bundle.rows("suppliers") if row.get("approved", True) and row.get("status", "ACTIVE") == "ACTIVE" ], key=lambda row: str(row.get("supplierCode") or row.get("supplierId") or ""), ) if not projects or not work_packages: raise ValueError("order generation requires project and work-package master rows") if not routing_templates: raise ValueError("order generation requires a routing with five finite-resource/team-compatible operations") if not resources or not teams: raise ValueError("order generation requires finite resources and teams") if not suppliers: raise ValueError("order generation requires at least one approved finite supplier") resources_by_type: dict[str, list[dict[str, Any]]] = defaultdict(list) for resource_index, resource in enumerate(resources, 1): resources_by_type[str(resource.get("resourceType") or "")].append(resource) if not resource.get("maintenanceBlackoutDates"): first_blackout = config.planning_base_date + timedelta(days=28 + resource_index % 17) second_blackout = config.planning_base_date + timedelta(days=196 + resource_index % 23) resource["maintenanceBlackoutDates"] = [ first_blackout.isoformat(), second_blackout.isoformat(), ] if "CRANE" in str(resource.get("resourceType") or ""): resource.setdefault("maximumLiftRadiusM", 36.0 + float(resource_index % 6) * 4.0) qualification_codes_by_team: dict[str, list[str]] = defaultdict(list) for employee in bundle.rows("employees"): if employee.get("status", "ACTIVE") != "ACTIVE" or not employee.get("qualificationCode"): continue qualification_codes_by_team[str(employee.get("teamId"))].append( str(employee["qualificationCode"]) ) for codes in qualification_codes_by_team.values(): codes.sort() ready_by_package = _material_ready_dates(bundle, config.planning_base_date) latest_schedulable_ready = config.planning_horizon_end - timedelta(days=90) project_packages: dict[str, list[dict[str, Any]]] = defaultdict(list) for work_package in work_packages: ready_date = ready_by_package.get( str(work_package["workPackageId"]), _as_date(work_package.get("plannedStart"), config.planning_base_date), ) if ready_date <= latest_schedulable_ready: project_packages[str(work_package["projectId"])].append(work_package) usable_projects = [row for row in projects if project_packages.get(str(row["projectId"]))] if not usable_projects: raise ValueError("no work package is linked to an available project") releases_by_project: dict[str, list[dict[str, Any]]] = defaultdict(list) for release in bundle.rows("engineering-releases"): if release.get("status") == "RELEASED": releases_by_project[str(release.get("projectId"))].append(release) for rows in releases_by_project.values(): rows.sort(key=lambda row: (str(row.get("effectiveDate") or ""), str(row.get("releaseId") or ""))) production_orders: list[dict[str, Any]] = [] work_orders: list[dict[str, Any]] = [] operations: list[dict[str, Any]] = [] capacity_demands: list[dict[str, Any]] = [] all_outsource_eligible_indexes = [ index for index in range(target_operations) if index % 5 in {1, 2, 3} ] outsource_candidate_limit = max( config.profile.outsource_suggestion_count * 5, len(all_outsource_eligible_indexes) * 3 // 5, ) outsource_eligible_indexes = all_outsource_eligible_indexes[:outsource_candidate_limit] selected_outsource_positions = _selected_indexes( len(outsource_eligible_indexes), config.profile.outsource_suggestion_count, ) outsource_indexes = {outsource_eligible_indexes[index] for index in selected_outsource_positions} special_resource_types = [ resource_type for resource_type in ("GANTRY_CRANE", "PORTAL_CRANE", "MOBILE_CRANE", "TRANSPORTER") if resources_by_type.get(resource_type) ] balancing_resource_types = sorted( resource_type for resource_type in resources_by_type if resource_type != "WORKSTATION" ) relation_types = ("FS", "SS", "FF", "SF") lag_values = (-2.0, 0.0, 2.0, 4.0, -1.0) hazard_classes = ("HOT_WORK", "PAINT_VOC", "CONFINED_SPACE") qualification_sample_count = 0 global_operation_index = 0 for order_index in range(1, target_orders + 1): project = usable_projects[(order_index - 1) % len(usable_projects)] project_id = str(project["projectId"]) packages = project_packages[project_id] work_package = packages[((order_index - 1) // len(usable_projects)) % len(packages)] routing, templates = routing_templates[(order_index - 1) % len(routing_templates)] code = f"PO-SYN-{order_index:05d}" production_order_id = stable_id("production-order", code, prefix="PO") work_order_code = f"WO-SYN-{order_index:05d}" work_order_id = stable_id("work-order", production_order_id, prefix="WO") need_date = _as_date(work_package.get("needDate"), config.planning_horizon_end) material_ready = max( config.planning_base_date, ready_by_package.get( str(work_package["workPackageId"]), _as_date(work_package.get("plannedStart"), config.planning_base_date), ), ) release_rows = releases_by_project.get(project_id) or [] release = release_rows[-1] if release_rows else None production_orders.append( { "productionOrderId": production_order_id, "productionOrderCode": code, "projectId": project_id, "projectCode": project.get("projectCode") or next( (project_code for project_code in config.project_codes if project_code in str(project)), config.project_codes[0], ), "wbsId": work_package["wbsId"], "workPackageId": work_package["workPackageId"], "routingId": routing["routingId"], "quantity": 1 + (order_index % 3), "unit": "SET", "plannedReleaseDate": max( config.planning_base_date, material_ready - timedelta(days=2), ).isoformat(), "materialReadyAt": material_ready.isoformat(), "needDate": need_date.isoformat(), "designReleaseId": release.get("releaseId") if release else None, "designReleaseStatus": ( "RELEASED" if release or not bundle.rows("engineering-releases") else "DESIGN_PENDING" ), "priority": int(project.get("priority") or 5), "status": "RELEASED", "sourceType": "SYNTHETIC", } ) work_orders.append( { "workOrderId": work_order_id, "workOrderCode": work_order_code, "productionOrderId": production_order_id, "projectId": project_id, "wbsId": work_package["wbsId"], "workPackageId": work_package["workPackageId"], "routingId": routing["routingId"], "status": "ACTIVE", "quantity": production_orders[-1]["quantity"], "materialReadyAt": material_ready.isoformat(), "needDate": need_date.isoformat(), "mesDispatchProtected": order_index <= max(1, target_orders // 20), "sourceType": "SYNTHETIC", } ) order_operation_ids = [ stable_id("operation", work_order_id, sequence, prefix="OP") for sequence in (10, 20, 30, 40, 50) ] for position, (sequence, template) in enumerate( zip((10, 20, 30, 40, 50), templates, strict=True) ): operation_id = order_operation_ids[position] required_type = str( template.get("resourceType") or template.get("requiredResourceType") or "WORKSTATION" ) if special_resource_types and global_operation_index % 47 == 0: required_type = special_resource_types[ (global_operation_index // 47) % len(special_resource_types) ] elif ( required_type == "WORKSTATION" and balancing_resource_types and global_operation_index % 3 != 0 ): required_type = balancing_resource_types[ global_operation_index % len(balancing_resource_types) ] candidate_resources = resources_by_type[required_type] target_resource = candidate_resources[global_operation_index % len(candidate_resources)] standard = float(template.get("standardHours") or 4.0) setup = float(template.get("setupHours") or 0.0) teardown = float(template.get("teardownHours") or 0.0) duration = round( max( 1.0, min( 4.5, (standard + setup + teardown) * 0.18 + ((order_index + position) % 3) * 0.25, ), ), 2, ) is_outsource = global_operation_index in outsource_indexes hold_point = bool(template.get("holdPoint")) or ( position == 4 and order_index % 17 == 0 ) strict_erection_sample = order_index % 31 == 0 strict_spatial_sample = order_index % 37 == 0 relation_type = "FS" if position == 0 else relation_types[(order_index + position) % 4] lag_hours = 0.0 if position == 0 else lag_values[(global_operation_index + order_index) % len(lag_values)] if position > 0 and (strict_erection_sample or strict_spatial_sample): relation_type = "FS" lag_hours = 0.0 required_skills = list(template.get("requiredSkillCodes") or []) crew_size = int(template.get("crewSize") or 1) balancing_skill_codes = sorted( { str(skill_code) for team in teams if int(team.get("crewSize") or 0) >= crew_size for skill_code in team.get("skillCodes") or [] } ) if balancing_skill_codes and global_operation_index % 3 != 0: required_skills = [ balancing_skill_codes[ global_operation_index % len(balancing_skill_codes) ] ] qualified_teams = [ team for team in teams if set(required_skills).issubset(set(team.get("skillCodes") or [])) and int(team.get("crewSize") or 0) >= crew_size and qualification_codes_by_team.get(str(team["teamId"])) ] required_qualifications: list[str] = [] if qualified_teams and ( qualification_sample_count == 0 or global_operation_index % 197 == 0 ): qualified_team = qualified_teams[ (global_operation_index // 197) % len(qualified_teams) ] required_qualifications = [ qualification_codes_by_team[str(qualified_team["teamId"])][0] ] qualification_sample_count += 1 physical_sample = global_operation_index % 19 == 0 or required_type in special_resource_types maximum_length = float(target_resource.get("maximumLength") or 45.0) maximum_width = float(target_resource.get("maximumWidth") or 28.0) maximum_height = float(target_resource.get("maximumHeight") or 18.0) required_length = round(min(maximum_length * 0.65, 8.0 + global_operation_index % 9), 2) if physical_sample else None required_width = round(min(maximum_width * 0.65, 5.0 + global_operation_index % 6), 2) if physical_sample else None required_height = round(min(maximum_height * 0.65, 4.0 + global_operation_index % 5), 2) if physical_sample else None maximum_weight = target_resource.get("maximumWeight") required_weight = ( round(min(float(maximum_weight) * 0.65, 45.0 + global_operation_index % 70), 2) if maximum_weight is not None and physical_sample else None ) maximum_radius = target_resource.get("maximumLiftRadiusM") required_radius = ( round(min(float(maximum_radius) * 0.7, 16.0 + global_operation_index % 12), 2) if maximum_radius is not None and "CRANE" in required_type else None ) transport_sample = required_type == "TRANSPORTER" or global_operation_index % 23 == 0 hazard_sample = global_operation_index % 9 == 0 weather_sensitive = bool(template.get("weatherSensitive")) or global_operation_index % 13 == 0 operation = { "operationId": operation_id, "operationCode": f"{work_order_code}-OP-{sequence:03d}", "operationName": template.get("operationName") or f"Operation-{sequence:03d}", "workOrderId": work_order_id, "productionOrderId": production_order_id, "projectId": project_id, "wbsId": work_package["wbsId"], "workPackageId": work_package["workPackageId"], "blockId": work_package.get("blockId"), "zoneId": work_package.get("zoneId"), "routingId": routing["routingId"], "sourceRoutingOperationId": template.get("routingOperationId") or template.get("operationId"), "sequence": sequence, "sequenceNo": sequence, "operationPosition": position + 1, "predecessorOperationId": order_operation_ids[position - 1] if position else None, "previousOperationId": order_operation_ids[position - 1] if position else None, "nextOperationId": order_operation_ids[position + 1] if position < 4 else None, "relationType": relation_type, "lagHours": lag_hours, "durationHours": duration, "setupHours": round(min(1.0, setup), 2), "requiredResourceType": required_type, "requiredResourceGroup": target_resource.get("resourceGroupId") or template.get("primaryResourceGroup"), "requiredCapabilityTags": [required_type, "FINITE_CAPACITY"], "requiredSkillCodes": required_skills, "requiredQualificationCodes": required_qualifications, "crewSize": crew_size, "requiredWeightT": required_weight, "requiredLengthM": required_length, "requiredWidthM": required_width, "requiredHeightM": required_height, "requiredLiftRadiusM": required_radius, "transportWindowStatus": "OPEN" if transport_sample else "NOT_REQUIRED", "transportZone": target_resource.get("transportZone") if transport_sample else None, "transportWindowStart": config.planning_base_date.isoformat() if transport_sample else None, "transportWindowEnd": config.planning_horizon_end.isoformat() if transport_sample else None, "hazardClass": hazard_classes[(global_operation_index // 9) % len(hazard_classes)] if hazard_sample else None, "areaDensityUnits": 1 if hazard_sample else 0, "maxAreaDensityUnits": 2 if hazard_sample else None, "materialReadyAt": material_ready.isoformat(), "needDate": need_date.isoformat(), "drawingReleased": True, "holdPoint": hold_point, "holdStatus": "RELEASED" if hold_point else "NOT_REQUIRED", "holdReleaseAt": material_ready.isoformat() if hold_point else None, "weatherSensitive": weather_sensitive, "weatherWindowStatus": "OPEN" if weather_sensitive else "NOT_REQUIRED", "weatherWindowStart": config.planning_base_date.isoformat() if weather_sensitive else None, "weatherWindowEnd": config.planning_horizon_end.isoformat() if weather_sensitive else None, "maintenanceWindowStatus": "ENFORCED", "dockExclusive": required_type == "DOCK", "dockId": target_resource.get("resourceId") if required_type == "DOCK" else None, "berthExclusive": required_type == "BERTH", "berthId": target_resource.get("resourceId") if required_type == "BERTH" else None, "supportFrameSiteExclusive": required_type in {"JIG", "PLATFORM", "BLOCK_YARD"}, "supportFrameSiteId": ( target_resource.get("resourceId") if required_type in {"JIG", "PLATFORM", "BLOCK_YARD"} else None ), "liftingRequired": "CRANE" in required_type, "liftWindowStatus": "OPEN" if "CRANE" in required_type else "NOT_REQUIRED", "liftWindowStart": config.planning_base_date.isoformat() if "CRANE" in required_type else None, "liftWindowEnd": config.planning_horizon_end.isoformat() if "CRANE" in required_type else None, "transportPathRequired": transport_sample, "transportPathId": ( target_resource.get("transportZone") or f"PATH-SYN-{global_operation_index % 12:02d}" if transport_sample else None ), "transportPathStatus": "OPEN" if transport_sample else "NOT_REQUIRED", "paintingEnvironmentRequired": required_type == "PAINT_BOOTH", "paintingEnvironmentStatus": ( "ACCEPTABLE" if required_type == "PAINT_BOOTH" else "NOT_REQUIRED" ), "paintHumidityPercent": 62.0 if required_type == "PAINT_BOOTH" else None, "paintMaxHumidityPercent": 80.0 if required_type == "PAINT_BOOTH" else None, "paintTemperatureC": 22.0 if required_type == "PAINT_BOOTH" else None, "paintMinTemperatureC": 5.0 if required_type == "PAINT_BOOTH" else None, "paintMaxTemperatureC": 35.0 if required_type == "PAINT_BOOTH" else None, "erectionSequence": position + 1 if strict_erection_sample else None, "erectionPredecessorOperationId": ( order_operation_ids[position - 1] if strict_erection_sample and position > 0 else None ), "spatialInterferenceGroup": ( f"SPATIAL-{work_order_id}" if strict_spatial_sample and position in {0, 4} else None ), "criticalEquipmentRequired": order_index % 29 == 0 and position == 0, "criticalEquipmentId": ( f"CEQ-SYN-{order_index:05d}" if order_index % 29 == 0 and position == 0 else None ), "criticalEquipmentStatus": ( "AVAILABLE" if order_index % 29 == 0 and position == 0 else "NOT_REQUIRED" ), "criticalEquipmentArrivalAt": ( material_ready.isoformat() if order_index % 29 == 0 and position == 0 else None ), "witnessRequired": hold_point, "witnessCalendarStatus": "OPEN" if hold_point else "NOT_REQUIRED", "witnessWindowStart": config.planning_base_date.isoformat() if hold_point else None, "witnessWindowEnd": config.planning_horizon_end.isoformat() if hold_point else None, "launchTrialWeatherRequired": required_type in {"SLIPWAY", "COMMISSION_EQUIPMENT"}, "launchTrialWeatherStatus": ( "OPEN" if required_type in {"SLIPWAY", "COMMISSION_EQUIPMENT"} else "NOT_REQUIRED" ), "launchTrialWeatherWindowStart": ( config.planning_base_date.isoformat() if required_type in {"SLIPWAY", "COMMISSION_EQUIPMENT"} else None ), "launchTrialWeatherWindowEnd": ( config.planning_horizon_end.isoformat() if required_type in {"SLIPWAY", "COMMISSION_EQUIPMENT"} else None ), "executionStatus": "PLANNED", "startedTaskImmutable": False, "frozenWorkOrderProtected": False, "sourcingMode": "OUTSOURCE" if is_outsource else "MAKE", "active": True, "freezePolicy": "RESPECT_TIME_FENCE", "status": "READY", "evidenceRefs": [ f"production-order:{production_order_id}", f"routing:{routing['routingId']}", f"routing-operation:{template.get('routingOperationId') or template.get('operationId')}", f"work-package:{work_package['workPackageId']}", ], } operations.append(operation) capacity_demands.append( { "capacityDemandId": stable_id("capacity-demand", operation_id, prefix="CAP"), "operationId": operation_id, "resourceType": required_type, "resourceGroupId": operation["requiredResourceGroup"], "demandHours": duration, "crewSize": crew_size, "requiredSkillCodes": required_skills, "requiredQualificationCodes": required_qualifications, "requiredWeightT": required_weight, "requiredLengthM": required_length, "requiredWidthM": required_width, "requiredHeightM": required_height, "requiredLiftRadiusM": required_radius, "capacityMode": "FINITE", } ) global_operation_index += 1 for operation in operations: operation["sourcingMode"] = "MAKE" outsource_candidates = sorted( [ operation for operation in operations if operation.get("previousOperationId") and operation.get("nextOperationId") ], key=lambda operation: ( str(operation.get("materialReadyAt") or ""), str(operation.get("needDate") or ""), str(operation.get("operationId") or ""), ), ) early_candidate_pool = outsource_candidates[ : max( config.profile.outsource_suggestion_count, config.profile.outsource_suggestion_count * 5, ) ] selected_early_positions = _selected_indexes( len(early_candidate_pool), config.profile.outsource_suggestion_count, ) for selected_position in selected_early_positions: early_candidate_pool[selected_position]["sourcingMode"] = "OUTSOURCE" outsource_operations = [ row for row in operations if row["sourcingMode"] == "OUTSOURCE" ] specialist_suppliers = [row for row in suppliers if row.get("outsourceOperationCodes")] if not specialist_suppliers and outsource_operations: raise ValueError("outsource suggestions require approved specialist suppliers") requirements_by_wbs: dict[str, list[dict[str, Any]]] = defaultdict(list) requirements_by_project: dict[str, list[dict[str, Any]]] = defaultdict(list) for requirement in sorted( bundle.rows("material-requirements"), key=lambda row: str(row.get("requirementId") or ""), ): requirements_by_wbs[str(requirement.get("wbsId") or "")].append(requirement) requirements_by_project[str(requirement.get("projectId") or "")].append(requirement) work_orders_by_id = {str(row["workOrderId"]): row for row in work_orders} production_orders_by_id = { str(row["productionOrderId"]): row for row in production_orders } outsource_suggestions: list[dict[str, Any]] = [] monthly_assignments: dict[tuple[str, str], int] = defaultdict(int) for index, operation in enumerate(outsource_operations, 1): candidate_suppliers = [ supplier for supplier in specialist_suppliers if supplier.get("approved", True) and supplier.get("status", "ACTIVE") == "ACTIVE" ] if not candidate_suppliers: raise ValueError(f"no approved specialist supplier for {operation['operationId']}") supplier = candidate_suppliers[(index - 1) % len(candidate_suppliers)] operation_code = str(supplier.get("outsourceOperationCodes")[0]) send_date = _as_date(operation["materialReadyAt"], config.planning_base_date) + timedelta( days=3 + index % 5 ) supplier_blackouts = {str(value)[:10] for value in supplier.get("blackoutDates") or []} while send_date.isoformat() in supplier_blackouts: send_date += timedelta(days=1) transport_days_each_way = int(supplier.get("transportLeadTimeDays") or 2) process_days = max(3, min(21, int(supplier.get("standardLeadTimeDays") or 8) // 2)) inspection_days = max(1, int(supplier.get("inspectionLeadTimeDays") or 2)) round_trip_transport_days = transport_days_each_way * 2 return_date = send_date + timedelta( days=round_trip_transport_days + process_days + inspection_days ) capacity_bucket = send_date.strftime("%Y-%m") capacity_key = (str(supplier["supplierId"]), capacity_bucket) monthly_assignments[capacity_key] += 1 monthly_capacity = float(supplier.get("monthlyCapacity") or 0.0) capacity_pass = monthly_capacity <= 0 or monthly_assignments[capacity_key] <= monthly_capacity operation.update( { "outsourceSupplierId": supplier["supplierId"], "outsourceOperationCode": operation_code, "outsourceSendAt": send_date.isoformat(), "outsourceSendDate": send_date.isoformat(), "outsourceReturnAt": return_date.isoformat(), "outsourceTransportDays": round_trip_transport_days, "outsourceProcessingDays": process_days, "outsourceInspectionDays": inspection_days, } ) candidate_requirements = requirements_by_wbs.get(str(operation["wbsId"])) or requirements_by_project.get( str(operation["projectId"]), [], ) if not candidate_requirements: raise ValueError( f"outsource operation {operation['operationId']} has no traceable material requirement" ) material_requirement = next( ( row for row in candidate_requirements if row.get("sourcingMode") == "OUTSOURCE" ), candidate_requirements[0], ) work_order = work_orders_by_id[str(operation["workOrderId"])] production_order = production_orders_by_id[str(work_order["productionOrderId"])] outsource_suggestion_id = stable_id( "outsource", operation["operationId"], prefix="OUT", ) required_certificate = f"SHIP-OUTSOURCE-{operation_code}-APPROVAL" outsource_suggestions.append( { "outsourceSuggestionId": outsource_suggestion_id, "outsourceOrderId": outsource_suggestion_id, "documentType": "OUTSOURCE_SUGGESTION", "recordType": "SUGGESTION", "isActualOrder": False, "projectId": operation["projectId"], "wbsId": operation["wbsId"], "materialId": material_requirement["materialId"], "operationId": operation["operationId"], "operationCode": operation_code, "supplierId": supplier["supplierId"], "previousOperationId": operation["previousOperationId"], "nextOperationId": operation["nextOperationId"], "sendDate": send_date.isoformat(), "expectedReturnDate": return_date.isoformat(), "returnDate": return_date.isoformat(), "transportOutboundDays": transport_days_each_way, "transportReturnDays": transport_days_each_way, "transportDays": round_trip_transport_days, "processDays": process_days, "processingDays": process_days, "inspectionDays": inspection_days, "quantity": float(production_order.get("quantity") or 1.0), "quantityUnit": production_order.get("unit") or "SET", "qualityStatus": "PENDING_INSPECTION", "requiredCertificate": required_certificate, "riskLevel": supplier.get("riskLevel") or "MEDIUM", "capacityBucket": capacity_bucket, "capacityUnit": supplier.get("monthlyCapacityUnit") or "OPERATION", "supplierCapacityCheck": "PASS" if capacity_pass else "FAIL", "status": "SUGGESTED", "evidenceRefs": [ f"operation:{operation['operationId']}", f"supplier:{supplier['supplierId']}", f"material:{material_requirement['materialId']}", f"requirement:{material_requirement['requirementId']}", ], } ) 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) frozen_work_order_count = max(1, min(3, target_orders // 20 or 1)) frozen_work_orders: list[dict[str, Any]] = [] for work_order in work_orders: order_operations = operations_by_work_order[str(work_order["workOrderId"])] if any(row.get("sourcingMode") == "OUTSOURCE" for row in order_operations): continue frozen_work_orders.append(work_order) if len(frozen_work_orders) >= frozen_work_order_count: break if not frozen_work_orders: raise AssertionError("at least one MAKE-only work order is required for the real frozen baseline") production_order_by_id = {str(row["productionOrderId"]): row for row in production_orders} for frozen_index, work_order in enumerate(frozen_work_orders): frozen_ready = config.planning_base_date + timedelta(days=frozen_index) work_order.update( { "materialReadyAt": frozen_ready.isoformat(), "mesDispatchProtected": True, "frozenOrderProtected": True, "freezeReason": "PLANNING_BASELINE_WITHIN_14_DAYS", } ) production_order = production_order_by_id[str(work_order["productionOrderId"])] production_order.update( { "materialReadyAt": frozen_ready.isoformat(), "plannedReleaseDate": config.planning_base_date.isoformat(), "freezeStatus": "FROZEN", } ) for operation in operations_by_work_order[str(work_order["workOrderId"])]: operation["materialReadyAt"] = frozen_ready.isoformat() operation["frozenWorkOrderProtected"] = True if operation.get("holdPoint"): operation["holdReleaseAt"] = frozen_ready.isoformat() if operation.get("criticalEquipmentRequired"): operation["criticalEquipmentArrivalAt"] = frozen_ready.isoformat() if int(operation.get("sequence") or 0) == 10: operation["executionStatus"] = "STARTED" operation["startedTaskImmutable"] = True for readiness in bundle.rows("kit-readiness"): if str(readiness.get("workPackageId")) == str(work_order.get("workPackageId")): readiness["readyDate"] = frozen_ready.isoformat() readiness["status"] = "READY" if len(production_orders) != target_orders or len(work_orders) != target_orders: raise AssertionError("production/work order cardinality contract failed") if ( len(operations) != target_operations or len(outsource_suggestions) != config.profile.outsource_suggestion_count ): raise AssertionError("operation/outsource cardinality contract failed") if any( sum(1 for operation in operations if operation["workOrderId"] == row["workOrderId"]) != 5 for row in work_orders ): raise AssertionError("every work order must contain exactly five operations") if any( row.get("previousOperationId") is None or row.get("nextOperationId") is None for row in outsource_suggestions ): raise AssertionError("every outsource suggestion must retain predecessor and successor context") bundle.set_rows("production-orders", production_orders) bundle.set_rows("work-orders", work_orders) bundle.set_rows("operations", operations) bundle.set_rows("capacity-demands", capacity_demands) bundle.set_rows("outsource-suggestions", outsource_suggestions) bundle.artifacts["order-generation"] = { "productionOrderCount": len(production_orders), "workOrderCount": len(work_orders), "operationCount": len(operations), "operationsPerOrder": 5, "outsourceSuggestionCount": len(outsource_suggestions), "relationTypeCounts": { relation_type: sum(row.get("relationType") == relation_type for row in operations) for relation_type in relation_types }, "positiveLagCount": sum(float(row.get("lagHours") or 0.0) > 0 for row in operations), "negativeLagCount": sum(float(row.get("lagHours") or 0.0) < 0 for row in operations), "qualificationRequirementCount": sum(bool(row.get("requiredQualificationCodes")) for row in operations), "physicalRequirementCount": sum(bool(row.get("requiredLengthM")) for row in operations), "transportRequirementCount": sum(row.get("transportWindowStatus") == "OPEN" for row in operations), "hazardRequirementCount": sum(bool(row.get("hazardClass")) for row in operations), "weatherRequirementCount": sum(bool(row.get("weatherSensitive")) for row in operations), "frozenWorkOrderCount": len(frozen_work_orders), "startedImmutableTaskCount": sum(bool(row.get("startedTaskImmutable")) for row in operations), "dockExclusiveSampleCount": sum(bool(row.get("dockExclusive")) for row in operations), "berthExclusiveSampleCount": sum(bool(row.get("berthExclusive")) for row in operations), "supportFrameSiteSampleCount": sum(bool(row.get("supportFrameSiteExclusive")) for row in operations), "liftingWindowSampleCount": sum(bool(row.get("liftingRequired")) for row in operations), "paintingEnvironmentSampleCount": sum(bool(row.get("paintingEnvironmentRequired")) for row in operations), "erectionSequenceSampleCount": sum(bool(row.get("erectionPredecessorOperationId")) for row in operations), "spatialInterferenceSampleCount": sum(bool(row.get("spatialInterferenceGroup")) for row in operations), "criticalEquipmentSampleCount": sum(bool(row.get("criticalEquipmentRequired")) for row in operations), "witnessCalendarSampleCount": sum(bool(row.get("witnessRequired")) for row in operations), "launchTrialWeatherSampleCount": sum(bool(row.get("launchTrialWeatherRequired")) for row in operations), "deterministic": True, "businessDate": config.planning_base_date.isoformat(), } return bundle