2026-07-28 02:12:46 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# SQL 数据包解析(松岳 MES dump 口径)
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
from server.importers.sql_pack import (
|
|
|
|
|
|
_iter_insert_tuples,
|
|
|
|
|
|
_map_orders,
|
|
|
|
|
|
apply_sql_pack_to_world,
|
|
|
|
|
|
sql_pack_to_flex,
|
|
|
|
|
|
)
|
|
|
|
|
|
from server.state.seed import seed_world
|
2026-09-08 00:07:26 +08:00
|
|
|
|
from tests.external_data import external_dir
|
2026-07-28 02:12:46 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_insert_tuple_with_quotes():
|
|
|
|
|
|
line = (
|
|
|
|
|
|
"INSERT INTO `pl_order` VALUES "
|
|
|
|
|
|
"(1, 'SYS001', 'B1', 'C1', '030129001', '支重轮', NULL, NULL, 'PC800', "
|
|
|
|
|
|
"NULL, NULL, 'D1', 117.00, NULL, NULL, 'G', '0', '0', 'S', NULL, "
|
|
|
|
|
|
"'2023-09-24 00:00:00', '2023-10-01 00:00:00', NULL, NULL, NULL, NULL, NULL, NULL, "
|
|
|
|
|
|
"NULL, '0', NULL, 5, 'u1');\n"
|
|
|
|
|
|
)
|
|
|
|
|
|
rows = list(_iter_insert_tuples(line))
|
|
|
|
|
|
assert len(rows) == 1
|
|
|
|
|
|
assert rows[0][1] == "SYS001"
|
|
|
|
|
|
assert rows[0][4] == "030129001"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_map_orders_and_apply():
|
|
|
|
|
|
rows = [{
|
|
|
|
|
|
"code": "SYS202309210001", "material_code": "030129001", "material_name": "支重轮",
|
|
|
|
|
|
"quantity": 117, "planned_end_time": "2023-10-01 00:00:00",
|
|
|
|
|
|
"customer_business_code": "CUST-A", "is_delete": "0", "level": 5,
|
|
|
|
|
|
}]
|
|
|
|
|
|
orders = _map_orders(rows)
|
|
|
|
|
|
assert len(orders) == 1
|
|
|
|
|
|
assert orders[0]["orderNo"] == "SYS202309210001"
|
|
|
|
|
|
assert orders[0]["productCode"] == "030129001"
|
|
|
|
|
|
assert orders[0]["dueDate"].startswith("2023-10-01")
|
|
|
|
|
|
|
|
|
|
|
|
world = seed_world()
|
|
|
|
|
|
for k in ("flexOrders", "flexMaterials", "flexRoutings", "flexEquipment", "flexBom"):
|
|
|
|
|
|
world[k] = []
|
|
|
|
|
|
flex = {
|
|
|
|
|
|
"orders": orders,
|
|
|
|
|
|
"materials": [{
|
|
|
|
|
|
"code": "030129001", "name": "支重轮", "type": "FINISHED_PRODUCT",
|
|
|
|
|
|
"unit": "件", "stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0,
|
|
|
|
|
|
}],
|
|
|
|
|
|
"equipment": [{
|
|
|
|
|
|
"code": "JY01001", "name": "数控车床", "capabilities": ["GENERAL"],
|
|
|
|
|
|
"opStdTime": {"GENERAL": 1.0}, "movable": False, "moveTimeMin": 0,
|
|
|
|
|
|
"zone": "ZONE-A", "adaptableMolds": [], "availabilityRate": 0.95, "status": "RUNNING",
|
|
|
|
|
|
}],
|
|
|
|
|
|
"routing": [{
|
|
|
|
|
|
"productCode": "030129001", "productName": "支重轮", "seq": 10,
|
|
|
|
|
|
"operationCode": "NZ0027", "operationName": "精车",
|
|
|
|
|
|
"requireMold": False, "stdTimePerUnit": 30.0,
|
|
|
|
|
|
}],
|
|
|
|
|
|
"bom": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
summary = apply_sql_pack_to_world(world, flex, replace=True)
|
|
|
|
|
|
assert summary["orders"] == 1
|
|
|
|
|
|
assert any(o["orderNo"] == "SYS202309210001" for o in world["flexOrders"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_routing_binds_via_craftl_not_output_material():
|
|
|
|
|
|
"""工艺挂成品:用订单 craftl→料号,不能用中间件 output_material_code。"""
|
|
|
|
|
|
flex = sql_pack_to_flex({
|
|
|
|
|
|
"rowsByTable": {
|
|
|
|
|
|
"pl_order": [{
|
|
|
|
|
|
"code": "ORD1", "material_code": "FG001", "material_name": "成品A",
|
|
|
|
|
|
"quantity": 10, "planned_end_time": "2026-08-01", "is_delete": "0",
|
|
|
|
|
|
"craftl_code": "CRAFT-9", "level": 5,
|
|
|
|
|
|
}],
|
|
|
|
|
|
"md_craftl": [{"code": "CRAFT-9", "product_code": "FG001", "name": "成品A工艺"}],
|
|
|
|
|
|
"r_production_craftl": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"craftl_code": "CRAFT-9", "procedure_code": "OP10", "procedure_name": "粗车",
|
|
|
|
|
|
"output_material_code": "WIP-X", "sort_no": 10, "working_hours": 0.5,
|
|
|
|
|
|
"is_delete": "0",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"craftl_code": "CRAFT-9", "procedure_code": "OP20", "procedure_name": "精车",
|
|
|
|
|
|
"output_material_code": "FG001", "sort_no": 20, "working_hours": 0.8,
|
|
|
|
|
|
"is_delete": "0",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
"md_material": [],
|
|
|
|
|
|
"md_equipment": [{
|
|
|
|
|
|
"code": "EQ1", "name": "车床", "is_delete": "0",
|
|
|
|
|
|
}],
|
|
|
|
|
|
},
|
|
|
|
|
|
"counts": {},
|
|
|
|
|
|
})
|
|
|
|
|
|
assert flex["stats"]["orders"] == 1
|
|
|
|
|
|
pcs = {s["productCode"] for s in flex["routing"]}
|
|
|
|
|
|
assert "FG001" in pcs
|
|
|
|
|
|
assert "WIP-X" not in pcs
|
|
|
|
|
|
assert flex["stats"]["routedProducts"] == 1
|
|
|
|
|
|
assert flex["stats"]["missingRouting"] == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_songyue_sql_if_present():
|
2026-09-08 00:07:26 +08:00
|
|
|
|
root = external_dir("SONGYUE_SQL_DIR", "songyue")
|
2026-07-28 02:12:46 +08:00
|
|
|
|
sql = root / "mesdb_pro.sql"
|
|
|
|
|
|
if not sql.exists():
|
|
|
|
|
|
return
|
|
|
|
|
|
from server.importers.sql_pack import scan_sql_pack
|
|
|
|
|
|
scan = scan_sql_pack(
|
|
|
|
|
|
str(sql),
|
|
|
|
|
|
limits={
|
|
|
|
|
|
"orders": 80, "materials": 200, "routing": 2000,
|
|
|
|
|
|
"routing_prefer": 15000, "equipment": 50, "bom": 200, "craft": 500,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
assert "pl_order" in (scan.get("counts") or {})
|
|
|
|
|
|
flex = sql_pack_to_flex(scan)
|
|
|
|
|
|
assert flex["stats"]["orders"] >= 1
|
|
|
|
|
|
assert flex["stats"]["materials"] >= 1
|
|
|
|
|
|
# 现场库有完整工艺时,订单产品应大部分挂上工艺
|
|
|
|
|
|
if flex["stats"].get("orderProducts", 0) >= 10:
|
|
|
|
|
|
ratio = flex["stats"]["routedProducts"] / max(flex["stats"]["orderProducts"], 1)
|
|
|
|
|
|
assert ratio >= 0.7, flex["stats"]
|