1068 lines
42 KiB
Python
1068 lines
42 KiB
Python
# ============================================================
|
||
# 现场「完整生产路线」导入(moduleId: domain-kangni-intake, 可重生 ✅)
|
||
# 将康尼现场 Excel 落到 flex* 键,替换演示种子,供 PoolEngine 排产。
|
||
# 口径:docs/product/demand-data-intake.md;缺口用显式推断并写入 flexParams.siteInferences。
|
||
# 客户差异(工厂命名/工时推断规则/默认路径)收敛在 server/importers/profiles/kangni.json,
|
||
# 本模块仅保留解析逻辑 + 兜底默认值(profile 缺失时可独立运行)。
|
||
# ============================================================
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
World = dict[str, Any]
|
||
|
||
|
||
def _profile() -> dict[str, Any]:
|
||
"""加载康尼导入 profile(配置文件缺失时返回空 dict → 走内置兜底)。"""
|
||
try:
|
||
from server.importers.excel_importer import load_profile
|
||
return load_profile("kangni")
|
||
except Exception:
|
||
return {}
|
||
|
||
|
||
_P = _profile()
|
||
|
||
# 现场默认路径(profile 可覆盖;再可被环境变量 / API / 脚本覆盖)
|
||
DEFAULT_ROUTE_XLSX = Path(_P.get("defaultRouteXlsx") or (
|
||
r"D:\ItemSpace\14.工业智核\康尼\数据\outputs"
|
||
r"\019f8987-be3f-7261-a9d3-efc1ecf9c27d\订单102285668_完整生产路线.xlsx"
|
||
))
|
||
DEFAULT_DATA_DIR = Path(_P.get("dataDir") or r"D:\ItemSpace\14.工业智核\康尼\数据")
|
||
|
||
# 工时/产能缺口推断(源表「待维护」时启用;单位:分钟/件)
|
||
_INFER_STD_MIN = {k: float(v) for k, v in (_P.get("inferStdMin") or {"部装": 45.0, "装配": 30.0, "_default": 30.0}).items()}
|
||
_DEFAULT_STATION_COUNT = int(_P.get("stationCount") or 4)
|
||
_FLEX_CLEAR_KEYS = (
|
||
"flexZones", "flexOperations", "flexEquipment", "flexMolds",
|
||
"flexMaterials", "flexBom", "flexRoutings", "flexTeams", "flexCalendar",
|
||
"flexOrders", "flexParams",
|
||
"flexScheduleVersions", "flexVirtualLines", "flexWorkOrders", "flexConflicts",
|
||
)
|
||
|
||
# 固定轨 + 排产产物:加载现场时整表清空(去掉演示垃圾)
|
||
_FIXED_CLEAR_KEYS = (
|
||
"factories", "workshops", "lines", "workstations", "equipment",
|
||
"operations", "routings", "routingSteps", "materials", "boms", "bomItems",
|
||
"lineProducts", "workstationOperations", "teams", "shifts", "shiftCalendar",
|
||
"maintenance", "salesOrders", "productionOrders", "workOrders",
|
||
"purchaseOrders", "outsourceOrders", "forecastOrders", "changeoverMatrix",
|
||
"scheduleVersions", "conflicts", "logs",
|
||
)
|
||
|
||
|
||
def _clean(v: Any) -> str:
|
||
if v is None:
|
||
return ""
|
||
return str(v).replace("\u200b", "").strip()
|
||
|
||
|
||
def _num(v: Any, default: float = 0.0) -> float:
|
||
if v is None or v == "":
|
||
return default
|
||
if isinstance(v, (int, float)):
|
||
return float(v)
|
||
s = _clean(v).replace(",", "")
|
||
try:
|
||
return float(s)
|
||
except ValueError:
|
||
return default
|
||
|
||
|
||
def _is_pending(v: Any) -> bool:
|
||
s = _clean(v)
|
||
return (not s) or ("待维护" in s) or s.lower() in ("n/a", "na", "-", "—")
|
||
|
||
|
||
def _parse_dt_date(v: Any) -> str | None:
|
||
"""任意日期/时间 → YYYY-MM-DD。"""
|
||
if v is None or v == "":
|
||
return None
|
||
if isinstance(v, datetime):
|
||
return v.strftime("%Y-%m-%d")
|
||
s = _clean(v)
|
||
m = re.match(r"(\d{4}-\d{2}-\d{2})", s)
|
||
if m:
|
||
return m.group(1)
|
||
try:
|
||
return datetime.fromisoformat(s.replace("/", "-")).strftime("%Y-%m-%d")
|
||
except ValueError:
|
||
return None
|
||
|
||
|
||
def _infer_std_min(op_type: str, raw_hours: Any, infer_map: dict[str, float] | None = None) -> tuple[float, bool]:
|
||
"""返回 (分钟/件, 是否推断)。源表工时单位为小时;推断规则可由 profile 覆盖。"""
|
||
rules = infer_map or _INFER_STD_MIN
|
||
if not _is_pending(raw_hours):
|
||
h = _num(raw_hours, 0.0)
|
||
if h > 0:
|
||
return (h * 60.0, False)
|
||
t = _clean(op_type)
|
||
for key, mins in rules.items():
|
||
if key != "_default" and key in t:
|
||
return (float(mins), True)
|
||
return (float(rules.get("_default", 30.0)), True)
|
||
|
||
|
||
def _find_file(data_dir: Path, kind: str) -> Path | None:
|
||
"""按语义找源表(文件名乱码时靠 sheet 特征)。kind: orders|routing|bom|equipment|molds|materials。"""
|
||
if not data_dir.exists():
|
||
return None
|
||
try:
|
||
import openpyxl
|
||
except ImportError:
|
||
return None
|
||
name_hints = {
|
||
"orders": ("订单",),
|
||
"routing": ("工艺路线", "路线"),
|
||
"bom": ("BOM", "bom"),
|
||
"equipment": ("设备", "Equipment"),
|
||
"molds": ("模具", "Mould", "Mold"),
|
||
"materials": ("物料",),
|
||
}
|
||
for f in sorted(data_dir.glob("*.xlsx")):
|
||
if any(h.lower() in f.name.lower() for h in name_hints.get(kind, ())):
|
||
return f
|
||
for f in sorted(data_dir.glob("*.xlsx")):
|
||
try:
|
||
wb = openpyxl.load_workbook(f, read_only=True, data_only=True)
|
||
names = wb.sheetnames
|
||
# 读首行表头
|
||
ws = wb[names[0]]
|
||
header = []
|
||
for i, row in enumerate(ws.iter_rows(values_only=True)):
|
||
header = [_clean(c) for c in row]
|
||
break
|
||
wb.close()
|
||
except Exception:
|
||
continue
|
||
hs = "".join(header)
|
||
if kind == "orders" and ("生产订单" in hs or "WBS" in hs) and "计划" in hs:
|
||
return f
|
||
if kind == "routing" and "工序编号" in hs and "标准工时" in hs and "工序类型" in hs:
|
||
return f
|
||
if kind == "bom" and "MES工序编号" in hs and "物料编号" in hs:
|
||
return f
|
||
if kind == "equipment" and ("设备编号" in hs or names == ["Equipment"]):
|
||
return f
|
||
if kind == "molds" and ("工装模具" in hs or "模具编号" in hs or "ABC分类" in hs):
|
||
return f
|
||
if kind == "materials" and "物料代码" in hs:
|
||
return f
|
||
# 工艺/BOM 多 sheet 名为订单号
|
||
if kind in ("routing", "bom") and any(re.fullmatch(r"\d{6,}", n or "") for n in names):
|
||
# 再看第一个订单 sheet 表头
|
||
try:
|
||
wb = openpyxl.load_workbook(f, read_only=True, data_only=True)
|
||
order_sheets = [n for n in wb.sheetnames if re.fullmatch(r"\d{6,}", n)]
|
||
if not order_sheets:
|
||
wb.close()
|
||
continue
|
||
ws = wb[order_sheets[0]]
|
||
hdr = []
|
||
for row in ws.iter_rows(values_only=True):
|
||
hdr = [_clean(c) for c in row]
|
||
break
|
||
wb.close()
|
||
blob = "".join(hdr)
|
||
if kind == "routing" and "工序编号" in blob:
|
||
return f
|
||
if kind == "bom" and "物料编号" in blob:
|
||
return f
|
||
except Exception:
|
||
continue
|
||
return None
|
||
|
||
|
||
def parse_complete_route_xlsx(path: str | Path, infer_map: dict[str, float] | None = None) -> dict[str, Any]:
|
||
"""解析「订单XXXX_完整生产路线.xlsx」→ 单订单包(infer_map=profile 工时推断规则)。"""
|
||
import openpyxl
|
||
|
||
path = Path(path)
|
||
if not path.exists():
|
||
raise FileNotFoundError(f"找不到生产路线文件: {path}")
|
||
|
||
wb = openpyxl.load_workbook(path, data_only=True)
|
||
ws = wb.active
|
||
rows = [[ws.cell(r, c).value for c in range(1, ws.max_column + 1)]
|
||
for r in range(1, ws.max_row + 1)]
|
||
wb.close()
|
||
|
||
order: dict[str, Any] = {
|
||
"orderNo": "", "productCode": "", "productName": "", "drawingNo": "",
|
||
"quantity": 1.0, "planStart": None, "dueDate": None,
|
||
"wbs": "", "project": "", "status": "RELEASED", "kitStatus": "",
|
||
"operations": [], "bom": [], "source": str(path),
|
||
}
|
||
|
||
# ---- 头信息:扫前 15 行键值对 ----
|
||
for row in rows[:15]:
|
||
cells = [_clean(c) for c in row]
|
||
for i, cell in enumerate(cells):
|
||
nxt = cells[i + 1] if i + 1 < len(cells) else ""
|
||
if cell in ("订单代码", "订单号") and nxt:
|
||
order["orderNo"] = nxt
|
||
elif cell in ("产品料号", "料号") and nxt:
|
||
order["productCode"] = nxt
|
||
elif cell == "订单数量" and nxt:
|
||
order["quantity"] = _num(nxt, 1.0) or 1.0
|
||
elif cell in ("计划开始",) and nxt:
|
||
order["planStart"] = _parse_dt_date(nxt)
|
||
elif cell in ("计划结束", "交期") and nxt:
|
||
order["dueDate"] = _parse_dt_date(nxt)
|
||
elif cell == "WBS" and nxt:
|
||
order["wbs"] = nxt
|
||
elif cell == "项目" and nxt:
|
||
order["project"] = nxt
|
||
elif cell == "订单状态" and nxt:
|
||
order["status"] = "RELEASED" if "运行" in nxt or nxt in ("RELEASED", "已下达") else "CREATED"
|
||
elif cell == "备料状态" and nxt:
|
||
order["kitStatus"] = nxt
|
||
|
||
# 标题行补充产品名
|
||
if rows:
|
||
title = _clean(rows[1][0] if len(rows) > 1 else "")
|
||
m = re.search(r"产品\s*(\S+)[||].+?(?:图号\s*(\S+))?", title)
|
||
if m:
|
||
if not order["productCode"]:
|
||
order["productCode"] = m.group(1)
|
||
order["drawingNo"] = m.group(2) or ""
|
||
# 「产品 CODE|NAME|图号」
|
||
parts = re.split(r"[||]", title)
|
||
if len(parts) >= 2:
|
||
order["productName"] = parts[1].strip()
|
||
|
||
# ---- 工艺明细表 ----
|
||
op_header_idx = None
|
||
for i, row in enumerate(rows):
|
||
cells = [_clean(c) for c in row]
|
||
if "工序编号" in cells and ("标准工时" in "".join(cells) or "顺序" in cells):
|
||
op_header_idx = i
|
||
break
|
||
if op_header_idx is not None:
|
||
headers = [_clean(c) for c in rows[op_header_idx]]
|
||
col = {h: i for i, h in enumerate(headers) if h}
|
||
|
||
def _c(row, *names, default=""):
|
||
for n in names:
|
||
if n in col and col[n] < len(row):
|
||
return row[col[n]]
|
||
return default
|
||
|
||
for row in rows[op_header_idx + 1:]:
|
||
code = _clean(_c(row, "工序编号"))
|
||
if not code or not re.match(r"^[\w\-]+$", code):
|
||
# 遇到下一节标题则停
|
||
first = _clean(row[0]) if row else ""
|
||
if first.startswith("三、") or first.startswith("关键说明") or first.startswith("BOM"):
|
||
break
|
||
continue
|
||
name = _clean(_c(row, "工序名称"))
|
||
op_type = _clean(_c(row, "类型", "工序类型"))
|
||
raw_std = _c(row, "标准工时(h)", "标准工时")
|
||
std_min, inferred = _infer_std_min(op_type, raw_std, infer_map)
|
||
seq = int(_num(_c(row, "顺序", "序号", "排序号"), len(order["operations"]) + 1))
|
||
order["operations"].append({
|
||
"seq": seq * 10 if seq < 100 else seq,
|
||
"operationCode": code,
|
||
"operationName": name or code,
|
||
"opType": op_type,
|
||
"stdTimePerUnit": std_min,
|
||
"stdTimeInferred": inferred,
|
||
"resourceGroup": _clean(_c(row, "资源组")),
|
||
"equipmentHint": _clean(_c(row, "具体设备/工位")),
|
||
"moldHint": _clean(_c(row, "工装/模具")),
|
||
"requireMold": False, # 源表未匹配专用模具
|
||
})
|
||
|
||
# ---- BOM ----
|
||
bom_header_idx = None
|
||
for i, row in enumerate(rows):
|
||
cells = [_clean(c) for c in row]
|
||
if "物料编号" in cells or ("物料编码" in cells and "投料点" in cells):
|
||
bom_header_idx = i
|
||
break
|
||
if bom_header_idx is not None:
|
||
headers = [_clean(c) for c in rows[bom_header_idx]]
|
||
col = {h: i for i, h in enumerate(headers) if h}
|
||
|
||
def _b(row, *names, default=""):
|
||
for n in names:
|
||
if n in col and col[n] < len(row):
|
||
return row[col[n]]
|
||
return default
|
||
|
||
for row in rows[bom_header_idx + 1:]:
|
||
mat = _clean(_b(row, "物料编号", "物料编码"))
|
||
if not mat or mat.startswith("关键") or mat.startswith("BOM"):
|
||
first = _clean(row[0]) if row else ""
|
||
if first.startswith("关键") or first.startswith("四、"):
|
||
break
|
||
continue
|
||
# 过滤层级伪编码(M01 / 1.1.01 误入物料列)
|
||
if re.match(r"^M\d{2}$", mat) or re.match(r"^\d+\.\d+", mat) or len(mat) < 4:
|
||
continue
|
||
op_code = _clean(_b(row, "工序编号", "MES工序编号"))
|
||
qty = _num(_b(row, "单位用量", "用量", "数量"), 1.0)
|
||
name = _clean(_b(row, "物料名称", "名称"))
|
||
if not name and len(row) > 4:
|
||
name = _clean(row[4])
|
||
order["bom"].append({
|
||
"materialCode": mat,
|
||
"materialName": name or mat,
|
||
"quantity": qty,
|
||
"consumeOp": op_code or (order["operations"][0]["operationCode"] if order["operations"] else ""),
|
||
"isKey": qty >= 1.0,
|
||
})
|
||
|
||
if not order["orderNo"]:
|
||
raise ValueError(f"未能从 {path.name} 解析出订单号")
|
||
if not order["operations"]:
|
||
raise ValueError(f"{path.name} 无工艺工序明细,无法排产")
|
||
if not order["dueDate"]:
|
||
order["dueDate"] = order["planStart"] or datetime.now().strftime("%Y-%m-%d")
|
||
if not order["productName"]:
|
||
order["productName"] = order["productCode"] or order["orderNo"]
|
||
return order
|
||
|
||
|
||
def parse_orders_workbook(path: Path) -> list[dict[str, Any]]:
|
||
"""解析源表「订单.xlsx」。"""
|
||
import openpyxl
|
||
|
||
wb = openpyxl.load_workbook(path, data_only=True)
|
||
ws = wb[wb.sheetnames[0]]
|
||
rows = list(ws.iter_rows(values_only=True))
|
||
wb.close()
|
||
if not rows:
|
||
return []
|
||
headers = [_clean(c) for c in rows[0]]
|
||
col = {h: i for i, h in enumerate(headers) if h}
|
||
out = []
|
||
for row in rows[1:]:
|
||
def g(*names):
|
||
for n in names:
|
||
if n in col and col[n] < len(row):
|
||
return row[col[n]]
|
||
return None
|
||
|
||
ono = _clean(g("生产订单", "订单号", "订单代码"))
|
||
if not ono:
|
||
continue
|
||
out.append({
|
||
"orderNo": ono,
|
||
"wbs": _clean(g("WBS号", "WBS")),
|
||
"productCode": _clean(g("料号", "产品料号")),
|
||
"productName": _clean(g("物料名称", "产品名称")),
|
||
"drawingNo": _clean(g("图号")),
|
||
"planStart": _parse_dt_date(g("计划开始时间", "计划开始")),
|
||
"dueDate": _parse_dt_date(g("计划结束时间", "计划结束", "交期")),
|
||
"quantity": _num(g("订单数量", "数量"), 1.0) or 1.0,
|
||
"status": "RELEASED",
|
||
})
|
||
return out
|
||
|
||
|
||
def parse_routing_sheet(path: Path, order_no: str, infer_map: dict[str, float] | None = None) -> list[dict[str, Any]]:
|
||
"""从「工艺路线.xlsx」按订单 sheet 取工序。"""
|
||
import openpyxl
|
||
|
||
wb = openpyxl.load_workbook(path, data_only=True)
|
||
if order_no not in wb.sheetnames:
|
||
wb.close()
|
||
return []
|
||
ws = wb[order_no]
|
||
rows = list(ws.iter_rows(values_only=True))
|
||
wb.close()
|
||
if not rows:
|
||
return []
|
||
headers = [_clean(c) for c in rows[0]]
|
||
col = {h: i for i, h in enumerate(headers) if h}
|
||
ops = []
|
||
for row in rows[1:]:
|
||
def g(*names):
|
||
for n in names:
|
||
if n in col and col[n] < len(row):
|
||
return row[col[n]]
|
||
return None
|
||
|
||
code = _clean(g("工序编号"))
|
||
if not code:
|
||
continue
|
||
op_type = _clean(g("工序类型", "类型"))
|
||
std_min, inferred = _infer_std_min(op_type, g("标准工时"), infer_map)
|
||
seq_raw = _clean(g("排序号", "序号"))
|
||
try:
|
||
seq = int(float(seq_raw)) if seq_raw else (len(ops) + 1) * 10
|
||
except ValueError:
|
||
seq = (len(ops) + 1) * 10
|
||
ops.append({
|
||
"seq": seq,
|
||
"operationCode": code,
|
||
"operationName": _clean(g("工序名称")) or code,
|
||
"opType": op_type,
|
||
"stdTimePerUnit": std_min,
|
||
"stdTimeInferred": inferred,
|
||
"resourceGroup": "",
|
||
"requireMold": False,
|
||
})
|
||
return ops
|
||
|
||
|
||
def parse_bom_sheet(path: Path, order_no: str) -> list[dict[str, Any]]:
|
||
"""从「BOM.xlsx」按订单 sheet 取物料。"""
|
||
import openpyxl
|
||
|
||
wb = openpyxl.load_workbook(path, data_only=True)
|
||
if order_no not in wb.sheetnames:
|
||
wb.close()
|
||
return []
|
||
ws = wb[order_no]
|
||
rows = list(ws.iter_rows(values_only=True))
|
||
wb.close()
|
||
if not rows:
|
||
return []
|
||
headers = [_clean(c) for c in rows[0]]
|
||
col = {h: i for i, h in enumerate(headers) if h}
|
||
out = []
|
||
for row in rows[1:]:
|
||
def g(*names):
|
||
for n in names:
|
||
if n in col and col[n] < len(row):
|
||
return row[col[n]]
|
||
return None
|
||
|
||
mat = _clean(g("物料编号", "物料编码"))
|
||
if not mat:
|
||
continue
|
||
out.append({
|
||
"materialCode": mat,
|
||
"materialName": _clean(g("物料名称")) or mat,
|
||
"quantity": _num(g("单位用量", "用量"), 1.0),
|
||
"consumeOp": _clean(g("MES工序编号", "工序编号")),
|
||
"isKey": _num(g("单位用量", "用量"), 1.0) >= 1.0,
|
||
})
|
||
return out
|
||
|
||
|
||
def build_flex_bundle(
|
||
route_path: str | Path | None = None,
|
||
data_dir: str | Path | None = None,
|
||
*,
|
||
include_sibling_orders: bool = True,
|
||
station_count: int = _DEFAULT_STATION_COUNT,
|
||
profile: dict[str, Any] | None = None,
|
||
) -> dict[str, Any]:
|
||
"""构建可写入 world 的 flex* 包 + 推断说明。"""
|
||
prof = profile if profile is not None else _P
|
||
infer_map = prof.get("inferStdMin") or None
|
||
route_path = Path(route_path) if route_path else DEFAULT_ROUTE_XLSX
|
||
data_dir = Path(data_dir) if data_dir else DEFAULT_DATA_DIR
|
||
|
||
primary = parse_complete_route_xlsx(route_path, infer_map)
|
||
packages = [primary]
|
||
|
||
if include_sibling_orders and data_dir.exists():
|
||
orders_xlsx = _find_file(data_dir, "orders")
|
||
routing_xlsx = _find_file(data_dir, "routing")
|
||
bom_xlsx = _find_file(data_dir, "bom")
|
||
if orders_xlsx:
|
||
for od in parse_orders_workbook(orders_xlsx):
|
||
if od["orderNo"] == primary["orderNo"]:
|
||
# 用源表补齐主订单字段
|
||
for k in ("wbs", "productCode", "productName", "drawingNo", "planStart", "dueDate", "quantity"):
|
||
if od.get(k) and not primary.get(k):
|
||
primary[k] = od[k]
|
||
continue
|
||
ops = parse_routing_sheet(routing_xlsx, od["orderNo"], infer_map) if routing_xlsx else []
|
||
if not ops:
|
||
continue
|
||
bom = parse_bom_sheet(bom_xlsx, od["orderNo"]) if bom_xlsx else []
|
||
packages.append({
|
||
**od,
|
||
"kitStatus": "未知",
|
||
"operations": ops,
|
||
"bom": bom,
|
||
"source": str(orders_xlsx),
|
||
})
|
||
|
||
inferences: list[str] = []
|
||
chg_map = prof.get("changeoverMin") or {"部装": 15, "_default": 10}
|
||
ops_map: dict[str, dict] = {}
|
||
for pkg in packages:
|
||
for op in pkg["operations"]:
|
||
code = op["operationCode"]
|
||
if code not in ops_map:
|
||
chg = chg_map.get("_default", 10)
|
||
for key, mins in chg_map.items():
|
||
if key != "_default" and key in (op.get("opType") or ""):
|
||
chg = mins
|
||
break
|
||
ops_map[code] = {
|
||
"code": code,
|
||
"name": op["operationName"],
|
||
"isBottleneck": False,
|
||
"changeoverMin": chg,
|
||
}
|
||
if op.get("stdTimeInferred"):
|
||
inferences.append(
|
||
f"工序 {code} 标准工时源表待维护 → 推断 {op['stdTimePerUnit']} 分钟/件"
|
||
f"(类型={op.get('opType') or '默认'})"
|
||
)
|
||
|
||
# 部装标瓶颈(首道/部装类)
|
||
for code, op in ops_map.items():
|
||
if "部装" in op["name"] or code.endswith("Z1M") and "部装" in op["name"]:
|
||
op["isBottleneck"] = True
|
||
if not any(o["isBottleneck"] for o in ops_map.values()) and packages:
|
||
first = packages[0]["operations"][0]["operationCode"]
|
||
ops_map[first]["isBottleneck"] = True
|
||
inferences.append(f"未标注瓶颈 → 将首道工序 {first} 标为瓶颈")
|
||
|
||
all_op_codes = list(ops_map.keys())
|
||
zone_cfg = prof.get("zone") or {}
|
||
zone = {"code": zone_cfg.get("code") or "ZONE-CG", "name": zone_cfg.get("name") or "城轨机构装配区"}
|
||
ws_prefix = prof.get("stationCodePrefix") or "WS-CG-"
|
||
ws_name = prof.get("stationNamePrefix") or "城轨机构装配工位"
|
||
avail = float(prof.get("availabilityRate") or 0.95)
|
||
# 共享能力池:每台工位具备全部工序能力(柔性装配台)
|
||
op_std: dict[str, float] = {}
|
||
for pkg in packages:
|
||
for op in pkg["operations"]:
|
||
op_std.setdefault(op["operationCode"], op["stdTimePerUnit"])
|
||
|
||
equipment = []
|
||
for i in range(station_count):
|
||
code = f"{ws_prefix}{i + 1:02d}"
|
||
equipment.append({
|
||
"id": i + 1,
|
||
"code": code,
|
||
"name": f"{ws_name}#{i + 1}",
|
||
"capabilities": list(all_op_codes),
|
||
"opStdTime": dict(op_std),
|
||
"movable": False,
|
||
"moveTimeMin": 0,
|
||
"zone": zone["code"],
|
||
"adaptableMolds": [],
|
||
"availabilityRate": avail,
|
||
"status": "RUNNING",
|
||
"inferred": True,
|
||
})
|
||
inferences.append(
|
||
f"设备/工位源表待维护 → 合成 {station_count} 台共享装配工位 "
|
||
f"(能力=全部 {len(all_op_codes)} 道工序)"
|
||
)
|
||
inferences.append("模具未匹配到本产品 → requireMold=false,不占模具槽")
|
||
|
||
materials: dict[str, dict] = {}
|
||
bom_rows: list[dict] = []
|
||
routings: list[dict] = []
|
||
orders: list[dict] = []
|
||
|
||
for pkg in packages:
|
||
pc = pkg["productCode"] or f"P-{pkg['orderNo']}"
|
||
pn = pkg.get("productName") or pc
|
||
materials[pc] = {
|
||
"code": pc, "name": pn, "type": "FINISHED_PRODUCT", "unit": "套",
|
||
"stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0,
|
||
"drawingNo": pkg.get("drawingNo") or "",
|
||
}
|
||
for op in sorted(pkg["operations"], key=lambda x: x["seq"]):
|
||
routings.append({
|
||
"productCode": pc,
|
||
"productName": pn,
|
||
"seq": op["seq"],
|
||
"operationCode": op["operationCode"],
|
||
"requireMold": False,
|
||
"stdTimePerUnit": op["stdTimePerUnit"],
|
||
"stdTimeSource": "推断" if op.get("stdTimeInferred") else "实测",
|
||
})
|
||
kit_ready = "已完成" in (pkg.get("kitStatus") or "")
|
||
for b in pkg["bom"]:
|
||
mc = b["materialCode"]
|
||
need = float(b["quantity"]) * float(pkg["quantity"])
|
||
stock = max(need * 5.0, need + 10.0) if kit_ready else max(need * 2.0, 10.0)
|
||
if mc not in materials:
|
||
materials[mc] = {
|
||
"code": mc, "name": b["materialName"], "type": "RAW_MATERIAL",
|
||
"unit": "件", "stock": stock, "inTransit": 0,
|
||
"safetyStock": max(1.0, need * 0.1), "procurementLeadTime": 7,
|
||
}
|
||
else:
|
||
materials[mc]["stock"] = max(float(materials[mc].get("stock") or 0), stock)
|
||
consume = b.get("consumeOp") or (pkg["operations"][0]["operationCode"] if pkg["operations"] else "")
|
||
bom_rows.append({
|
||
"productCode": pc,
|
||
"materialCode": mc,
|
||
"quantity": float(b["quantity"]),
|
||
"consumeOp": consume,
|
||
"isKey": bool(b.get("isKey")),
|
||
})
|
||
if kit_ready:
|
||
inferences.append(f"订单 {pkg['orderNo']} 备料状态=已完成 → 原料库存按齐套放大")
|
||
|
||
orders.append({
|
||
"orderNo": pkg["orderNo"],
|
||
"productCode": pc,
|
||
"quantity": int(pkg["quantity"]) if float(pkg["quantity"]).is_integer() else float(pkg["quantity"]),
|
||
"dueDate": pkg["dueDate"],
|
||
"priority": 1 if pkg["orderNo"] == primary["orderNo"] else 2,
|
||
"wbs": pkg.get("wbs") or "",
|
||
"productionController": "现场导入",
|
||
"status": pkg.get("status") or "RELEASED",
|
||
"project": pkg.get("project") or "",
|
||
"sourceFile": pkg.get("source") or "",
|
||
})
|
||
|
||
# 去重 BOM(同产品+物料保留首条)
|
||
seen_bom: set[tuple[str, str]] = set()
|
||
uniq_bom = []
|
||
for b in bom_rows:
|
||
key = (b["productCode"], b["materialCode"])
|
||
if key in seen_bom:
|
||
continue
|
||
seen_bom.add(key)
|
||
uniq_bom.append(b)
|
||
|
||
team_cfg = prof.get("team") or {}
|
||
teams = [{
|
||
"code": team_cfg.get("code") or "T-CG-ASSY",
|
||
"name": team_cfg.get("name") or "城轨机构装配班组",
|
||
"memberCount": max(station_count, 4),
|
||
"supportOps": list(all_op_codes),
|
||
"skillLevel": team_cfg.get("skillLevel") or "L3",
|
||
}]
|
||
cal_cfg = prof.get("calendar") or {}
|
||
calendar = [{
|
||
"shiftCode": cal_cfg.get("shiftCode") or "D",
|
||
"startTime": cal_cfg.get("startTime") or "08:00",
|
||
"endTime": cal_cfg.get("endTime") or "17:00",
|
||
"breaks": cal_cfg.get("breaks") or [{"start": "12:00", "end": "13:00"}],
|
||
"workdays": cal_cfg.get("workdays") or [1, 2, 3, 4, 5],
|
||
}]
|
||
# 去重推断文案
|
||
uniq_inf = []
|
||
for line in inferences:
|
||
if line not in uniq_inf:
|
||
uniq_inf.append(line)
|
||
|
||
params = {
|
||
"sortMode": "BOTTLENECK",
|
||
"beforeDays": 0,
|
||
"afterDays": 30,
|
||
"rollingWindows": {"realtime": "60m", "long": "7d", "mid": "2d", "short": "2h"},
|
||
"mrpControllerDays": 3,
|
||
"weights": {"tardiness": 0.4, "cost": 0.3, "utilization": 0.2, "balance": 0.1},
|
||
"siteProfile": "complete-route",
|
||
"siteSource": str(route_path),
|
||
"siteInferences": uniq_inf,
|
||
"demoDataCleared": True,
|
||
}
|
||
|
||
return {
|
||
"flexZones": [zone],
|
||
"flexOperations": list(ops_map.values()),
|
||
"flexEquipment": equipment,
|
||
"flexMolds": [],
|
||
"flexMaterials": list(materials.values()),
|
||
"flexBom": uniq_bom,
|
||
"flexRoutings": routings,
|
||
"flexTeams": teams,
|
||
"flexCalendar": calendar,
|
||
"flexOrders": orders,
|
||
"flexParams": params,
|
||
"flexScheduleVersions": [],
|
||
"flexVirtualLines": [],
|
||
"flexWorkOrders": [],
|
||
"flexConflicts": [],
|
||
"_meta": {
|
||
"orderCount": len(orders),
|
||
"operationCount": len(ops_map),
|
||
"bomCount": len(uniq_bom),
|
||
"inferences": uniq_inf,
|
||
"primaryOrder": primary["orderNo"],
|
||
"source": str(route_path),
|
||
},
|
||
}
|
||
|
||
|
||
def build_fixed_master(
|
||
packages: list[dict[str, Any]],
|
||
*,
|
||
station_count: int = _DEFAULT_STATION_COUNT,
|
||
profile: dict[str, Any] | None = None,
|
||
) -> dict[str, Any]:
|
||
"""把现场订单包映射到主数据页/订单池读取的固定轨表(materials/operations/salesOrders…)。"""
|
||
from server.timeutil import add_minutes, fmt_date, today0
|
||
|
||
if not packages:
|
||
raise ValueError("无订单包可写入主数据")
|
||
primary = packages[0]
|
||
prof = profile if profile is not None else _P
|
||
f_cfg = prof.get("factory") or {}
|
||
w_cfg = prof.get("workshop") or {}
|
||
l_cfg = prof.get("line") or {}
|
||
ws_prefix = prof.get("stationCodePrefix") or "WS-CG-"
|
||
ws_name = prof.get("stationNamePrefix") or "城轨机构装配工位"
|
||
eq_prefix = prof.get("equipmentCodePrefix") or "EQ-CG-"
|
||
avail = float(prof.get("availabilityRate") or 0.95)
|
||
setup_cfg = prof.get("setupMin") or {"first": 15.0, "rest": 10.0}
|
||
lead_days = int(prof.get("rawMaterialLeadTimeDays") or 7)
|
||
family = prof.get("productFamily") or "CG-ASSY"
|
||
|
||
factories = [{
|
||
"id": 1, "code": f_cfg.get("code") or "CG-F01", "name": f_cfg.get("name") or "城轨机构装配工厂",
|
||
"timezone": f_cfg.get("timezone") or "Asia/Shanghai", "address": "现场导入", "status": "ACTIVE",
|
||
}]
|
||
workshops = [{
|
||
"id": 1, "factoryId": 1, "code": w_cfg.get("code") or "CG-WS01",
|
||
"name": w_cfg.get("name") or "城轨机构装配车间", "status": "ACTIVE",
|
||
}]
|
||
lines = [{
|
||
"id": 1, "workshopId": 1, "code": l_cfg.get("code") or "CG-L01",
|
||
"name": l_cfg.get("name") or "城轨机构装配线",
|
||
"capacityPerDay": max(4, station_count), "taktTime": float(l_cfg.get("taktTime") or 30.0),
|
||
"efficiencyFactor": float(l_cfg.get("efficiencyFactor") or 0.95),
|
||
"status": "ACTIVE", "alternativeLineIds": [],
|
||
}]
|
||
workstations = []
|
||
equipment = []
|
||
for i in range(station_count):
|
||
wsid = i + 1
|
||
code = f"{ws_prefix}{i + 1:02d}"
|
||
workstations.append({
|
||
"id": wsid, "lineId": 1, "code": code,
|
||
"name": f"{ws_name}#{i + 1}", "sequenceNo": i + 1, "status": "ACTIVE",
|
||
})
|
||
equipment.append({
|
||
"id": wsid, "code": f"{eq_prefix}{i + 1:02d}", "name": f"装配工位设备#{i + 1}",
|
||
"model": "SITE-ASSY", "workstationId": wsid, "capacityPerHour": 2,
|
||
"efficiencyFactor": 0.95, "availabilityRate": avail, "status": "RUNNING",
|
||
})
|
||
|
||
# 工序库(全订单并集)
|
||
ops_map: dict[str, dict] = {}
|
||
for pkg in packages:
|
||
for op in pkg["operations"]:
|
||
code = op["operationCode"]
|
||
if code not in ops_map:
|
||
ops_map[code] = {
|
||
"code": code,
|
||
"name": op["operationName"],
|
||
"type": "INTERNAL",
|
||
"standardTime": float(op["stdTimePerUnit"]),
|
||
"seq": op["seq"],
|
||
}
|
||
operations = []
|
||
for i, (_code, op) in enumerate(sorted(ops_map.items(), key=lambda x: x[1]["seq"])):
|
||
operations.append({
|
||
"id": i + 1, "code": op["code"], "name": op["name"],
|
||
"type": op["type"], "standardTime": op["standardTime"],
|
||
})
|
||
op_id_by_code = {o["code"]: o["id"] for o in operations}
|
||
|
||
materials: list[dict] = []
|
||
mat_id_by_code: dict[str, int] = {}
|
||
boms: list[dict] = []
|
||
bom_items: list[dict] = []
|
||
routings: list[dict] = []
|
||
routing_steps: list[dict] = []
|
||
line_products: list[dict] = []
|
||
sales_orders: list[dict] = []
|
||
mid = 1
|
||
bom_id = 1
|
||
rid = 1
|
||
step_id = 1
|
||
item_id = 1
|
||
lp_id = 1
|
||
|
||
for pkg in packages:
|
||
pc = pkg["productCode"] or f"P-{pkg['orderNo']}"
|
||
pn = pkg.get("productName") or pc
|
||
if pc not in mat_id_by_code:
|
||
materials.append({
|
||
"id": mid, "code": pc, "name": pn,
|
||
"spec": pkg.get("drawingNo") or "",
|
||
"type": "FINISHED_PRODUCT", "unit": "套",
|
||
"productFamily": family,
|
||
"safetyStock": 0, "procurementLeadTime": 0,
|
||
"stock": 0, "inTransit": 0, "status": "ACTIVE",
|
||
})
|
||
mat_id_by_code[pc] = mid
|
||
mid += 1
|
||
product_id = mat_id_by_code[pc]
|
||
|
||
kit_ready = "已完成" in (pkg.get("kitStatus") or "")
|
||
for b in pkg["bom"]:
|
||
mc = b["materialCode"]
|
||
need = float(b["quantity"]) * float(pkg["quantity"])
|
||
stock = max(need * 5.0, need + 10.0) if kit_ready else max(need * 2.0, 10.0)
|
||
if mc not in mat_id_by_code:
|
||
materials.append({
|
||
"id": mid, "code": mc, "name": b["materialName"],
|
||
"spec": "", "type": "RAW_MATERIAL", "unit": "件",
|
||
"productFamily": "",
|
||
"safetyStock": max(1.0, need * 0.1),
|
||
"procurementLeadTime": lead_days,
|
||
"stock": stock, "inTransit": 0, "status": "ACTIVE",
|
||
})
|
||
mat_id_by_code[mc] = mid
|
||
mid += 1
|
||
else:
|
||
# 同物料库存取较大
|
||
mrow = next(m for m in materials if m["id"] == mat_id_by_code[mc])
|
||
mrow["stock"] = max(float(mrow.get("stock") or 0), stock)
|
||
|
||
# BOM 头 + 明细(每产品一版)
|
||
boms.append({
|
||
"id": bom_id, "productId": product_id, "version": "V1.0",
|
||
"versionName": f"{pn} BOM(现场)", "isDefault": True, "status": "ACTIVE",
|
||
})
|
||
for b in pkg["bom"]:
|
||
consume = b.get("consumeOp") or ""
|
||
oid = op_id_by_code.get(consume) or (operations[0]["id"] if operations else None)
|
||
bom_items.append({
|
||
"id": item_id, "bomId": bom_id,
|
||
"materialId": mat_id_by_code[b["materialCode"]],
|
||
"quantity": float(b["quantity"]),
|
||
"operationId": oid,
|
||
"isKeyMaterial": bool(b.get("isKey")),
|
||
})
|
||
item_id += 1
|
||
bom_id += 1
|
||
|
||
# 工艺路线
|
||
routings.append({
|
||
"id": rid, "productId": product_id, "version": "V1.0",
|
||
"versionName": f"{pn} 工艺(现场)", "isDefault": True, "status": "ACTIVE",
|
||
})
|
||
prev_step = None
|
||
for seq_i, op in enumerate(sorted(pkg["operations"], key=lambda x: x["seq"]), start=1):
|
||
oid = op_id_by_code[op["operationCode"]]
|
||
routing_steps.append({
|
||
"id": step_id, "routingId": rid, "operationId": oid,
|
||
"sequenceNo": seq_i, "prevStepId": prev_step,
|
||
"setupTime": float(setup_cfg.get("first", 15.0)) if seq_i == 1 else float(setup_cfg.get("rest", 10.0)),
|
||
"runTimePerUnit": float(op["stdTimePerUnit"]),
|
||
"waitTime": 0, "transferTime": 0, "isExternal": False,
|
||
"stdTimeSource": "推断" if op.get("stdTimeInferred") else "实测",
|
||
})
|
||
prev_step = step_id
|
||
step_id += 1
|
||
rid += 1
|
||
|
||
line_products.append({
|
||
"id": lp_id, "lineId": 1, "productId": product_id,
|
||
"standardCapacity": max(4, station_count), "priority": 1, "setupTime": 30,
|
||
})
|
||
lp_id += 1
|
||
|
||
due = pkg.get("dueDate") or fmt_date(add_minutes(today0(), 14 * 24 * 60))
|
||
order_date = pkg.get("planStart") or fmt_date(today0())
|
||
so_id = len(sales_orders) + 1
|
||
sales_orders.append({
|
||
"id": so_id,
|
||
"orderNo": str(pkg["orderNo"]),
|
||
"customerId": "SITE-001",
|
||
"customerName": pkg.get("project") or "现场生产订单",
|
||
"customerLevel": "A",
|
||
"orderDate": order_date, "deliveryDate": due,
|
||
"priority": 1 if pkg is primary or pkg.get("orderNo") == primary["orderNo"] else 2,
|
||
"manualPriority": None, "status": "APPROVED",
|
||
"source": "SITE_XLSX",
|
||
"specialRequirements": f"WBS={pkg.get('wbs') or ''}",
|
||
"totalAmount": 0,
|
||
"isRush": False, "rushStrategy": None,
|
||
"changes": [], "createdBy": "site-import",
|
||
"createdAt": order_date + " 08:00", "updatedAt": order_date + " 08:00",
|
||
"wbs": pkg.get("wbs") or "",
|
||
"items": [{
|
||
"id": so_id * 10 + 1, "orderId": so_id, "lineNo": 1,
|
||
"productId": product_id, "productName": pn, "productCode": pc,
|
||
"quantity": int(pkg["quantity"]) if float(pkg["quantity"]).is_integer() else float(pkg["quantity"]),
|
||
"unit": "套", "bomVersion": "V1.0", "routingVersion": "V1.0",
|
||
"deliveryDate": due, "status": "OPEN",
|
||
}],
|
||
})
|
||
|
||
# 工位可执行全部工序
|
||
wso = []
|
||
wid = 1
|
||
for ws in workstations:
|
||
for op in operations:
|
||
wso.append({
|
||
"id": wid, "workstationId": ws["id"], "operationId": op["id"],
|
||
"setupTime": 10, "runTimePerUnit": op["standardTime"], "isPrimary": True,
|
||
})
|
||
wid += 1
|
||
|
||
team_cfg = prof.get("team") or {}
|
||
teams = [{
|
||
"id": 1, "code": team_cfg.get("code") or "T-CG-ASSY",
|
||
"name": team_cfg.get("name") or "城轨机构装配班组",
|
||
"workshopId": 1, "memberCount": max(station_count, 4),
|
||
"skillLevel": team_cfg.get("skillLevel") or "L3", "status": "ACTIVE",
|
||
}]
|
||
cal_cfg = prof.get("calendar") or {}
|
||
shifts = [{
|
||
"id": 1, "code": cal_cfg.get("shiftCode") or "D", "name": "早班",
|
||
"startTime": cal_cfg.get("startTime") or "08:00", "endTime": cal_cfg.get("endTime") or "17:00",
|
||
"breakPeriods": cal_cfg.get("breaks") or [{"start": "12:00", "end": "13:00"}],
|
||
"isOvertime": False, "status": "ACTIVE",
|
||
}]
|
||
base = today0()
|
||
shift_calendar = []
|
||
for i in range(int(prof.get("calendarDays") or 45)):
|
||
date = add_minutes(base, i * 24 * 60)
|
||
date_str = fmt_date(date)
|
||
is_weekend = date.weekday() >= 5
|
||
shift_calendar.append({
|
||
"id": i + 1, "lineId": 1, "date": date_str, "shiftId": 1,
|
||
"isWorking": not is_weekend, "teamId": 1, "maxWorkers": max(station_count, 4),
|
||
})
|
||
|
||
return {
|
||
"factories": factories,
|
||
"workshops": workshops,
|
||
"lines": lines,
|
||
"workstations": workstations,
|
||
"equipment": equipment,
|
||
"operations": operations,
|
||
"routings": routings,
|
||
"routingSteps": routing_steps,
|
||
"materials": materials,
|
||
"boms": boms,
|
||
"bomItems": bom_items,
|
||
"lineProducts": line_products,
|
||
"workstationOperations": wso,
|
||
"teams": teams,
|
||
"shifts": shifts,
|
||
"shiftCalendar": shift_calendar,
|
||
"maintenance": [],
|
||
"salesOrders": sales_orders,
|
||
"productionOrders": [],
|
||
"workOrders": [],
|
||
"purchaseOrders": [],
|
||
"outsourceOrders": [],
|
||
"forecastOrders": [],
|
||
"changeoverMatrix": [],
|
||
"scheduleVersions": [],
|
||
"conflicts": [],
|
||
"logs": [],
|
||
}
|
||
|
||
|
||
def apply_flex_bundle(world: World, bundle: dict[str, Any], *, replace: bool = True) -> dict[str, Any]:
|
||
"""将 flex 包写入 world;replace=True 时清空演示 flex*。"""
|
||
if replace:
|
||
for k in _FLEX_CLEAR_KEYS:
|
||
world[k] = [] if k != "flexParams" else {}
|
||
meta = bundle.pop("_meta", {})
|
||
# 订单补 id
|
||
orders = bundle.get("flexOrders") or []
|
||
for i, o in enumerate(orders):
|
||
o["id"] = i + 1
|
||
o.setdefault("status", "RELEASED")
|
||
for k, v in bundle.items():
|
||
if k.startswith("_"):
|
||
continue
|
||
world[k] = v
|
||
world.setdefault("flexParams", {})["demoDataCleared"] = True
|
||
return meta
|
||
|
||
|
||
def clear_demo_world(world: World) -> None:
|
||
"""清空固定轨 + 柔性轨演示/残留数据。"""
|
||
for k in _FIXED_CLEAR_KEYS:
|
||
world[k] = []
|
||
for k in _FLEX_CLEAR_KEYS:
|
||
world[k] = [] if k != "flexParams" else {}
|
||
|
||
|
||
def load_site_into_world(
|
||
world: World,
|
||
*,
|
||
route_path: str | Path | None = None,
|
||
data_dir: str | Path | None = None,
|
||
include_sibling_orders: bool = False,
|
||
station_count: int = _DEFAULT_STATION_COUNT,
|
||
clear_all: bool = True,
|
||
profile: dict[str, Any] | None = None,
|
||
) -> dict[str, Any]:
|
||
"""一键:清空演示 → 写入固定轨主数据+订单池 + flex* → 返回摘要。"""
|
||
prof = profile if profile is not None else _P
|
||
infer_map = prof.get("inferStdMin") or None
|
||
route_path = Path(route_path) if route_path else DEFAULT_ROUTE_XLSX
|
||
data_dir = Path(data_dir) if data_dir else DEFAULT_DATA_DIR
|
||
|
||
primary = parse_complete_route_xlsx(route_path, infer_map)
|
||
packages = [primary]
|
||
if include_sibling_orders and data_dir.exists():
|
||
# 仅当显式要求时带同源其它单
|
||
orders_xlsx = _find_file(data_dir, "orders")
|
||
routing_xlsx = _find_file(data_dir, "routing")
|
||
bom_xlsx = _find_file(data_dir, "bom")
|
||
if orders_xlsx:
|
||
for od in parse_orders_workbook(orders_xlsx):
|
||
if od["orderNo"] == primary["orderNo"]:
|
||
for k in ("wbs", "productCode", "productName", "drawingNo", "planStart", "dueDate", "quantity"):
|
||
if od.get(k) and not primary.get(k):
|
||
primary[k] = od[k]
|
||
continue
|
||
ops = parse_routing_sheet(routing_xlsx, od["orderNo"], infer_map) if routing_xlsx else []
|
||
if not ops:
|
||
continue
|
||
bom = parse_bom_sheet(bom_xlsx, od["orderNo"]) if bom_xlsx else []
|
||
packages.append({**od, "kitStatus": "未知", "operations": ops, "bom": bom, "source": str(orders_xlsx)})
|
||
|
||
if clear_all:
|
||
clear_demo_world(world)
|
||
|
||
fixed = build_fixed_master(packages, station_count=station_count, profile=prof)
|
||
for k, v in fixed.items():
|
||
world[k] = v
|
||
|
||
# flex 包:用同一 packages,避免再扫 siblings
|
||
bundle = build_flex_bundle(
|
||
route_path, data_dir,
|
||
include_sibling_orders=False,
|
||
station_count=station_count,
|
||
profile=prof,
|
||
)
|
||
# 若带了 siblings,重建 flex 订单集以与 packages 对齐
|
||
if len(packages) > 1:
|
||
bundle = build_flex_bundle(
|
||
route_path, data_dir,
|
||
include_sibling_orders=True,
|
||
station_count=station_count,
|
||
profile=prof,
|
||
)
|
||
meta = apply_flex_bundle(world, bundle, replace=False) # 已 clear_all
|
||
meta["fixedMaterials"] = len(world.get("materials") or [])
|
||
meta["fixedOperations"] = len(world.get("operations") or [])
|
||
meta["salesOrders"] = len(world.get("salesOrders") or [])
|
||
meta["clearedDemo"] = clear_all
|
||
# 工厂名写入审计友好字段
|
||
world.setdefault("flexParams", {})["siteProfile"] = "complete-route-full"
|
||
world["flexParams"]["fixedMasterSynced"] = True
|
||
return meta
|
||
|
||
|
||
def confirmation_for_site_load(meta: dict[str, Any]) -> tuple[str, list[str]]:
|
||
title = "加载现场完整生产路线(清空演示并写入主数据+订单)"
|
||
lines = [
|
||
f"主订单:{meta.get('primaryOrder')}",
|
||
f"订单数:{meta.get('orderCount')} · 工序:{meta.get('operationCount')} · BOM:{meta.get('bomCount')}",
|
||
f"主数据物料:{meta.get('fixedMaterials')} · 工序库:{meta.get('fixedOperations')} · 销售订单:{meta.get('salesOrders')}",
|
||
f"来源:{meta.get('source')}",
|
||
"将清空演示工厂全部垃圾数据,写入现场订单/物料/BOM/工艺/产线/柔性资源。",
|
||
]
|
||
for line in (meta.get("inferences") or [])[:6]:
|
||
lines.append(f"推断:{line}")
|
||
if len(meta.get("inferences") or []) > 6:
|
||
lines.append(f"…另有 {len(meta['inferences']) - 6} 条推断说明")
|
||
return title, lines
|