477 lines
19 KiB
Python
477 lines
19 KiB
Python
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)
|