560 lines
24 KiB
Python
560 lines
24 KiB
Python
# ============================================================
|
||
# MOM 主数据收集表导入(moduleId: importers-mom-pack, 可重生 ✅)
|
||
# 识别「01-生产模型 / 02-工艺模型 / 04-物料模型 / 07-设备模型」多页工作簿,
|
||
# 写入 flexMaterials / flexBom / flexEquipment / flexOrders(缺工艺时生成钣金默认路线占位)。
|
||
# 「02-工艺模型」在锐扬表里实际是多层 BOM(物料清单),不是工序步骤表。
|
||
# ============================================================
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import re
|
||
from typing import Any
|
||
|
||
World = dict[str, Any]
|
||
|
||
_SHEET_HINTS = ("生产模型", "工艺模型", "物料模型", "设备模型")
|
||
|
||
|
||
def is_mom_workbook(path: str) -> bool:
|
||
"""根据文件名或 sheet 名判断是否为 MOM 主数据收集表。"""
|
||
name = os.path.basename(path)
|
||
if re.search(r"MOM|主数据收集", name, re.I):
|
||
return True
|
||
try:
|
||
from openpyxl import load_workbook
|
||
wb = load_workbook(path, read_only=True, data_only=True)
|
||
titles = " ".join(wb.sheetnames)
|
||
wb.close()
|
||
return sum(1 for h in _SHEET_HINTS if h in titles) >= 2
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
# 整表替换(replace=True)只认「生产模型主干」行:物料 / BOM / 订单 / 工艺路线。
|
||
# 刻意不含另外三类:
|
||
# - flexOperations:解析器对任何工作簿都会生成 5 条钣金默认模板工序,空表也有;
|
||
# - flexZones / flexEquipment:车间与设备属辅助台账,单独出现不足以证明
|
||
# 这是一份可整体替换的主数据收集表(例:只有一张 01-生产模型、一行车间记录)。
|
||
_MOM_CORE_KEYS = ("flexMaterials", "flexBom", "flexOrders", "flexRoutings")
|
||
|
||
# 整表替换还要求结构证据:工作簿至少含 2 张模型表(与 is_mom_workbook 不靠文件名时的
|
||
# 判定同口径)。只有文件名匹配、或单表 + 解析出的标签行,都不足以背书清空既有主数据。
|
||
_MOM_MIN_MODEL_SHEETS = 2
|
||
|
||
# 重复/畸形表头行:不得造出「伪物料」主干行(02-工艺模型里 ERP品号/品号 已有同类先例)。
|
||
_HEADER_ROW_LABELS = frozenset({
|
||
"ERP品号", "品号", "物料编码", "物料名称", "产品编码", "产品名称",
|
||
"编码", "名称", "品名",
|
||
})
|
||
|
||
# 模板填写指引行 / 占位行:MOM 模板在数据区留了「填写说明」「请填写真实物料信息」之类的说明文字,
|
||
# 客户未填写时这些行很容易被当成真实物料(并派生出假订单)。既不能入库,也不能作为整表替换证据。
|
||
# 一律「前缀 / 整值」匹配,不做任意位置子串匹配——「说明书」「测试夹具」这类真实行不能被误杀。
|
||
_PLACEHOLDER_CODE_PREFIXES = (
|
||
"填写", "填寫", "填表", "请填", "請填", "待填", "未填", "必填", "选填", "選填",
|
||
"示例", "样例", "模板", "占位", "测试", "演示", "假数据", "无效",
|
||
"test", "demo", "sample", "dummy", "placeholder", "tmp", "temp", "xxx", "xxxx",
|
||
)
|
||
# 名称侧更窄:客户可能真有「测试夹具」这类物料名,所以名称不认 测试/演示 前缀。
|
||
_PLACEHOLDER_NAME_PREFIXES = (
|
||
"填写", "填寫", "填表", "请填", "請填", "待填", "未填",
|
||
"示例", "样例", "模板", "占位",
|
||
)
|
||
_PLACEHOLDER_VALUES = frozenset({
|
||
"说明", "备注", "提示", "注意", "模板", "无", "-", "--", "占位", "测试", "test",
|
||
"demo", "sample", "dummy", "placeholder", "xx", "xxx", "xxxx", "n/a", "na", "tbd",
|
||
"待定", "待补", "待补充", "null", "none",
|
||
})
|
||
|
||
# 主干证据还要「像真实业务编码」:模板说明文字通常不含字母/数字,而真实主数据编码几乎总是
|
||
# 字母数字混排(A0050101-00280、FG-001、SO-001)。
|
||
_ALNUM_RE = re.compile(r"[0-9A-Za-z]")
|
||
|
||
# 每类主干行的证据字段:(标识字段, 展示字段)。标识字段必须非占位且至少一个含字母/数字。
|
||
_MOM_CORE_EVIDENCE_FIELDS: dict[str, tuple[tuple[str, ...], tuple[str, ...]]] = {
|
||
"flexMaterials": (("code",), ("name",)),
|
||
"flexBom": (("productCode", "materialCode"), ()),
|
||
"flexOrders": (("productCode",), ("productName",)),
|
||
"flexRoutings": (("productCode",), ("productName",)),
|
||
}
|
||
|
||
|
||
def mom_pack_row_count(pack: dict[str, Any]) -> int:
|
||
"""MOM 解析结果的生产模型主干行数(物料/BOM/订单/工艺路线)。
|
||
|
||
空表、仅表头、只有辅助台账(车间/设备)、只剩模板填写指引行,或只是文件名带 MOM
|
||
的普通工作簿 → 0;
|
||
与 mom_pack_is_adoptable() 一起构成 replace=True 的 fail-closed 准入。
|
||
|
||
只认「能追到真实物料行」的主干行:BOM / 订单 / 工艺路线的产品编码必须落在被判定为
|
||
真实物料的编码集合里,模板占位物料派生出的假订单、假路线因此不计入。
|
||
"""
|
||
materials = [
|
||
row for row in (pack.get("flexMaterials") or [])
|
||
if _row_is_business_evidence(row, keys=_MOM_CORE_EVIDENCE_FIELDS["flexMaterials"][0])
|
||
]
|
||
material_codes = {_cell(row.get("code")) for row in materials}
|
||
total = len(materials)
|
||
for key in ("flexBom", "flexOrders", "flexRoutings"):
|
||
id_keys, _display_keys = _MOM_CORE_EVIDENCE_FIELDS[key]
|
||
for row in pack.get(key) or []:
|
||
if not _row_is_business_evidence(row, keys=id_keys):
|
||
continue
|
||
product_code = _cell(row.get("productCode"))
|
||
if key == "flexBom":
|
||
if (product_code in material_codes
|
||
and _cell(row.get("materialCode")) in material_codes):
|
||
total += 1
|
||
elif product_code in material_codes:
|
||
total += 1
|
||
return total
|
||
|
||
|
||
def mom_pack_is_adoptable(pack: dict[str, Any]) -> bool:
|
||
"""整表替换(replace=True)准入判定:结构证据 + 主干行证据,两者缺一不可。"""
|
||
if int(pack.get("modelSheetHits") or 0) < _MOM_MIN_MODEL_SHEETS:
|
||
return False
|
||
return mom_pack_row_count(pack) > 0
|
||
|
||
|
||
def _cell(v: Any) -> str:
|
||
if v is None:
|
||
return ""
|
||
return str(v).replace("\u200c", "").replace("\ufeff", "").strip()
|
||
|
||
|
||
def _is_placeholder_text(value: Any, *, side: str = "code") -> bool:
|
||
"""模板填写指引 / 占位文本:既不是主数据,也不能充当整表替换的证据。
|
||
|
||
side="code" 用编码侧前缀(含测试/演示类前缀),side="name" 用更窄的名称侧前缀;
|
||
匹配是前缀或整值,避免把「说明书」「测试夹具」这类真实行误杀。
|
||
"""
|
||
text = _cell(value)
|
||
if not text:
|
||
return False
|
||
lowered = text.lower()
|
||
if lowered in _PLACEHOLDER_VALUES:
|
||
return True
|
||
prefixes = _PLACEHOLDER_NAME_PREFIXES if side == "name" else _PLACEHOLDER_CODE_PREFIXES
|
||
return any(lowered.startswith(prefix) for prefix in prefixes)
|
||
|
||
|
||
def _row_is_business_evidence(row: Any, *, keys: tuple[str, ...]) -> bool:
|
||
"""主干行证据:标识字段非占位,且至少一个标识字段含字母/数字(真实业务编码形态)。"""
|
||
if not isinstance(row, dict):
|
||
return False
|
||
values = [_cell(row.get(key)) for key in keys]
|
||
values = [value for value in values if value]
|
||
if not values or any(_is_placeholder_text(value) for value in values):
|
||
return False
|
||
return any(_ALNUM_RE.search(value) for value in values)
|
||
|
||
|
||
def _sheet_matrix(path: str) -> dict[str, list[list[str]]]:
|
||
from openpyxl import load_workbook
|
||
wb = load_workbook(path, read_only=True, data_only=True)
|
||
out: dict[str, list[list[str]]] = {}
|
||
for ws in wb.worksheets:
|
||
rows: list[list[str]] = []
|
||
for row in ws.iter_rows(values_only=True):
|
||
rows.append([_cell(c) for c in (row or ())])
|
||
out[ws.title] = rows
|
||
wb.close()
|
||
return out
|
||
|
||
|
||
def _find_sheet(sheets: dict[str, list], *keywords: str) -> tuple[str, list[list[str]]] | None:
|
||
for title, rows in sheets.items():
|
||
if all(k in title for k in keywords):
|
||
return title, rows
|
||
if any(k in title for k in keywords) and len(keywords) == 1:
|
||
return title, rows
|
||
for title, rows in sheets.items():
|
||
if any(k in title for k in keywords):
|
||
return title, rows
|
||
return None
|
||
|
||
|
||
def _find_header(rows: list[list[str]], required: list[str]) -> tuple[int, dict[str, int]] | None:
|
||
"""找包含必备列名的表头行,返回 (row_index, {规范名: col_index})。"""
|
||
alias = {
|
||
"code": ("物料编码", "ERP品号", "品号", "财务编号", "厂内编号", "固定资产编号"),
|
||
"name": ("物料名称", "产品名称", "设备名称", "物料简称"),
|
||
"level": ("层次", "层级", "BOM层次"),
|
||
"qty": ("单套用量", "用量", "数量", "单位用量"),
|
||
"attr": ("品号属性", "物料来源", "来源"),
|
||
"unit": ("单位",),
|
||
"group": ("物料组名称", "物料组"),
|
||
"drawing": ("图号", "客户图号"),
|
||
"spec": ("设备型号", "型号"),
|
||
"equip_code": ("厂内编号", "财务编号", "固定资产编号"),
|
||
"asset_code": ("固定资产编号",),
|
||
"factory_code": ("出厂编号",),
|
||
"status": ("设备状态", "状态"),
|
||
}
|
||
for i, row in enumerate(rows[:40]):
|
||
joined: dict[str, int] = {}
|
||
for idx, cell in enumerate(row):
|
||
normalized = cell.replace(" ", "").strip()
|
||
if normalized:
|
||
joined.setdefault(normalized, idx)
|
||
if not joined:
|
||
continue
|
||
mapped: dict[str, int] = {}
|
||
for key, names in alias.items():
|
||
for n in names:
|
||
n2 = n.replace(" ", "")
|
||
if n2 in joined:
|
||
mapped[key] = joined[n2]
|
||
break
|
||
if all(r in mapped for r in required):
|
||
return i, mapped
|
||
return None
|
||
|
||
|
||
def _mat_type(attr: str, group: str = "") -> str:
|
||
text = f"{attr} {group}"
|
||
if any(k in text for k in ("外购", "采购", "原材料", "YCL", "P外")):
|
||
return "RAW_MATERIAL"
|
||
if any(k in text for k in ("半成品", "BCP", "焊接", "组件")):
|
||
return "SEMI_FINISHED"
|
||
if any(k in text for k in ("成品", "CP", "自制")):
|
||
return "FINISHED_PRODUCT"
|
||
if "M自制" in text:
|
||
return "SEMI_FINISHED"
|
||
return "RAW_MATERIAL"
|
||
|
||
|
||
def _parent_level(level: str) -> str | None:
|
||
level = level.strip()
|
||
if not level or "." not in level:
|
||
return None
|
||
return level.rsplit(".", 1)[0]
|
||
|
||
|
||
def parse_mom_workbook(path: str) -> dict[str, Any]:
|
||
"""解析 MOM xlsx → flex 结构(不写盘)。"""
|
||
sheets = _sheet_matrix(path)
|
||
materials: dict[str, dict] = {}
|
||
bom: list[dict] = []
|
||
equipment: list[dict] = []
|
||
zones: list[dict] = []
|
||
orders: list[dict] = []
|
||
root_code = ""
|
||
root_name = ""
|
||
customer = ""
|
||
|
||
# ---- 02 工艺模型 = BOM 树 ----
|
||
hit = _find_sheet(sheets, "工艺模型") or _find_sheet(sheets, "物料清单")
|
||
level_to_code: dict[str, str] = {}
|
||
if hit:
|
||
_, rows = hit
|
||
# 从元信息抓产品/客户
|
||
for row in rows[:8]:
|
||
for i, c in enumerate(row):
|
||
if c == "产品名称" and i + 1 < len(row) and row[i + 1]:
|
||
root_name = row[i + 1]
|
||
if c == "客户代码" and i + 1 < len(row) and row[i + 1]:
|
||
customer = row[i + 1]
|
||
if "客户料号" in c and i + 1 < len(row) and row[i + 1]:
|
||
pass
|
||
hdr = _find_header(rows, ["code", "name"])
|
||
if hdr:
|
||
hi, cols = hdr
|
||
for row in rows[hi + 1:]:
|
||
code = row[cols["code"]] if cols["code"] < len(row) else ""
|
||
name = row[cols["name"]] if cols["name"] < len(row) else ""
|
||
if not code or not name:
|
||
continue
|
||
if (code in _HEADER_ROW_LABELS or name in _HEADER_ROW_LABELS
|
||
or _is_placeholder_text(code)
|
||
or _is_placeholder_text(name, side="name")):
|
||
continue
|
||
level = row[cols["level"]] if "level" in cols and cols["level"] < len(row) else ""
|
||
qty_s = row[cols["qty"]] if "qty" in cols and cols["qty"] < len(row) else "1"
|
||
attr = row[cols["attr"]] if "attr" in cols and cols["attr"] < len(row) else ""
|
||
unit = row[cols["unit"]] if "unit" in cols and cols["unit"] < len(row) else "件"
|
||
try:
|
||
qty = float(qty_s) if qty_s else 1.0
|
||
except ValueError:
|
||
qty = 1.0
|
||
mtype = _mat_type(attr)
|
||
materials[code] = {
|
||
"code": code, "name": name, "type": mtype, "unit": unit or "件",
|
||
"stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0,
|
||
"spec": row[cols["drawing"]] if "drawing" in cols and cols["drawing"] < len(row) else "",
|
||
"sourcingType": "BUY" if mtype == "RAW_MATERIAL" else "MAKE",
|
||
}
|
||
if level:
|
||
level_to_code[level] = code
|
||
parent = _parent_level(level)
|
||
if parent and parent in level_to_code:
|
||
bom.append({
|
||
"productCode": level_to_code[parent],
|
||
"materialCode": code,
|
||
"quantity": qty or 1.0,
|
||
"isKey": qty >= 1,
|
||
})
|
||
if level == "1" or (not root_code and level in ("1", "1.0")):
|
||
root_code = code
|
||
root_name = root_name or name
|
||
|
||
# ---- 04 物料模型 ----
|
||
hit = _find_sheet(sheets, "物料模型")
|
||
if hit:
|
||
_, rows = hit
|
||
hdr = _find_header(rows, ["code", "name"])
|
||
if hdr:
|
||
hi, cols = hdr
|
||
for row in rows[hi + 1:]:
|
||
code = row[cols["code"]] if cols["code"] < len(row) else ""
|
||
name = row[cols["name"]] if cols["name"] < len(row) else ""
|
||
if (not code or not name
|
||
or code in _HEADER_ROW_LABELS or name in _HEADER_ROW_LABELS
|
||
or _is_placeholder_text(code)
|
||
or _is_placeholder_text(name, side="name")):
|
||
continue
|
||
group = row[cols["group"]] if "group" in cols and cols["group"] < len(row) else ""
|
||
attr = row[cols["attr"]] if "attr" in cols and cols["attr"] < len(row) else ""
|
||
unit = row[cols["unit"]] if "unit" in cols and cols["unit"] < len(row) else "件"
|
||
mtype = _mat_type(attr, group)
|
||
prev = materials.get(code) or {}
|
||
materials[code] = {
|
||
"code": code,
|
||
"name": name or prev.get("name") or code,
|
||
"type": mtype or prev.get("type") or "RAW_MATERIAL",
|
||
"unit": unit or prev.get("unit") or "件",
|
||
"stock": float(prev.get("stock") or 0),
|
||
"inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0,
|
||
"spec": (row[cols["drawing"]] if "drawing" in cols and cols["drawing"] < len(row) else "")
|
||
or prev.get("spec") or "",
|
||
"sourcingType": "BUY" if mtype == "RAW_MATERIAL" else "MAKE",
|
||
}
|
||
|
||
# ---- 07 设备 ----
|
||
hit = _find_sheet(sheets, "设备模型")
|
||
used_equipment_codes: set[str] = set()
|
||
if hit:
|
||
_, rows = hit
|
||
hdr = _find_header(rows, ["name"])
|
||
if hdr:
|
||
hi, cols = hdr
|
||
for row in rows[hi + 1:]:
|
||
name = row[cols["name"]] if cols["name"] < len(row) else ""
|
||
if not name or name in ("设备名称",):
|
||
continue
|
||
finance_code = row[cols["code"]] if "code" in cols and cols["code"] < len(row) else ""
|
||
internal_code = (
|
||
row[cols["equip_code"]]
|
||
if "equip_code" in cols and cols["equip_code"] < len(row) else ""
|
||
)
|
||
asset_code = (
|
||
row[cols["asset_code"]]
|
||
if "asset_code" in cols and cols["asset_code"] < len(row) else ""
|
||
)
|
||
factory_code = (
|
||
row[cols["factory_code"]]
|
||
if "factory_code" in cols and cols["factory_code"] < len(row) else ""
|
||
)
|
||
code = finance_code or internal_code or asset_code or factory_code
|
||
if not code:
|
||
# 财务编号列常在 name 前
|
||
for c in row:
|
||
if re.match(r"^[A-Z]\d", c):
|
||
code = c
|
||
break
|
||
if not code:
|
||
continue
|
||
if code.endswith(("编码", "编号")) or name in ("单位", "设备名称", "备件名称", "部件名称"):
|
||
continue
|
||
if code in used_equipment_codes:
|
||
for suffix in (internal_code, asset_code, factory_code):
|
||
candidate = f"{code}/{suffix}" if suffix else ""
|
||
if candidate and candidate not in used_equipment_codes:
|
||
code = candidate
|
||
break
|
||
else:
|
||
base = code
|
||
serial = 2
|
||
while f"{base}/{serial}" in used_equipment_codes:
|
||
serial += 1
|
||
code = f"{base}/{serial}"
|
||
used_equipment_codes.add(code)
|
||
status_raw = row[cols["status"]] if "status" in cols and cols["status"] < len(row) else "使用中"
|
||
running = status_raw in ("", "使用中", "RUNNING", "在用", "正常")
|
||
# 按设备名粗分能力
|
||
caps = ["GENERAL"]
|
||
if any(k in name for k in ("激光", "切割", "下料")):
|
||
caps = ["CUT", "GENERAL"]
|
||
elif any(k in name for k in ("折弯", "折板")):
|
||
caps = ["BEND", "GENERAL"]
|
||
elif any(k in name for k in ("焊",)):
|
||
caps = ["WELD", "GENERAL"]
|
||
elif any(k in name for k in ("喷涂", "喷粉")):
|
||
caps = ["PAINT", "GENERAL"]
|
||
equipment.append({
|
||
"code": code, "name": name,
|
||
"capabilities": caps,
|
||
"opStdTime": {c: 1.0 for c in caps},
|
||
"movable": False, "moveTimeMin": 0,
|
||
"zone": "ZONE-A", "adaptableMolds": [],
|
||
"availabilityRate": 0.9,
|
||
"status": "RUNNING" if running else "DOWN",
|
||
"spec": row[cols["spec"]] if "spec" in cols and cols["spec"] < len(row) else "",
|
||
"financeCode": finance_code,
|
||
"internalCode": internal_code,
|
||
"assetCode": asset_code,
|
||
"factoryCode": factory_code,
|
||
})
|
||
|
||
# ---- 01 生产模型 → 车间作区域 ----
|
||
hit = _find_sheet(sheets, "生产模型")
|
||
if hit:
|
||
_, rows = hit
|
||
mode = ""
|
||
for row in rows:
|
||
cells = [c for c in row if c]
|
||
head = row[0] if row else ""
|
||
if "车间管理" in head or (len(row) > 2 and row[2] == "车间名称"):
|
||
mode = "workshop"
|
||
continue
|
||
if "工段管理" in head or "工厂信息" in head:
|
||
mode = ""
|
||
continue
|
||
if mode == "workshop" and len(row) >= 5:
|
||
wname, wcode = row[2], row[4]
|
||
if wname and wcode and wname != "车间名称" and not wname.startswith("示例"):
|
||
zones.append({"code": wcode, "name": wname})
|
||
|
||
if root_code:
|
||
orders.append({
|
||
"orderNo": f"MOM-{root_code[-6:]}",
|
||
"productCode": root_code,
|
||
"productName": root_name or root_code,
|
||
"quantity": 1,
|
||
"dueDate": "",
|
||
"priority": 5,
|
||
"status": "RELEASED",
|
||
"customerName": customer or "MOM客户",
|
||
"customerLevel": "B",
|
||
})
|
||
# 确保根物料存在
|
||
materials.setdefault(root_code, {
|
||
"code": root_code, "name": root_name or root_code,
|
||
"type": "FINISHED_PRODUCT", "unit": "件",
|
||
"stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0,
|
||
"sourcingType": "MAKE",
|
||
})
|
||
|
||
# 自制件默认钣金工艺占位(表内无工序步骤列)
|
||
routings: list[dict] = []
|
||
ops = [
|
||
("CUT", "下料/激光切割", 5.0),
|
||
("BEND", "折弯", 3.0),
|
||
("WELD", "焊接", 8.0),
|
||
("PAINT", "喷涂", 6.0),
|
||
("ASM", "组装", 4.0),
|
||
]
|
||
make_codes = [
|
||
m["code"] for m in materials.values()
|
||
if m.get("type") in ("FINISHED_PRODUCT", "SEMI_FINISHED") and m.get("sourcingType") == "MAKE"
|
||
]
|
||
for pc in make_codes[:80]:
|
||
for seq, (ocode, oname, std) in enumerate(ops, 1):
|
||
routings.append({
|
||
"productCode": pc, "productName": materials[pc]["name"],
|
||
"seq": seq, "operationCode": ocode, "operationName": oname,
|
||
"requireMold": False, "stdTimePerUnit": std, "stdTimeSource": "模板",
|
||
"sourcingType": "MAKE", "isExternal": False,
|
||
})
|
||
|
||
return {
|
||
"filename": os.path.basename(path),
|
||
# 按「工作表张数」计,不按命中的 hint 数量:一个叫「生产模型-工艺模型」的单表
|
||
# 不能算作两张模型表的结构证据。
|
||
"modelSheetHits": sum(
|
||
1 for title in sheets if any(hint in title for hint in _SHEET_HINTS)
|
||
),
|
||
"flexMaterials": list(materials.values()),
|
||
"flexBom": bom,
|
||
"flexEquipment": equipment,
|
||
"flexZones": zones,
|
||
"flexOrders": orders,
|
||
"flexRoutings": routings,
|
||
"flexOperations": [
|
||
{"code": o, "name": n, "type": "INTERNAL", "sourcingType": "MAKE", "changeoverMin": 10}
|
||
for o, n, _ in ops
|
||
],
|
||
"stats": {
|
||
"materials": len(materials),
|
||
"bom": len(bom),
|
||
"equipment": len(equipment),
|
||
"zones": len(zones),
|
||
"orders": len(orders),
|
||
"routings": len(routings),
|
||
"rootProduct": root_code,
|
||
"note": "02-工艺模型按BOM树解析;工序为钣金默认模板占位,可在主数据中改",
|
||
},
|
||
}
|
||
|
||
|
||
def apply_mom_pack_to_world(world: World, pack: dict[str, Any], *, replace: bool = True) -> dict[str, int]:
|
||
"""把 MOM 解析结果写入世界。"""
|
||
summary: dict[str, int] = {}
|
||
mapping = [
|
||
("flexMaterials", "materials"),
|
||
("flexBom", "bom"),
|
||
("flexEquipment", "equipment"),
|
||
("flexZones", "zones"),
|
||
("flexOrders", "orders"),
|
||
("flexRoutings", "routings"),
|
||
("flexOperations", "operations"),
|
||
]
|
||
for key, label in mapping:
|
||
rows = pack.get(key) or []
|
||
if replace:
|
||
world[key] = []
|
||
bucket = world.setdefault(key, [])
|
||
if key in ("flexMaterials", "flexEquipment", "flexZones", "flexOperations"):
|
||
by_code = {str(x.get("code")): x for x in bucket if x.get("code")}
|
||
for i, row in enumerate(rows, 1):
|
||
code = str(row.get("code") or "")
|
||
payload = dict(row)
|
||
payload.setdefault("id", len(by_code) + i)
|
||
if code and code in by_code:
|
||
by_code[code].update(payload)
|
||
else:
|
||
bucket.append(payload)
|
||
if code:
|
||
by_code[code] = payload
|
||
else:
|
||
for i, row in enumerate(rows, 1):
|
||
payload = dict(row)
|
||
payload.setdefault("id", len(bucket) + i)
|
||
bucket.append(payload)
|
||
summary[label] = len(rows)
|
||
|
||
# 投影经典主数据,便于工艺模型页展示
|
||
try:
|
||
from server.importers.sql_pack import sync_flex_to_classic_master
|
||
sync_flex_to_classic_master(world)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
from server.aps_domain.orders import sync_flex_orders_to_sales
|
||
sync_flex_orders_to_sales(world)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
from server.aps_domain.sourcing import annotate_world_sourcing
|
||
annotate_world_sourcing(world)
|
||
except Exception:
|
||
pass
|
||
return summary
|
||
|
||
|
||
def import_mom_excel(world: World, path: str, *, replace: bool = True) -> dict[str, Any]:
|
||
pack = parse_mom_workbook(path)
|
||
applied = apply_mom_pack_to_world(world, pack, replace=replace)
|
||
return {"filename": pack.get("filename"), "stats": pack.get("stats"), "applied": applied}
|