aps-agent/server/importers/sql_pack.py

972 lines
38 KiB
Python
Raw Normal View History

# ============================================================
# MySQL/Navicat SQL 数据包解析(moduleId: importer-sql-pack, 可重生 ✅)
# 流式读取超大 dump(如松岳 mesdb_pro.sql ~1GB),抽取排产相关表 → flex*
# ============================================================
from __future__ import annotations
import os
import re
from datetime import datetime
from typing import Any, Iterable
# 表 → 角色(松岳 MES / 通用命名)
_TABLE_KIND: dict[str, str] = {
"pl_order": "orders",
"r_plan_order": "orders",
"pl_order_bom": "bom",
"md_material": "materials",
"md_item": "materials",
"md_equipment": "equipment",
"em_devices": "equipment",
"r_production_craftl": "routing",
"c_project_process_route": "routing",
"md_craftl": "craft",
"r_production_procedure": "op_equip",
"pl_work_order": "work_orders",
}
_CREATE_RE = re.compile(r"CREATE TABLE\s+`([^`]+)`", re.I)
_INSERT_RE = re.compile(r"INSERT INTO\s+`([^`]+)`\s+VALUES\s*", re.I)
# 单表预览/导入上限(防 1GB 全量撑爆内存)
_DEFAULT_LIMITS = {
"orders": 800,
"materials": 4000,
"bom": 8000,
"routing": 8000, # 其它工艺上限
"routing_prefer": 25000, # 订单相关工艺路线优先配额
"equipment": 800,
"craft": 2000,
"op_equip": 4000,
"work_orders": 3000,
}
def _split_sql_values(blob: str) -> list[tuple[str, bool]]:
"""拆分 INSERT VALUES 元组内的字段 → (文本, 是否引号字符串)。"""
out: list[tuple[str, bool]] = []
cur: list[str] = []
i = 0
in_str = False
quoted = False
n = len(blob)
while i < n:
ch = blob[i]
if in_str:
if ch == "\\" and i + 1 < n:
cur.append(blob[i + 1])
i += 2
continue
if ch == "'":
if i + 1 < n and blob[i + 1] == "'":
cur.append("'")
i += 2
continue
in_str = False
i += 1
continue
cur.append(ch)
i += 1
continue
if ch == "'":
in_str = True
quoted = True
i += 1
continue
if ch == ",":
out.append(("".join(cur).strip(), quoted))
cur = []
quoted = False
i += 1
continue
cur.append(ch)
i += 1
if cur or blob.endswith(","):
out.append(("".join(cur).strip(), quoted))
return out
def _parse_value(raw: str, *, quoted: bool = False) -> Any:
s = (raw or "").strip()
if not s or s.upper() == "NULL":
return None
if quoted:
return s # 引号字段保持字符串(料号 030129001 不能丢前导 0)
if (s.startswith("'") and s.endswith("'")) or (s.startswith('"') and s.endswith('"')):
return s[1:-1]
try:
if "." in s:
return float(s)
return int(s)
except ValueError:
return s
def _iter_insert_tuples(line: str) -> Iterable[list[Any]]:
"""从一行或多行 INSERT 语句产出行值列表。"""
m = _INSERT_RE.search(line)
if not m:
return
rest = line[m.end():].strip()
if rest.endswith(";"):
rest = rest[:-1]
# 多个 (...),(...)
i = 0
n = len(rest)
while i < n:
while i < n and rest[i] in " \t\r\n,":
i += 1
if i >= n or rest[i] != "(":
break
depth = 0
in_str = False
j = i
while j < n:
ch = rest[j]
if in_str:
if ch == "\\" and j + 1 < n:
j += 2
continue
if ch == "'":
if j + 1 < n and rest[j + 1] == "'":
j += 2
continue
in_str = False
j += 1
continue
if ch == "'":
in_str = True
j += 1
continue
if ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if depth == 0:
inner = rest[i + 1:j]
yield [_parse_value(x, quoted=q) for x, q in _split_sql_values(inner)]
i = j + 1
break
j += 1
else:
break
def _parse_create_columns(block: str) -> list[str]:
"""解析 CREATE TABLE 列名;支持一行多列(测试/精简 dump)与 Navicat 一行一列。"""
cols: list[str] = []
for line in block.splitlines():
s = line.strip().upper()
if s.startswith(("PRIMARY ", "UNIQUE ", "KEY ", "INDEX ", "CONSTRAINT ")):
continue
if s.startswith(")"):
break
for m in re.finditer(r"`([^`]+)`\s+[a-zA-Z]", line):
name = m.group(1)
if name.upper() in ("PRIMARY", "UNIQUE", "KEY", "INDEX"):
continue
cols.append(name)
return cols
def _due_str(v: Any) -> str:
if v is None:
return ""
if isinstance(v, datetime):
return v.strftime("%Y-%m-%d")
s = str(v).strip()
m = re.match(r"(\d{4}-\d{2}-\d{2})", s)
return m.group(1) if m else s[:10]
def _row_dict(cols: list[str], vals: list[Any]) -> dict[str, Any]:
d: dict[str, Any] = {}
for i, c in enumerate(cols):
if i < len(vals):
d[c] = vals[i]
return d
def scan_sql_pack(
path: str,
*,
limits: dict[str, int] | None = None,
) -> dict[str, Any]:
"""流式扫描 SQL dump,返回按 kind 分组的行 + 表统计。"""
lim = {**_DEFAULT_LIMITS, **(limits or {})}
schemas: dict[str, list[str]] = {}
rows_by_table: dict[str, list[dict[str, Any]]] = {}
counts: dict[str, int] = {}
creating: str | None = None
create_buf: list[str] = []
size = os.path.getsize(path) if os.path.isfile(path) else 0
try:
from server.agent_core.progress import emit_thinking
except Exception:
def emit_thinking(*_a, **_k): # type: ignore
return None
emit_thinking("读取 SQL 数据包", f"{os.path.basename(path)} · {size / (1024 ** 2):.0f} MB", pct=1)
lines_read = 0
bytes_read = 0
last_emit = 0
# 订单上的工艺路线编码:dump 里 pl_order 早于 r_production_craftl,可优先抽取
prefer_craftl: set[str] = set()
routing_pref: list[dict[str, Any]] = []
routing_other: list[dict[str, Any]] = []
routing_table = "r_production_craftl"
with open(path, "r", encoding="utf-8", errors="replace") as f:
for line in f:
lines_read += 1
bytes_read += len(line.encode("utf-8", errors="ignore"))
if creating is not None:
create_buf.append(line)
if "ENGINE" in line.upper() and line.strip().endswith(";"):
schemas[creating] = _parse_create_columns("".join(create_buf))
creating = None
create_buf = []
continue
cm = _CREATE_RE.search(line)
if cm:
name = cm.group(1)
if name in _TABLE_KIND:
creating = name
create_buf = [line]
emit_thinking("解析表结构", name, pct=min(40, 5 + len(schemas)))
continue
if "INSERT INTO" not in line.upper():
if lines_read - last_emit >= 400_000:
last_emit = lines_read
pct = min(85.0, 10 + (bytes_read / max(size, 1)) * 70)
emit_thinking("扫描 SQL 全文", f"已读 {lines_read // 10000 / 10:.1f} 万行", pct=pct)
continue
im = _INSERT_RE.search(line)
if not im:
continue
table = im.group(1)
kind = _TABLE_KIND.get(table)
if not kind:
continue
cols = schemas.get(table) or []
if not cols:
continue
# 支持 INSERT 跨行:VALUES 在下一行,直到分号结束
insert_blob = line
while ";" not in insert_blob:
nxt = f.readline()
if not nxt:
break
lines_read += 1
bytes_read += len(nxt.encode("utf-8", errors="ignore"))
insert_blob += nxt
before = counts.get(table, 0)
for vals in _iter_insert_tuples(insert_blob):
counts[table] = counts.get(table, 0) + 1
row = _row_dict(cols, vals)
if kind == "orders":
cc = str(row.get("craftl_code") or "").strip()
if cc:
prefer_craftl.add(cc)
bucket = rows_by_table.setdefault(table, [])
if len(bucket) < lim.get("orders", 800):
bucket.append(row)
elif kind == "routing":
cc = str(row.get("craftl_code") or row.get("process_id") or "").strip()
if cc and cc in prefer_craftl:
if len(routing_pref) < lim.get("routing_prefer", 25000):
routing_pref.append(row)
elif len(routing_other) < lim.get("routing", 8000):
routing_other.append(row)
else:
bucket = rows_by_table.setdefault(table, [])
cap = lim.get(kind, 1000)
if len(bucket) < cap:
bucket.append(row)
if counts.get(table, 0) and before == 0:
emit_thinking("抽取业务表", f"{table} → {kind}", pct=min(90, 20 + len(counts) * 5))
if lines_read - last_emit >= 400_000:
last_emit = lines_read
pct = min(90.0, 10 + (bytes_read / max(size, 1)) * 75)
emit_thinking(
"扫描 SQL 全文",
f"已读 {lines_read // 10000 / 10:.1f} 万行 · 订单工艺 {len(prefer_craftl)} 条",
pct=pct,
)
if routing_pref or routing_other:
rows_by_table[routing_table] = routing_pref + routing_other
emit_thinking(
"工艺优先装载",
f"订单相关 {len(routing_pref)} 步 · 其它 {len(routing_other)} 步 · 覆盖工艺编码 {len(prefer_craftl)}",
status="done", pct=91,
)
emit_thinking("SQL 扫描完成", f"命中 {len(counts)} 张相关表", status="done", pct=92)
return {
"path": path,
"size": size,
"schemas": {k: v for k, v in schemas.items()},
"counts": counts,
"rowsByTable": rows_by_table,
"preferCraftl": sorted(prefer_craftl),
"kinds": sorted({_TABLE_KIND[t] for t in counts}),
}
def _map_orders(rows: list[dict]) -> list[dict]:
out = []
for r in rows:
if str(r.get("is_delete") or "0") not in ("0", "0.0", "", "None"):
continue
ono = str(r.get("code") or r.get("order_code") or "").strip()
pc = str(r.get("material_code") or r.get("product_code") or "").strip()
if not ono or not pc:
continue
qty = float(r.get("quantity") or r.get("quatity") or r.get("all_quantity") or 0)
if qty <= 0:
qty = 1
due = _due_str(r.get("planned_end_time") or r.get("end_time") or r.get("actual_end_time"))
if not due:
due = datetime.now().strftime("%Y-%m-%d")
out.append({
"orderNo": ono,
"productCode": pc,
"productName": str(r.get("material_name") or r.get("product_name") or pc),
"quantity": int(qty) if qty == int(qty) else qty,
"dueDate": due,
"priority": int(float(r.get("level") or 5) or 5),
"customerName": str(r.get("customer_business_code") or r.get("dept") or "松岳现场"),
"status": "RELEASED",
"drawingNo": str(r.get("drawing_code") or r.get("figure_no") or ""),
"machineModel": str(r.get("machine_model") or r.get("model") or ""),
"craftlCode": str(r.get("craftl_code") or "").strip(),
})
return out
def _map_materials(rows: list[dict]) -> list[dict]:
out = []
seen: set[str] = set()
for r in rows:
if str(r.get("is_delete") or "0") not in ("0", "0.0", "", "None"):
continue
code = str(r.get("code") or "").strip()
name = str(r.get("name") or code).strip()
if not code or code in seen:
continue
seen.add(code)
raw_type = str(r.get("type") or r.get("classify_name") or "").upper()
type_cn = str(r.get("type") or "")
if "毛坯" in name or "原料" in type_cn or raw_type in ("RM", "RAW"):
mtype = "RAW_MATERIAL"
elif "半" in type_cn or raw_type in ("SF", "SEMI"):
mtype = "SEMI_FINISHED"
elif "成品" in type_cn or raw_type in ("FG", "FINISHED"):
mtype = "FINISHED_PRODUCT"
else:
# 松岳料号习惯:01* 毛坯/原料,其余先按原料,有工艺时再升为半成品
mtype = "RAW_MATERIAL" if code.startswith("01") else "SEMI_FINISHED"
sourcing = "BUY" if mtype == "RAW_MATERIAL" or "毛坯" in name else "MAKE"
out.append({
"code": code, "name": name, "type": mtype,
"sourcingType": sourcing,
"unit": str(r.get("unit") or "件").strip('"') or "件",
"stock": float(r.get("stock") or 0),
"inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0,
"spec": str(r.get("specification_type") or r.get("figure_no") or ""),
})
return out
def _map_equipment(rows: list[dict]) -> list[dict]:
out = []
seen: set[str] = set()
for r in rows:
if str(r.get("is_delete") or r.get("delete_flag") or "0") not in ("0", "0.0", "", "None"):
continue
code = str(r.get("code") or r.get("devices_code") or "").strip()
name = str(r.get("name") or r.get("devices_name") or code).strip()
if not code or code in seen:
continue
seen.add(code)
out.append({
"code": code, "name": name,
"capabilities": ["GENERAL"],
"opStdTime": {"GENERAL": 1.0},
"movable": False, "moveTimeMin": 0,
"zone": "ZONE-A", "adaptableMolds": [],
"availabilityRate": 0.95, "status": "RUNNING",
})
return out
def _map_routing(rows: list[dict], craft_product: dict[str, str]) -> list[dict]:
"""工艺步骤挂到成品:优先 craftl_code→订单/工艺主数据料号,禁止用中间件料号冒充成品。"""
out = []
for r in rows:
if str(r.get("is_delete") or r.get("del_flag") or "0") not in ("0", "0.0", "", "None"):
continue
op = str(r.get("procedure_code") or r.get("process_code") or "").strip()
if not op:
continue
craft = str(r.get("craftl_code") or r.get("process_id") or "").strip()
out_mat = str(r.get("output_material_code") or r.get("product_code") or "").strip()
# 成品优先:工艺编码映射 > 输出料号(仅当映射缺失)
pc = craft_product.get(craft) or out_mat or craft
if not pc:
continue
try:
seq = int(float(r.get("sort_no") or r.get("process_sort") or r.get("serial_number") or 10))
except (TypeError, ValueError):
seq = 10
if seq <= 0:
seq = 10
std = r.get("working_hours") or r.get("process_shift_output")
try:
std_f = float(std) if std not in (None, "") else None
except (TypeError, ValueError):
std_f = None
# 工时若像小时则转分钟
if std_f is not None and std_f > 0 and std_f < 24:
std_f = round(std_f * 60, 2)
if std_f is None or std_f <= 0:
std_f = 1.0
src = "推断"
else:
src = "实测"
pname = str(r.get("craftl_name") or r.get("output_material_name") or pc)
op_name = str(r.get("procedure_name") or r.get("process_name") or op)
from server.aps_domain.sourcing import infer_op_sourcing
sourcing = infer_op_sourcing(op, op_name)
out.append({
"productCode": pc,
"productName": pname,
"seq": seq,
"operationCode": op,
"operationName": op_name,
"requireMold": False,
"stdTimePerUnit": std_f,
"stdTimeSource": src,
"craftlCode": craft,
"outputMaterialCode": out_mat,
"sourcingType": sourcing,
"isExternal": sourcing == "OUTSOURCE",
})
return out
def _expand_routing_for_orders(
orders: list[dict],
steps: list[dict],
) -> list[dict]:
"""同一 craftl 下的步骤,按订单成品料号再挂一份(多订单共用工艺)。"""
by_craft: dict[str, list[dict]] = {}
for s in steps:
cc = str(s.get("craftlCode") or "").strip()
if cc:
by_craft.setdefault(cc, []).append(s)
out: list[dict] = []
seen: set[tuple] = set()
for o in orders:
pc = str(o.get("productCode") or "").strip()
cc = str(o.get("craftlCode") or "").strip()
if not pc:
continue
src = by_craft.get(cc) or [s for s in steps if s.get("productCode") == pc]
for s in src:
key = (pc, s.get("seq"), s.get("operationCode"))
if key in seen:
continue
seen.add(key)
row = dict(s)
row["productCode"] = pc
row["productName"] = o.get("productName") or pc
out.append(row)
# 保留未能挂到订单的步骤(其它产品)
for s in steps:
key = (s.get("productCode"), s.get("seq"), s.get("operationCode"))
if key not in seen:
out.append(s)
seen.add(key)
return out
def _map_bom(rows: list[dict], order_product: dict[str, str]) -> list[dict]:
out = []
for r in rows:
if str(r.get("is_delete") or "0") not in ("0", "0.0", "", "None"):
continue
ono = str(r.get("order_code") or "").strip()
mc = str(r.get("material_code") or "").strip()
pc = order_product.get(ono) or str(r.get("machine_model") or "").strip()
qty = float(r.get("composition_quantity") or r.get("consumption_quantity") or r.get("quantity") or 0)
if not pc or not mc or qty <= 0:
continue
out.append({
"productCode": pc, "materialCode": mc, "quantity": qty,
"consumeOp": str(r.get("procedure_code") or ""),
"isKey": qty >= 1,
"materialName": str(r.get("material_name") or mc),
})
return out
def _apply_op_equip(equip: list[dict], rows: list[dict]) -> None:
"""用工序-设备关系补全 capabilities。"""
by_code = {e["code"]: e for e in equip}
for r in rows:
if str(r.get("is_delete") or "0") not in ("0", "0.0", "", "None"):
continue
ec = str(r.get("equipment_code") or "").strip()
op = str(r.get("procedure_code") or "").strip()
if not ec or not op or ec not in by_code:
continue
e = by_code[ec]
caps = list(e.get("capabilities") or [])
if "GENERAL" in caps and len(caps) == 1:
caps = []
if op not in caps:
caps.append(op)
e["capabilities"] = caps or ["GENERAL"]
std = dict(e.get("opStdTime") or {})
if op not in std:
std[op] = 1.0
if "GENERAL" in std and op != "GENERAL" and len(std) > 1:
std.pop("GENERAL", None)
e["opStdTime"] = std
def sql_pack_to_flex(scan: dict[str, Any]) -> dict[str, Any]:
"""把 scan 结果映射为可写入 world 的 flex* 批次。"""
rbt = scan.get("rowsByTable") or {}
craft_product: dict[str, str] = {}
for r in rbt.get("md_craftl") or []:
code = str(r.get("code") or "").strip()
pc = str(r.get("product_code") or "").strip()
if code and pc:
craft_product[code] = pc
orders = _map_orders((rbt.get("pl_order") or []) + (rbt.get("r_plan_order") or []))
# 订单上的 craftl→成品料号优先(现场口径,覆盖 md_craftl 偏差)
for o in orders:
cc = str(o.get("craftlCode") or "").strip()
pc = str(o.get("productCode") or "").strip()
if cc and pc:
craft_product[cc] = pc
order_product = {o["orderNo"]: o["productCode"] for o in orders}
# 订单产品补进物料
materials = _map_materials((rbt.get("md_material") or []) + (rbt.get("md_item") or []))
mat_codes = {m["code"] for m in materials}
for o in orders:
if o["productCode"] not in mat_codes:
materials.append({
"code": o["productCode"], "name": o.get("productName") or o["productCode"],
"type": "FINISHED_PRODUCT", "unit": "件",
"stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0, "spec": "",
})
mat_codes.add(o["productCode"])
equipment = _map_equipment((rbt.get("md_equipment") or []) + (rbt.get("em_devices") or []))
_apply_op_equip(equipment, rbt.get("r_production_procedure") or [])
routing_raw = _map_routing(
(rbt.get("r_production_craftl") or []) + (rbt.get("c_project_process_route") or []),
craft_product,
)
routing = _expand_routing_for_orders(orders, routing_raw)
# 设备能力:把订单工艺工序挂到设备(MES 设备表常无工序能力列)
ops_needed = sorted({str(s.get("operationCode") or "") for s in routing if s.get("operationCode")})
if ops_needed and equipment:
for e in equipment:
caps = [c for c in (e.get("capabilities") or []) if c and c != "GENERAL"]
if not caps:
# MES 设备台账常无工序能力列:按已挂工艺放开,否则齐备度永远卡 NO_CAPABLE
e["capabilities"] = list(ops_needed)
e["opStdTime"] = {op: float((e.get("opStdTime") or {}).get(op) or 1.0) for op in ops_needed}
bom = _map_bom(rbt.get("pl_order_bom") or [], order_product)
pcs_ord = {o["productCode"] for o in orders}
pcs_rt = {s["productCode"] for s in routing}
return {
"orders": orders,
"materials": materials,
"equipment": equipment,
"routing": routing,
"bom": bom,
"stats": {
"orders": len(orders), "materials": len(materials),
"equipment": len(equipment), "routing": len(routing), "bom": len(bom),
"orderProducts": len(pcs_ord),
"routedProducts": len(pcs_ord & pcs_rt),
"missingRouting": len(pcs_ord - pcs_rt),
"rawCounts": scan.get("counts") or {},
},
}
def sync_flex_to_classic_master(world: dict[str, Any]) -> dict[str, int]:
"""把 flex* 投影到主数据页读的经典表(materials/routings/…),避免工艺模型仍显示演示厂。"""
summary: dict[str, int] = {}
def _next(table: str) -> int:
items = world.get(table) or []
return max((x.get("id", 0) for x in items if isinstance(x.get("id"), int)), default=0) + 1
# 物料
mats = world.setdefault("materials", [])
code_to_id: dict[str, int] = {str(m.get("code")): int(m["id"]) for m in mats if m.get("id") is not None}
for row in world.get("flexMaterials") or []:
code = str(row.get("code") or "").strip()
if not code:
continue
payload = {
"code": code,
"name": row.get("name") or code,
"spec": row.get("spec") or "",
"type": row.get("type") or "FINISHED_PRODUCT",
"unit": row.get("unit") or "件",
"productFamily": row.get("productFamily") or "",
"safetyStock": float(row.get("safetyStock") or 0),
"procurementLeadTime": float(row.get("procurementLeadTime") or 0),
"stock": float(row.get("stock") or 0),
"inTransit": float(row.get("inTransit") or 0),
"status": "ACTIVE",
}
if code in code_to_id:
ex = next(m for m in mats if m.get("code") == code)
ex.update(payload)
else:
mid = _next("materials")
mats.append({"id": mid, **payload})
code_to_id[code] = mid
summary["materials"] = summary.get("materials", 0) + 1
# 工序库
ops = world.setdefault("operations", [])
op_code_to_id: dict[str, int] = {str(o.get("code")): int(o["id"]) for o in ops if o.get("id") is not None}
for row in world.get("flexOperations") or []:
code = str(row.get("code") or "").strip()
if not code:
continue
if code in op_code_to_id:
continue
oid = _next("operations")
from server.aps_domain.sourcing import infer_op_sourcing
sourcing = infer_op_sourcing(code, row.get("name"))
ops.append({
"id": oid, "code": code, "name": row.get("name") or code,
"type": "EXTERNAL" if sourcing == "OUTSOURCE" else "INTERNAL",
"sourcingType": sourcing,
"standardTime": float((row.get("changeoverMin") or 0) or 0) / 60.0 or 0.5,
})
op_code_to_id[code] = oid
summary["operations"] = summary.get("operations", 0) + 1
# 工艺路线 + 步骤(按产品)
routings = world.setdefault("routings", [])
steps = world.setdefault("routingSteps", [])
live_pcs = {str(r.get("productCode") or "") for r in (world.get("flexRoutings") or [])}
if live_pcs:
drop_pids = {code_to_id[pc] for pc in live_pcs if pc in code_to_id}
drop_rids = {r["id"] for r in routings if r.get("productId") in drop_pids}
if drop_rids:
world["routings"] = [r for r in routings if r["id"] not in drop_rids]
world["routingSteps"] = [s for s in steps if s.get("routingId") not in drop_rids]
routings = world["routings"]
steps = world["routingSteps"]
by_pc: dict[str, list[dict]] = {}
for row in world.get("flexRoutings") or []:
pc = str(row.get("productCode") or "").strip()
if pc:
by_pc.setdefault(pc, []).append(row)
for pc, rows in by_pc.items():
pid = code_to_id.get(pc)
if not pid:
continue
rid = _next("routings")
routings.append({
"id": rid, "productId": pid, "version": "V1",
"versionName": f"{pc}工艺", "isDefault": True, "status": "ACTIVE",
})
rows_sorted = sorted(rows, key=lambda r: int(r.get("seq") or 0))
prev = None
for i, row in enumerate(rows_sorted):
op = str(row.get("operationCode") or "").strip()
if op and op not in op_code_to_id:
oid = _next("operations")
ops.append({
"id": oid, "code": op, "name": row.get("operationName") or op,
"type": "EXTERNAL" if (
row.get("isExternal") or
str(row.get("sourcingType") or "").upper() == "OUTSOURCE" or
str(op).upper().startswith("WZ")
) else "INTERNAL",
"sourcingType": row.get("sourcingType") or (
"OUTSOURCE" if str(op).upper().startswith("WZ") else "MAKE"
),
"standardTime": float(row.get("stdTimePerUnit") or 1) / 60.0,
})
op_code_to_id[op] = oid
oid = op_code_to_id.get(op)
if not oid:
continue
sid = _next("routingSteps")
std = float(row.get("stdTimePerUnit") or 1.0)
from server.aps_domain.sourcing import infer_op_sourcing
sourcing = row.get("sourcingType") or infer_op_sourcing(op, row.get("operationName"))
is_ext = bool(row.get("isExternal")) or sourcing == "OUTSOURCE"
# 同步工序主数据 type
for o in ops:
if o.get("id") == oid:
o["type"] = "EXTERNAL" if is_ext else o.get("type") or "INTERNAL"
o["sourcingType"] = sourcing
break
steps.append({
"id": sid, "routingId": rid, "operationId": oid,
"sequenceNo": i + 1, "prevStepId": prev,
"setupTime": 10, "runTimePerUnit": std, "waitTime": 0,
"transferTime": 0, "isExternal": is_ext,
"sourcingType": sourcing,
})
prev = sid
summary["routingSteps"] = summary.get("routingSteps", 0) + 1
if is_ext:
summary["externalSteps"] = summary.get("externalSteps", 0) + 1
summary["routings"] = summary.get("routings", 0) + 1
# BOM
boms = world.setdefault("boms", [])
bom_items = world.setdefault("bomItems", [])
bom_by_pc: dict[str, list[dict]] = {}
for row in world.get("flexBom") or []:
pc = str(row.get("productCode") or "").strip()
if pc:
bom_by_pc.setdefault(pc, []).append(row)
for pc, rows in bom_by_pc.items():
pid = code_to_id.get(pc)
if not pid:
continue
# 清旧
old = [b["id"] for b in boms if b.get("productId") == pid]
if old:
world["boms"] = [b for b in boms if b["id"] not in old]
world["bomItems"] = [bi for bi in bom_items if bi.get("bomId") not in old]
boms = world["boms"]
bom_items = world["bomItems"]
bid = _next("boms")
boms.append({
"id": bid, "productId": pid, "version": "V1",
"versionName": f"{pc}BOM", "isDefault": True, "status": "ACTIVE",
})
for row in rows:
mc = str(row.get("materialCode") or "").strip()
if not mc:
continue
if mc not in code_to_id:
mid = _next("materials")
mats.append({
"id": mid, "code": mc, "name": row.get("materialName") or mc,
"spec": "", "type": "RAW_MATERIAL", "unit": row.get("unit") or "件",
"productFamily": "", "safetyStock": 0, "procurementLeadTime": 0,
"stock": 0, "inTransit": 0, "status": "ACTIVE",
})
code_to_id[mc] = mid
bom_items.append({
"id": _next("bomItems"), "bomId": bid,
"materialId": code_to_id[mc],
"quantity": float(row.get("quantity") or 1),
"operationId": None, "isKeyMaterial": False,
})
summary["bomItems"] = summary.get("bomItems", 0) + 1
summary["boms"] = summary.get("boms", 0) + 1
# 资源树:无工厂时挂一个「现场」占位,设备挂到柔性资源页即可
if (world.get("flexEquipment") or []) and not (world.get("factories") or []):
world["factories"] = [{
"id": 1, "code": "SITE", "name": "现场工厂",
"timezone": "Asia/Shanghai", "address": "", "status": "ACTIVE",
}]
world["workshops"] = [{
"id": 1, "factoryId": 1, "code": "WS1", "name": "生产车间", "status": "ACTIVE",
}]
world["lines"] = [{
"id": 1, "workshopId": 1, "code": "FLEX", "name": "柔性能力池",
"capacityPerDay": 9999, "taktTime": 1, "efficiencyFactor": 1.0,
"status": "ACTIVE", "alternativeLineIds": [],
}]
summary["factories"] = 1
params = world.setdefault("flexParams", {})
params["demoDataCleared"] = True
params["siteProfile"] = params.get("siteProfile") or "sql-pack"
return summary
def apply_sql_pack_to_world(world: dict[str, Any], flex: dict[str, Any], *, replace: bool = True) -> dict[str, int]:
"""将 SQL 映射结果写入 world flex*,并投影到主数据页经典表。"""
from server.state.seed import ensure_flex_seed, purge_demo_residue
# 先清演示残留,再保证键存在(不会回填假数据)
purge_demo_residue(world)
world.setdefault("flexParams", {})["demoDataCleared"] = True
ensure_flex_seed(world)
summary: dict[str, int] = {}
def _next(table: str) -> int:
items = world.get(table) or []
return max((x.get("id", 0) for x in items if isinstance(x.get("id"), int)), default=0) + 1
if replace:
for k in ("flexOrders", "flexMaterials", "flexEquipment", "flexRoutings", "flexBom"):
world[k] = []
# 清掉演示厂班组,否则现场工序(NZ*/WZ*)会被 NO_TEAM 卡死
world["flexTeams"] = []
# 经典主数据也整表换新,禁止智能控制器与现场料号并存
for k in (
"materials", "boms", "bomItems", "operations", "routings", "routingSteps",
"lineProducts", "workstationOperations", "changeoverMatrix",
"factories", "workshops", "lines", "workstations", "equipment",
):
world[k] = []
# materials first
fm = world.setdefault("flexMaterials", [])
for row in flex.get("materials") or []:
ex = next((m for m in fm if m.get("code") == row["code"]), None)
if ex:
ex.update(row)
else:
fm.append({"id": _next("flexMaterials"), **row})
summary["materials"] = summary.get("materials", 0) + 1
fo = world.setdefault("flexOrders", [])
for row in flex.get("orders") or []:
ex = next((o for o in fo if o.get("orderNo") == row["orderNo"]), None)
if ex:
ex.update(row)
else:
fo.append({"id": _next("flexOrders"), **row})
summary["orders"] = summary.get("orders", 0) + 1
fe = world.setdefault("flexEquipment", [])
for row in flex.get("equipment") or []:
ex = next((e for e in fe if e.get("code") == row["code"]), None)
if ex:
ex.update(row)
else:
fe.append({"id": _next("flexEquipment"), **row})
summary["equipment"] = summary.get("equipment", 0) + 1
fr = world.setdefault("flexRoutings", [])
for row in flex.get("routing") or []:
fr[:] = [s for s in fr if not (
s.get("productCode") == row["productCode"] and s.get("seq") == row["seq"]
and s.get("operationCode") == row["operationCode"]
)]
fr.append(dict(row))
summary["routing"] = summary.get("routing", 0) + 1
fb = world.setdefault("flexBom", [])
for row in flex.get("bom") or []:
fb[:] = [s for s in fb if not (
s.get("productCode") == row["productCode"]
and s.get("materialCode") == row["materialCode"]
)]
fb.append(dict(row))
summary["bom"] = summary.get("bom", 0) + 1
# 默认班次
if not world.get("flexCalendar"):
world["flexCalendar"] = [{
"shiftCode": "D", "startTime": "08:00", "endTime": "17:00",
"breaks": [{"start": "12:00", "end": "13:00"}],
"workdays": [1, 2, 3, 4, 5],
}]
summary["calendar"] = 1
# 工序库
ops = world.setdefault("flexOperations", [])
seen_op = {o.get("code") for o in ops}
all_ops: list[str] = []
for row in flex.get("routing") or []:
code = row.get("operationCode")
if code and code not in seen_op:
ops.append({"code": code, "name": row.get("operationName") or code,
"isBottleneck": False, "changeoverMin": 10})
seen_op.add(code)
summary["operations"] = summary.get("operations", 0) + 1
if code and code not in all_ops:
all_ops.append(code)
# 现场综合班组:覆盖 SQL 工艺全部工序,避免演示厂班组资格卡死
if all_ops:
teams = world.setdefault("flexTeams", [])
plant = next((t for t in teams if t.get("code") == "T-PLANT"), None)
if plant is None:
plant = {
"code": "T-PLANT", "name": "现场综合班组",
"memberCount": max(8, min(40, len(world.get("flexEquipment") or []) or 8)),
"supportOps": [], "skillLevel": "L3",
}
teams.append(plant)
summary["teams"] = 1
for code in all_ops:
if code not in plant["supportOps"]:
plant["supportOps"].append(code)
# 投影到主数据「工艺模型」页(经典 materials/routings)
classic = sync_flex_to_classic_master(world)
for k, v in classic.items():
summary[f"classic_{k}"] = v
# 投影到订单管理页(salesOrders)
try:
from server.aps_domain.orders import sync_flex_orders_to_sales
n = sync_flex_orders_to_sales(world)
if n:
summary["salesOrders"] = n
except Exception:
pass
# 工序/物料供应类型标注(WZ*=委外,毛坯=采购)
try:
from server.aps_domain.sourcing import annotate_world_sourcing
summary["sourcing"] = annotate_world_sourcing(world)
except Exception:
pass
return summary
def preview_sql_file(path: str) -> dict[str, Any]:
"""给 folder_pack / 分析用的轻量预览。"""
scan = scan_sql_pack(path)
flex = sql_pack_to_flex(scan)
st = flex["stats"]
return {
"filename": os.path.basename(path),
"kind": "sql-pack",
"kindCn": "SQL 数据包",
"size": scan["size"],
"okCount": st["orders"] + st["materials"] + st["routing"] + st["equipment"],
"errorCount": 0,
"tables": [
{"table": t, "kind": _TABLE_KIND.get(t, ""), "rawCount": c,
"loaded": len((scan.get("rowsByTable") or {}).get(t) or [])}
for t, c in sorted((scan.get("counts") or {}).items())
],
"fieldMapText": (
f"订单{st['orders']} · 物料{st['materials']} · 工艺{st['routing']} · "
f"设备{st['equipment']} · BOM{st['bom']}"
),
"samples": [
f"订单 {o['orderNo']}:{o['productCode']}×{o['quantity']} 交期{o['dueDate']}"
for o in (flex.get("orders") or [])[:3]
],
"flex": flex,
"canSchedule": st["orders"] > 0 and (st["routing"] > 0 or st["equipment"] > 0),
}