from __future__ import annotations from datetime import timedelta from typing import Any from .config import GeneratorConfig from .models import DatasetBundle, stable_id from .registry import TABLE_SPECS _ENGINEERING_TABLES = frozenset( { "drawings", "ebom", "pbom", "mbom", "routings", "routing-operations", "engineering-releases", } ) _ROUTING_TEMPLATES = ( ( "STEEL_PREP", "钢材预处理", ( ("钢材预处理", "SHOT_BLASTER", 3, False, False, False), ("钢板矫平", "PLATE_BENDER", 1, False, False, False), ("喷码标识", "WORKSTATION", 1, False, False, False), ("数控套料", "WORKSTATION", 1, False, False, False), ("数控切割", "CNC_CUTTER", 3, False, False, False), ("坡口加工", "CNC_CUTTER", 2, False, False, False), ("尺寸检查", "NDT_EQUIPMENT", 14, False, True, False), ("转运入库", "TRANSPORTER", 12, False, False, True), ), ), ( "PART_FAB", "零件制造", ( ("零件下料", "CNC_CUTTER", 3, False, False, False), ("零件成形", "PLATE_BENDER", 1, False, False, False), ("小组立", "JIG", 1, False, False, False), ("焊接", "WELDER", 2, False, False, False), ("矫正", "WORKSTATION", 1, False, False, False), ("打磨", "WORKSTATION", 1, False, False, False), ("检验", "NDT_EQUIPMENT", 13, False, True, False), ("配送", "TRANSPORTER", 12, False, False, True), ), ), ( "SECTION_ASSEMBLY", "分段制造", ( ("胎架准备", "JIG", 1, False, False, False), ("拼板", "PLATFORM", 1, False, False, False), ("自动焊", "WELDER", 3, False, False, False), ("构架安装", "JIG", 1, False, False, False), ("分段总组", "PLATFORM", 1, False, False, False), ("精度测量", "WORKSTATION", 14, False, True, False), ("分段涂装", "PAINT_BOOTH", 9, False, False, True), ("完工检验", "NDT_EQUIPMENT", 13, False, True, False), ), ), ( "GRAND_BLOCK", "总段合拢", ( ("场地准备", "BLOCK_YARD", 1, False, False, False), ("总段定位", "GANTRY_CRANE", 10, False, False, True), ("大接缝焊接", "WELDER", 4, False, False, False), ("精度测量", "WORKSTATION", 14, False, True, False), ("管路连接", "WORKSTATION", 5, False, False, False), ("电缆连接", "WORKSTATION", 7, False, False, False), ("密性试验", "PRESSURE_TEST", 15, False, True, False), ("总段检验", "NDT_EQUIPMENT", 13, False, True, False), ), ), ( "DOCK_ERECTION", "船坞搭载", ( ("总段运输", "TRANSPORTER", 12, False, False, True), ("吊装准备", "DOCK", 10, False, False, True), ("龙门吊占用", "GANTRY_CRANE", 11, False, False, True), ("总段搭载", "DOCK", 1, False, False, True), ("定位", "WORKSTATION", 14, False, False, False), ("焊接", "WELDER", 4, False, False, False), ("精度检验", "NDT_EQUIPMENT", 13, False, True, False), ("舾装恢复", "WORKSTATION", 1, False, False, False), ), ), ( "PIPE", "管系", ( ("管子下料", "WORKSTATION", 5, False, False, False), ("弯管", "PIPE_BENDER", 5, False, False, False), ("装配", "WORKSTATION", 5, False, False, False), ("焊接", "WELDER", 6, False, False, False), ("酸洗镀锌", "WORKSTATION", 6, True, False, False), ("压力试验", "PRESSURE_TEST", 16, False, True, False), ("托盘集配", "WORKSTATION", 5, False, False, False), ("船上安装", "WORKSTATION", 5, False, False, False), ), ), ( "ELECTRICAL", "电气", ( ("电缆托架安装", "WORKSTATION", 7, False, False, False), ("电缆敷设", "WORKSTATION", 7, False, False, False), ("设备安装", "WORKSTATION", 7, False, False, False), ("接线", "WORKSTATION", 7, False, False, False), ("绝缘测试", "COMMISSION_EQUIPMENT", 8, False, True, False), ("通电测试", "SHORE_POWER", 8, False, True, False), ("系统联调", "COMMISSION_EQUIPMENT", 18, False, False, False), ("完工检验", "NDT_EQUIPMENT", 13, False, True, False), ), ), ( "PAINT", "涂装", ( ("表面处理", "SHOT_BLASTER", 9, False, False, True), ("清洁", "PAINT_BOOTH", 9, False, False, True), ("底漆", "PAINT_BOOTH", 9, False, False, True), ("中间漆", "PAINT_BOOTH", 9, False, False, True), ("面漆", "PAINT_BOOTH", 9, False, False, True), ("固化", "PAINT_BOOTH", 9, False, False, True), ("膜厚检查", "NDT_EQUIPMENT", 13, False, True, False), ("缺陷修补", "PAINT_BOOTH", 9, False, False, True), ), ), ( "OUTFITTING", "舾装", ( ("托盘准备", "WORKSTATION", 1, False, False, False), ("设备定位", "WORKSTATION", 1, False, False, False), ("设备安装", "WORKSTATION", 1, False, False, False), ("管路接驳", "WORKSTATION", 5, False, False, False), ("电缆接驳", "WORKSTATION", 7, False, False, False), ("内装封板", "WORKSTATION", 1, False, False, False), ("功能检查", "COMMISSION_EQUIPMENT", 18, False, True, False), ("区域移交", "WORKSTATION", 14, False, True, False), ), ), ( "COMMISSION", "调试与试航", ( ("单机调试", "COMMISSION_EQUIPMENT", 17, False, False, False), ("系统调试", "COMMISSION_EQUIPMENT", 18, False, False, False), ("系泊试验", "SHORE_POWER", 18, False, True, False), ("倾斜试验", "BERTH", 19, False, True, True), ("航行试验", "BERTH", 19, False, False, True), ("性能测试", "COMMISSION_EQUIPMENT", 19, False, True, True), ("故障整改", "WORKSTATION", 18, False, False, False), ("船东船级社验收", "BERTH", 19, False, True, True), ), ), ) def _set_owned_rows( bundle: DatasetBundle, table_name: str, rows: list[dict[str, Any]], *, order_by: tuple[str, ...], ) -> None: if table_name not in _ENGINEERING_TABLES or table_name not in TABLE_SPECS: raise ValueError(f"table is outside W69-ENGINEERING ownership: {table_name}") spec = TABLE_SPECS[table_name] for index, row in enumerate(rows): for field in spec.required: if field not in row: raise ValueError( f"{table_name}[{index}] missing required field {field}" ) primary_keys = [row[spec.primary_key] for row in rows] if len(primary_keys) != len(set(primary_keys)): raise ValueError(f"duplicate primary key in {table_name}") bundle.set_rows( table_name, sorted(rows, key=lambda row: tuple(str(row.get(key, "")) for key in order_by)), ) def _material_identity(index: int) -> tuple[str, str]: material_code = f"MAT-SYN-{index:05d}" return material_code, stable_id("material", material_code, prefix="MAT") def _routing_identity(index: int) -> tuple[str, str]: routing_code = f"RT-SYN-{index:04d}" return routing_code, stable_id("routing", routing_code, prefix="RTG") def _resource_group_id(resource_type: str) -> str: return stable_id("resource-group", f"RG-SYN-{resource_type}", prefix="RGP") def _project_rows(bundle: DatasetBundle) -> list[dict[str, Any]]: rows = sorted(bundle.rows("ship-projects"), key=lambda row: str(row["projectCode"])) if not rows: raise ValueError("generate_engineering requires generate_projects to run first") return rows def generate_engineering( bundle: DatasetBundle, config: GeneratorConfig ) -> DatasetBundle: """Populate deterministic drawings, three BOM views and routing templates.""" projects = _project_rows(bundle) work_packages = sorted( bundle.rows("work-packages"), key=lambda row: str(row["workPackageCode"]) ) if not work_packages: raise ValueError("generate_engineering requires work-packages") base_date = config.planning_base_date releases: list[dict[str, Any]] = [] for project in projects: for sequence, (release_type, offset) in enumerate( (("BASIC_DESIGN", -75), ("DETAIL_DESIGN", -45), ("PRODUCTION_DESIGN", -20)), start=1, ): release_code = f"{project['projectCode']}-REL-{sequence:02d}" releases.append( { "releaseId": stable_id( "engineering-release", release_code, prefix="REL" ), "releaseCode": release_code, "projectId": project["projectId"], "releaseType": release_type, "status": "RELEASED", "effectiveDate": ( date_from_iso(str(project["plannedStart"])) + timedelta(days=offset) ).isoformat(), "revision": "A", "sourceType": "SYNTHETIC", } ) drawings: list[dict[str, Any]] = [] drawing_types = ("STRUCTURE", "OUTFITTING", "PIPE", "ELECTRICAL", "HVAC") for index, package in enumerate(work_packages, start=1): drawing_no = f"DWG-SYN-{index:05d}" drawings.append( { "drawingId": stable_id("drawing", drawing_no, prefix="DWG"), "drawingNo": drawing_no, "projectId": package["projectId"], "workPackageId": package["workPackageId"], "drawingType": drawing_types[(index - 1) % len(drawing_types)], "releaseStatus": "RELEASED", "revision": chr(65 + (index % 3)), "effectiveFrom": ( base_date - timedelta(days=45) + timedelta(days=index % 60) ).isoformat(), "status": "ACTIVE", "sourceType": "SYNTHETIC", } ) routings: list[dict[str, Any]] = [] routing_operations: list[dict[str, Any]] = [] for routing_index in range(1, config.profile.routing_count + 1): routing_code, routing_id = _routing_identity(routing_index) product_group, product_group_name, operations = _ROUTING_TEMPLATES[ (routing_index - 1) % len(_ROUTING_TEMPLATES) ] material_code, material_id = _material_identity(routing_index) routings.append( { "routingId": routing_id, "routingCode": routing_code, "name": f"{product_group_name}工艺路线-SYN-{routing_index:04d}", "productGroup": product_group, "outputMaterialId": material_id, "outputMaterialCode": material_code, "version": "1.0", "validFrom": base_date.isoformat(), "validTo": config.planning_horizon_end.isoformat(), "status": "ACTIVE", "sourceType": "SYNTHETIC_TEMPLATE", } ) predecessor_id: str | None = None for operation_index, ( operation_name, resource_type, skill_index, outsourcing_allowed, hold_point, weather_sensitive, ) in enumerate(operations, start=1): sequence = operation_index * 10 operation_code = f"{routing_code}-OP-{sequence:03d}" operation_id = stable_id("routing-operation", operation_code, prefix="ROP") standard_hours = round( 4.0 + ((routing_index * 7 + operation_index * 3) % 28) * 0.75, 2, ) routing_operations.append( { "routingOperationId": operation_id, "operationId": operation_id, "operationCode": operation_code, "operationName": f"{operation_name}-SYN", "routingId": routing_id, "sequence": sequence, "sequenceNo": sequence, "predecessorOperationIds": [predecessor_id] if predecessor_id else [], "relationType": "FS", "lagHours": 0.0, "durationType": "STANDARD_HOURS", "standardHours": standard_hours, "setupHours": round(0.5 + operation_index % 3 * 0.25, 2), "teardownHours": 0.5, "crewSize": 4 + (operation_index + routing_index) % 9, "resourceType": resource_type, "requiredSkillCodes": [f"SK-SYN-{skill_index:03d}"], "primaryResourceGroup": _resource_group_id(resource_type), "alternativeResourceGroups": [], "toolRequirement": f"TOOL-SYN-{resource_type}", "materialRequirementIds": [], "drawingRequirement": True, "inspectionRequirement": hold_point, "holdPoint": hold_point, "outsourcingAllowed": outsourcing_allowed, "splitAllowed": operation_index in {3, 4, 6}, "overlapAllowed": operation_index in {2, 3}, "minimumTransferBatch": 1, "maximumParallelism": 2 if operation_index in {3, 4} else 1, "weatherSensitive": weather_sensitive, "temperatureRange": [5, 35] if weather_sensitive else None, "humidityLimit": 85 if weather_sensitive else None, "status": "ACTIVE", } ) predecessor_id = operation_id total_bom = config.profile.bom_relation_count ebom_count = total_bom * 2 // 5 pbom_count = (total_bom - ebom_count) // 2 mbom_count = total_bom - ebom_count - pbom_count material_count = config.profile.material_count ebom: list[dict[str, Any]] = [] for index in range(1, ebom_count + 1): project = projects[(index - 1) % len(projects)] material_index = ((index * 37 + 11) % material_count) + 1 parent_index = ((material_index + 97 + index % 31 - 1) % material_count) + 1 if parent_index == material_index: parent_index = parent_index % material_count + 1 material_code, material_id = _material_identity(material_index) parent_code, parent_id = _material_identity(parent_index) line_code = f"EBOM-SYN-{index:06d}" ebom.append( { "bomLineId": stable_id("ebom-line", line_code, prefix="EBL"), "bomLineCode": line_code, "projectId": project["projectId"], "bomType": "EBOM", "parentMaterialId": parent_id, "parentMaterialCode": parent_code, "materialId": material_id, "materialCode": material_code, "quantity": round(1.0 + (index % 19) * 0.25, 3), "unit": "PCS", "bomLevel": 1 + index % 5, "scrapRate": round((index % 5) * 0.005, 3), "revision": "A", "effectiveFrom": base_date.isoformat(), "status": "ACTIVE", "sourceType": "SYNTHETIC", } ) pbom: list[dict[str, Any]] = [] for index in range(1, pbom_count + 1): package = work_packages[(index - 1) % len(work_packages)] material_index = ((index * 43 + 17) % material_count) + 1 material_code, material_id = _material_identity(material_index) line_code = f"PBOM-SYN-{index:06d}" pbom.append( { "bomLineId": stable_id("pbom-line", line_code, prefix="PBL"), "bomLineCode": line_code, "projectId": package["projectId"], "workPackageId": package["workPackageId"], "materialId": material_id, "materialCode": material_code, "quantity": round(1.0 + (index % 23) * 0.2, 3), "unit": "PCS", "bomType": "PBOM", "allocationMode": "PALLET" if index % 2 else "AREA", "scrapRate": round((index % 4) * 0.006, 3), "revision": "A", "effectiveFrom": base_date.isoformat(), "status": "ACTIVE", "sourceType": "SYNTHETIC", } ) mbom: list[dict[str, Any]] = [] for index in range(1, mbom_count + 1): production_order_index = (index - 1) % config.profile.production_order_count + 1 production_order_code = f"PO-SYN-{production_order_index:05d}" project = projects[(production_order_index - 1) % len(projects)] material_index = ((index * 47 + 23) % material_count) + 1 material_code, material_id = _material_identity(material_index) routing_index = (production_order_index - 1) % config.profile.routing_count + 1 routing_code, routing_id = _routing_identity(routing_index) output_code, output_id = _material_identity(routing_index) line_code = f"MBOM-SYN-{index:06d}" mbom.append( { "bomLineId": stable_id("mbom-line", line_code, prefix="MBL"), "bomLineCode": line_code, "projectId": project["projectId"], "productionOrderId": stable_id( "production-order", production_order_code, prefix="PO" ), "productionOrderCode": production_order_code, "routingId": routing_id, "routingCode": routing_code, "outputMaterialId": output_id, "outputMaterialCode": output_code, "materialId": material_id, "materialCode": material_code, "quantity": round(1.0 + (index % 29) * 0.15, 3), "unit": "PCS", "bomType": "MBOM", "scrapRate": round((index % 6) * 0.004, 3), "revision": "A", "effectiveFrom": base_date.isoformat(), "status": "ACTIVE", "sourceType": "SYNTHETIC", } ) table_rows = { "drawings": drawings, "engineering-releases": releases, "routings": routings, "routing-operations": routing_operations, "ebom": ebom, "pbom": pbom, "mbom": mbom, } order_fields = { "drawings": ("drawingNo",), "engineering-releases": ("releaseCode",), "routings": ("routingCode",), "routing-operations": ("operationCode",), "ebom": ("bomLineCode",), "pbom": ("bomLineCode",), "mbom": ("bomLineCode",), } for table_name, rows in table_rows.items(): _set_owned_rows(bundle, table_name, rows, order_by=order_fields[table_name]) return bundle def date_from_iso(value: str): from datetime import date return date.fromisoformat(value)