2026-08-11 00:54:05 +08:00
|
|
|
|
# ============================================================
|
2026-07-28 02:12:46 +08:00
|
|
|
|
# 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),
|
2026-08-11 00:54:05 +08:00
|
|
|
|
"expectedArrivalDate": row.get("expectedArrivalDate") or "",
|
|
|
|
|
|
"sourcingType": row.get("sourcingType") or "",
|
|
|
|
|
|
"sourcingTypeSource": row.get("sourcingTypeSource") or ("IMPORT" if row.get("sourcingType") else ""),
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"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),
|
|
|
|
|
|
}
|