387 lines
20 KiB
Python
387 lines
20 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 主数据维护领域服务(moduleId: domain-masterdata, 可重生 ✅)
|
|||
|
|
# MD-01/02/03 首切片:资源树 / 物料与BOM / 日历与维保 的只读投影
|
|||
|
|
# + 三个最小 P2 写动作(产线编辑停启用 / 物料库存编辑 / 维保新增取消)。
|
|||
|
|
# 规则:主数据写入影响后续新排产输入,必须由调用方走 Harness 确认卡;
|
|||
|
|
# 被排产版本引用的资源禁止物理删除,只能停用(INACTIVE)。
|
|||
|
|
# ============================================================
|
|||
|
|
from __future__ import annotations # 前向类型引用
|
|||
|
|
|
|||
|
|
from datetime import datetime # 时间戳
|
|||
|
|
from typing import Any # 类型标注
|
|||
|
|
|
|||
|
|
from server.timeutil import fmt_dt # 时间格式化
|
|||
|
|
|
|||
|
|
World = dict[str, Any] # 世界状态类型别名
|
|||
|
|
|
|||
|
|
# 主数据写动作白名单(Gateway 与 workflow 共用)
|
|||
|
|
MASTER_ACTIONS = ("master.line.upsert", "master.material.upsert", "master.maintenance.upsert",
|
|||
|
|
"master.bom.upsert", "master.routing.upsert")
|
|||
|
|
|
|||
|
|
# 产线/维保允许的状态集
|
|||
|
|
LINE_STATUSES = {"ACTIVE", "INACTIVE"}
|
|||
|
|
MAINTENANCE_STATUSES = {"PLANNED", "CANCELLED"}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _now() -> str:
|
|||
|
|
"""当前时间字符串(与世界状态 createdAt/updatedAt 口径一致)。"""
|
|||
|
|
return fmt_dt(datetime.now())
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------- P0 只读投影 ----------------
|
|||
|
|
|
|||
|
|
def master_overview(world: World) -> dict[str, Any]:
|
|||
|
|
"""主数据管理页全量投影(P0 只读;资源树 + 物料BOM + 日历维保)。"""
|
|||
|
|
# 产线被工单引用计数(未完成工单;确认卡影响面与"禁止删除"依据)
|
|||
|
|
line_wo_count: dict[int, int] = {}
|
|||
|
|
for wo in world.get("workOrders", []):
|
|||
|
|
if wo.get("status") not in ("COMPLETED", "CANCELLED"):
|
|||
|
|
line_wo_count[wo["lineId"]] = line_wo_count.get(wo["lineId"], 0) + 1
|
|||
|
|
|
|||
|
|
# ---- 资源树(工厂 → 车间 → 产线 → 工位;设备挂工位) ----
|
|||
|
|
equipment_by_ws = {}
|
|||
|
|
for eq in world.get("equipment", []):
|
|||
|
|
equipment_by_ws.setdefault(eq["workstationId"], []).append({
|
|||
|
|
"id": eq["id"], "code": eq["code"], "name": eq["name"],
|
|||
|
|
"model": eq.get("model", ""), "status": eq.get("status", "RUNNING"),
|
|||
|
|
})
|
|||
|
|
factories = []
|
|||
|
|
for f in world.get("factories", []):
|
|||
|
|
workshops = []
|
|||
|
|
for w in world.get("workshops", []):
|
|||
|
|
if w["factoryId"] != f["id"]:
|
|||
|
|
continue
|
|||
|
|
lines = []
|
|||
|
|
for ln in world.get("lines", []):
|
|||
|
|
if ln["workshopId"] != w["id"]:
|
|||
|
|
continue
|
|||
|
|
stations = [{
|
|||
|
|
"id": ws["id"], "code": ws["code"], "name": ws["name"],
|
|||
|
|
"sequenceNo": ws["sequenceNo"], "status": ws.get("status", "ACTIVE"),
|
|||
|
|
"equipment": equipment_by_ws.get(ws["id"], []),
|
|||
|
|
} for ws in world.get("workstations", []) if ws["lineId"] == ln["id"]]
|
|||
|
|
lines.append({
|
|||
|
|
"id": ln["id"], "code": ln["code"], "name": ln["name"],
|
|||
|
|
"capacityPerDay": ln["capacityPerDay"],
|
|||
|
|
"efficiencyFactor": ln.get("efficiencyFactor", 1.0),
|
|||
|
|
"status": ln.get("status", "ACTIVE"),
|
|||
|
|
"openWorkOrders": line_wo_count.get(ln["id"], 0),
|
|||
|
|
"workstations": stations,
|
|||
|
|
})
|
|||
|
|
workshops.append({"id": w["id"], "code": w["code"], "name": w["name"], "lines": lines})
|
|||
|
|
factories.append({"id": f["id"], "code": f["code"], "name": f["name"], "workshops": workshops})
|
|||
|
|
|
|||
|
|
# ---- 物料 / BOM / 工艺路线 ----
|
|||
|
|
materials = [{
|
|||
|
|
"id": m["id"], "code": m["code"], "name": m["name"], "spec": m.get("spec", ""),
|
|||
|
|
"type": m["type"], "unit": m["unit"],
|
|||
|
|
"stock": m.get("stock", 0), "inTransit": m.get("inTransit", 0),
|
|||
|
|
"safetyStock": m.get("safetyStock", 0),
|
|||
|
|
"procurementLeadTime": m.get("procurementLeadTime", 0),
|
|||
|
|
} for m in world.get("materials", [])]
|
|||
|
|
material_name = {m["id"]: m["name"] for m in world.get("materials", [])}
|
|||
|
|
boms = []
|
|||
|
|
for b in world.get("boms", []):
|
|||
|
|
items = [{
|
|||
|
|
"id": i["id"], "materialId": i["materialId"],
|
|||
|
|
"materialName": material_name.get(i["materialId"], f"#{i['materialId']}"),
|
|||
|
|
"quantity": i["quantity"], "isKeyMaterial": i.get("isKeyMaterial", False),
|
|||
|
|
} for i in world.get("bomItems", []) if i["bomId"] == b["id"]]
|
|||
|
|
boms.append({
|
|||
|
|
"id": b["id"], "productId": b["productId"],
|
|||
|
|
"productName": material_name.get(b["productId"], f"#{b['productId']}"),
|
|||
|
|
"version": b["version"], "isDefault": b.get("isDefault", False),
|
|||
|
|
"status": b.get("status", "ACTIVE"), "items": items,
|
|||
|
|
})
|
|||
|
|
op_name = {o["id"]: o["name"] for o in world.get("operations", [])}
|
|||
|
|
routings = []
|
|||
|
|
for r in world.get("routings", []):
|
|||
|
|
steps = sorted([s for s in world.get("routingSteps", []) if s["routingId"] == r["id"]],
|
|||
|
|
key=lambda s: s["sequenceNo"])
|
|||
|
|
routings.append({
|
|||
|
|
"id": r["id"], "productId": r["productId"],
|
|||
|
|
"productName": material_name.get(r["productId"], f"#{r['productId']}"),
|
|||
|
|
"version": r["version"], "isDefault": r.get("isDefault", False),
|
|||
|
|
"status": r.get("status", "ACTIVE"),
|
|||
|
|
"steps": [{
|
|||
|
|
"id": s["id"], "sequenceNo": s["sequenceNo"],
|
|||
|
|
"operationName": op_name.get(s["operationId"], f"OP#{s['operationId']}"),
|
|||
|
|
"setupTime": s["setupTime"], "runTimePerUnit": s["runTimePerUnit"],
|
|||
|
|
"isExternal": s.get("isExternal", False),
|
|||
|
|
} for s in steps],
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# ---- 班次 / 日历摘要 / 维保 ----
|
|||
|
|
shifts = [{
|
|||
|
|
"id": s["id"], "code": s["code"], "name": s["name"],
|
|||
|
|
"startTime": s["startTime"], "endTime": s["endTime"],
|
|||
|
|
"breakPeriods": s.get("breakPeriods", []),
|
|||
|
|
} for s in world.get("shifts", [])]
|
|||
|
|
line_name = {ln["id"]: ln["name"] for ln in world.get("lines", [])}
|
|||
|
|
# 日历摘要:逐产线统计未来工作日数量(页面展示概览,不铺 30 天明细)
|
|||
|
|
working_days: dict[int, set] = {}
|
|||
|
|
for sc in world.get("shiftCalendar", []):
|
|||
|
|
if sc.get("isWorking"):
|
|||
|
|
working_days.setdefault(sc["lineId"], set()).add(sc["date"])
|
|||
|
|
calendar = [{
|
|||
|
|
"lineId": lid, "lineName": line_name.get(lid, f"#{lid}"),
|
|||
|
|
"workingDays": len(days),
|
|||
|
|
} for lid, days in sorted(working_days.items())]
|
|||
|
|
equipment_name = {eq["id"]: eq["name"] for eq in world.get("equipment", [])}
|
|||
|
|
maintenance = [{
|
|||
|
|
"id": m["id"], "equipmentId": m["equipmentId"],
|
|||
|
|
"equipmentName": equipment_name.get(m["equipmentId"], f"#{m['equipmentId']}"),
|
|||
|
|
"type": m.get("type", "MAINTENANCE"),
|
|||
|
|
"plannedStart": m["plannedStart"], "plannedEnd": m["plannedEnd"],
|
|||
|
|
"status": m.get("status", "PLANNED"), "description": m.get("description", ""),
|
|||
|
|
} for m in world.get("maintenance", [])]
|
|||
|
|
|
|||
|
|
equipment_options = [{"id": eq["id"], "code": eq["code"], "name": eq["name"]}
|
|||
|
|
for eq in world.get("equipment", [])]
|
|||
|
|
return {
|
|||
|
|
"factories": factories,
|
|||
|
|
"materials": materials, "boms": boms, "routings": routings,
|
|||
|
|
"shifts": shifts, "calendar": calendar, "maintenance": maintenance,
|
|||
|
|
"equipmentOptions": equipment_options,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------- 校验(P0 纯校验) ----------------
|
|||
|
|
|
|||
|
|
def _find_line(world: World, line_id: int) -> dict[str, Any]:
|
|||
|
|
"""取产线;不存在抛 ValueError。"""
|
|||
|
|
line = next((ln for ln in world["lines"] if ln["id"] == line_id), None)
|
|||
|
|
if line is None:
|
|||
|
|
raise ValueError(f"产线不存在:{line_id}")
|
|||
|
|
return line
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _find_material(world: World, material_id: int) -> dict[str, Any]:
|
|||
|
|
"""取物料;不存在抛 ValueError。"""
|
|||
|
|
m = next((m for m in world["materials"] if m["id"] == material_id), None)
|
|||
|
|
if m is None:
|
|||
|
|
raise ValueError(f"物料不存在:{material_id}")
|
|||
|
|
return m
|
|||
|
|
|
|||
|
|
|
|||
|
|
def normalize_line_payload(world: World, payload: dict[str, Any]) -> dict[str, Any]:
|
|||
|
|
"""产线编辑载荷归一化(首切片只允许改名称/产能/效率/状态,禁止删除)。"""
|
|||
|
|
line = _find_line(world, int(payload.get("id") or 0))
|
|||
|
|
out: dict[str, Any] = {"id": line["id"]}
|
|||
|
|
out["name"] = str(payload.get("name") or line["name"]).strip()
|
|||
|
|
if not out["name"]:
|
|||
|
|
raise ValueError("产线名称不能为空")
|
|||
|
|
out["capacityPerDay"] = int(float(payload.get("capacityPerDay") or line["capacityPerDay"]))
|
|||
|
|
if out["capacityPerDay"] <= 0:
|
|||
|
|
raise ValueError("日产能必须为正数")
|
|||
|
|
out["efficiencyFactor"] = round(float(payload.get("efficiencyFactor") or line.get("efficiencyFactor", 1.0)), 3)
|
|||
|
|
if not (0.1 <= out["efficiencyFactor"] <= 2.0):
|
|||
|
|
raise ValueError("效率系数必须在 0.1 ~ 2.0 之间")
|
|||
|
|
status = str(payload.get("status") or line.get("status", "ACTIVE")).upper()
|
|||
|
|
if status not in LINE_STATUSES:
|
|||
|
|
raise ValueError("产线状态必须是 ACTIVE/INACTIVE")
|
|||
|
|
out["status"] = status
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def normalize_material_payload(world: World, payload: dict[str, Any]) -> dict[str, Any]:
|
|||
|
|
"""物料编辑载荷归一化(首切片只允许改库存/在途/安全库存/采购前置期)。"""
|
|||
|
|
m = _find_material(world, int(payload.get("id") or 0))
|
|||
|
|
out: dict[str, Any] = {"id": m["id"]}
|
|||
|
|
for field in ("stock", "inTransit", "safetyStock"):
|
|||
|
|
val = payload.get(field, m.get(field, 0))
|
|||
|
|
out[field] = float(val)
|
|||
|
|
if out[field] < 0:
|
|||
|
|
raise ValueError(f"{field} 不能为负数")
|
|||
|
|
out["procurementLeadTime"] = int(float(payload.get("procurementLeadTime", m.get("procurementLeadTime", 0))))
|
|||
|
|
if out["procurementLeadTime"] < 0:
|
|||
|
|
raise ValueError("采购前置期不能为负数")
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def normalize_maintenance_payload(world: World, payload: dict[str, Any]) -> dict[str, Any]:
|
|||
|
|
"""维保载荷归一化:新增(无 id)或取消(有 id + status=CANCELLED)。"""
|
|||
|
|
out: dict[str, Any] = {}
|
|||
|
|
if payload.get("id"):
|
|||
|
|
mid = int(payload["id"])
|
|||
|
|
mnt = next((m for m in world["maintenance"] if m["id"] == mid), None)
|
|||
|
|
if mnt is None:
|
|||
|
|
raise ValueError(f"维保计划不存在:{mid}")
|
|||
|
|
status = str(payload.get("status") or "CANCELLED").upper()
|
|||
|
|
if status not in MAINTENANCE_STATUSES:
|
|||
|
|
raise ValueError("维保状态必须是 PLANNED/CANCELLED")
|
|||
|
|
out["id"] = mid
|
|||
|
|
out["status"] = status
|
|||
|
|
return out
|
|||
|
|
eq_id = int(payload.get("equipmentId") or 0)
|
|||
|
|
eq = next((e for e in world["equipment"] if e["id"] == eq_id), None)
|
|||
|
|
if eq is None:
|
|||
|
|
raise ValueError(f"设备不存在:{eq_id}")
|
|||
|
|
start = str(payload.get("plannedStart") or "").strip()
|
|||
|
|
end = str(payload.get("plannedEnd") or "").strip()
|
|||
|
|
if not start or not end:
|
|||
|
|
raise ValueError("维保起止时间不能为空")
|
|||
|
|
if end <= start: # 字符串格式统一(YYYY-MM-DD HH:MM),可直接比较
|
|||
|
|
raise ValueError("维保结束时间必须晚于开始时间")
|
|||
|
|
out.update({
|
|||
|
|
"equipmentId": eq_id, "equipmentName": eq["name"],
|
|||
|
|
"plannedStart": start, "plannedEnd": end,
|
|||
|
|
"description": str(payload.get("description") or "").strip() or "计划维保",
|
|||
|
|
})
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def normalize_bom_payload(world: World, payload: dict[str, Any]) -> dict[str, Any]:
|
|||
|
|
"""BOM 明细编辑载荷归一化(首切片:改用量/关键料标记,逐行;不增删行)。"""
|
|||
|
|
item_id = int(payload.get("itemId") or 0)
|
|||
|
|
bi = next((i for i in world["bomItems"] if i["id"] == item_id), None)
|
|||
|
|
if bi is None:
|
|||
|
|
raise ValueError(f"BOM 明细不存在:{item_id}")
|
|||
|
|
out: dict[str, Any] = {"itemId": item_id}
|
|||
|
|
out["quantity"] = float(payload.get("quantity", bi["quantity"]))
|
|||
|
|
if out["quantity"] <= 0:
|
|||
|
|
raise ValueError("单件用量必须为正数")
|
|||
|
|
out["isKeyMaterial"] = bool(payload.get("isKeyMaterial", bi.get("isKeyMaterial", False)))
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def normalize_routing_payload(world: World, payload: dict[str, Any]) -> dict[str, Any]:
|
|||
|
|
"""工艺步骤编辑载荷归一化(首切片:改准备/单件时间与外协标记,逐步骤)。"""
|
|||
|
|
step_id = int(payload.get("stepId") or 0)
|
|||
|
|
step = next((s for s in world["routingSteps"] if s["id"] == step_id), None)
|
|||
|
|
if step is None:
|
|||
|
|
raise ValueError(f"工艺步骤不存在:{step_id}")
|
|||
|
|
out: dict[str, Any] = {"stepId": step_id}
|
|||
|
|
out["setupTime"] = float(payload.get("setupTime", step["setupTime"]))
|
|||
|
|
out["runTimePerUnit"] = float(payload.get("runTimePerUnit", step["runTimePerUnit"]))
|
|||
|
|
if out["setupTime"] < 0 or out["runTimePerUnit"] <= 0:
|
|||
|
|
raise ValueError("准备时间不能为负,单件时间必须为正")
|
|||
|
|
out["isExternal"] = bool(payload.get("isExternal", step.get("isExternal", False)))
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------- 确认卡(影响面摘要) ----------------
|
|||
|
|
|
|||
|
|
def confirmation_for_master_action(world: World, action: str, payload: dict[str, Any]) -> tuple[str, list[str]]:
|
|||
|
|
"""生成主数据 P2 确认卡标题与影响摘要。"""
|
|||
|
|
if action == "master.line.upsert":
|
|||
|
|
p = normalize_line_payload(world, payload)
|
|||
|
|
line = _find_line(world, p["id"])
|
|||
|
|
open_wo = sum(1 for wo in world.get("workOrders", [])
|
|||
|
|
if wo["lineId"] == line["id"] and wo.get("status") not in ("COMPLETED", "CANCELLED"))
|
|||
|
|
lines = [f"产线:{line['name']}({line['code']})→ 名称 {p['name']} · 日产能 {p['capacityPerDay']} · 效率 {p['efficiencyFactor']}"]
|
|||
|
|
if p["status"] != line.get("status", "ACTIVE"):
|
|||
|
|
verb = "停用" if p["status"] == "INACTIVE" else "启用"
|
|||
|
|
lines.append(f"状态变更:{verb}该产线;停用后新排产不再选择它(历史版本不回写)")
|
|||
|
|
if open_wo:
|
|||
|
|
lines.append(f"当前有 {open_wo} 个未完工工单排在该线(仅新版本受影响)")
|
|||
|
|
lines.append("批准后写入主干主数据,执行前自动建档可回滚(P2)")
|
|||
|
|
return f"编辑产线 {line['name']}", lines
|
|||
|
|
if action == "master.material.upsert":
|
|||
|
|
p = normalize_material_payload(world, payload)
|
|||
|
|
m = _find_material(world, p["id"])
|
|||
|
|
return f"编辑物料 {m['name']}", [
|
|||
|
|
f"物料:{m['name']}({m['code']})",
|
|||
|
|
f"库存 {m.get('stock', 0)} → {p['stock']} · 在途 {m.get('inTransit', 0)} → {p['inTransit']}",
|
|||
|
|
f"安全库存 {m.get('safetyStock', 0)} → {p['safetyStock']} · 采购前置期 {m.get('procurementLeadTime', 0)} → {p['procurementLeadTime']} 天",
|
|||
|
|
"库存变化会改变后续新排产的齐套检查结果(P2)",
|
|||
|
|
]
|
|||
|
|
if action == "master.maintenance.upsert":
|
|||
|
|
p = normalize_maintenance_payload(world, payload)
|
|||
|
|
if p.get("id"):
|
|||
|
|
mnt = next(m for m in world["maintenance"] if m["id"] == p["id"])
|
|||
|
|
verb = "取消" if p["status"] == "CANCELLED" else "恢复"
|
|||
|
|
return f"{verb}维保计划 #{p['id']}", [
|
|||
|
|
f"设备:{mnt.get('description', '')}({mnt['plannedStart']} ~ {mnt['plannedEnd']})",
|
|||
|
|
f"{verb}后新排产的设备避让窗口随之变化(P2)",
|
|||
|
|
]
|
|||
|
|
return f"新增维保:{p['equipmentName']}", [
|
|||
|
|
f"窗口:{p['plannedStart']} ~ {p['plannedEnd']}",
|
|||
|
|
"新排产将避开该窗口或产生 EQUIPMENT 冲突提示(P2)",
|
|||
|
|
]
|
|||
|
|
if action == "master.bom.upsert":
|
|||
|
|
p = normalize_bom_payload(world, payload)
|
|||
|
|
bi = next(i for i in world["bomItems"] if i["id"] == p["itemId"])
|
|||
|
|
mat = _find_material(world, bi["materialId"])
|
|||
|
|
bom = next(b for b in world["boms"] if b["id"] == bi["bomId"])
|
|||
|
|
prod = _find_material(world, bom["productId"])
|
|||
|
|
return f"编辑 BOM 明细:{prod['name']} ← {mat['name']}", [
|
|||
|
|
f"BOM {bom['version']}({prod['name']})",
|
|||
|
|
f"单件用量 {bi['quantity']} → {p['quantity']} {mat['unit']} · 关键料 {'是' if p['isKeyMaterial'] else '否'}",
|
|||
|
|
"用量变化直接改变后续新排产的齐套需求与采购建议(P2)",
|
|||
|
|
]
|
|||
|
|
if action == "master.routing.upsert":
|
|||
|
|
p = normalize_routing_payload(world, payload)
|
|||
|
|
step = next(s for s in world["routingSteps"] if s["id"] == p["stepId"])
|
|||
|
|
routing = next(r for r in world["routings"] if r["id"] == step["routingId"])
|
|||
|
|
prod = _find_material(world, routing["productId"])
|
|||
|
|
op = next((o for o in world["operations"] if o["id"] == step["operationId"]), {})
|
|||
|
|
lines = [
|
|||
|
|
f"工艺 {routing['version']}({prod['name']})第 {step['sequenceNo']} 步 · {op.get('name', '')}",
|
|||
|
|
f"准备 {step['setupTime']} → {p['setupTime']} 分 · 单件 {step['runTimePerUnit']} → {p['runTimePerUnit']} 分",
|
|||
|
|
]
|
|||
|
|
if p["isExternal"] != step.get("isExternal", False):
|
|||
|
|
lines.append("外协标记变更:外协工序在订单分解时生成委外订单建议" if p["isExternal"]
|
|||
|
|
else "外协标记取消:该工序回归厂内排产")
|
|||
|
|
lines.append("工时变化直接影响后续新排产的占槽时长(P2)")
|
|||
|
|
return f"编辑工艺步骤:{prod['name']} · {op.get('name', '')}", lines
|
|||
|
|
raise ValueError(f"不支持的主数据动作:{action}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------- P2 写入(调用方负责门禁/快照/审计/落盘) ----------------
|
|||
|
|
|
|||
|
|
def apply_master_action(world: World, next_id, action: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|||
|
|
"""应用主数据写入动作到内存世界(P2 动作的业务侧实现)。"""
|
|||
|
|
if action == "master.line.upsert":
|
|||
|
|
p = normalize_line_payload(world, payload)
|
|||
|
|
line = _find_line(world, p["id"])
|
|||
|
|
before_status = line.get("status", "ACTIVE")
|
|||
|
|
line.update({"name": p["name"], "capacityPerDay": p["capacityPerDay"],
|
|||
|
|
"efficiencyFactor": p["efficiencyFactor"], "status": p["status"]})
|
|||
|
|
return {"kind": "LINE", "id": line["id"], "name": line["name"],
|
|||
|
|
"beforeStatus": before_status, "afterStatus": p["status"]}
|
|||
|
|
if action == "master.material.upsert":
|
|||
|
|
p = normalize_material_payload(world, payload)
|
|||
|
|
m = _find_material(world, p["id"])
|
|||
|
|
before_stock = m.get("stock", 0)
|
|||
|
|
m.update({"stock": p["stock"], "inTransit": p["inTransit"],
|
|||
|
|
"safetyStock": p["safetyStock"], "procurementLeadTime": p["procurementLeadTime"]})
|
|||
|
|
return {"kind": "MATERIAL", "id": m["id"], "name": m["name"],
|
|||
|
|
"beforeStock": before_stock, "afterStock": p["stock"]}
|
|||
|
|
if action == "master.maintenance.upsert":
|
|||
|
|
p = normalize_maintenance_payload(world, payload)
|
|||
|
|
if p.get("id"):
|
|||
|
|
mnt = next(m for m in world["maintenance"] if m["id"] == p["id"])
|
|||
|
|
before_status = mnt.get("status", "PLANNED")
|
|||
|
|
mnt["status"] = p["status"]
|
|||
|
|
return {"kind": "MAINTENANCE", "id": mnt["id"], "name": mnt.get("description", ""),
|
|||
|
|
"beforeStatus": before_status, "afterStatus": p["status"]}
|
|||
|
|
mid = next_id("maintenance")
|
|||
|
|
mnt = {"id": mid, "equipmentId": p["equipmentId"], "type": "MAINTENANCE",
|
|||
|
|
"plannedStart": p["plannedStart"], "plannedEnd": p["plannedEnd"],
|
|||
|
|
"status": "PLANNED", "description": p["description"],
|
|||
|
|
"createdAt": _now()}
|
|||
|
|
world["maintenance"].append(mnt)
|
|||
|
|
return {"kind": "MAINTENANCE", "id": mid, "name": p["description"],
|
|||
|
|
"beforeStatus": None, "afterStatus": "PLANNED"}
|
|||
|
|
if action == "master.bom.upsert":
|
|||
|
|
p = normalize_bom_payload(world, payload)
|
|||
|
|
bi = next(i for i in world["bomItems"] if i["id"] == p["itemId"])
|
|||
|
|
before_qty = bi["quantity"]
|
|||
|
|
bi["quantity"] = p["quantity"]
|
|||
|
|
bi["isKeyMaterial"] = p["isKeyMaterial"]
|
|||
|
|
mat = _find_material(world, bi["materialId"])
|
|||
|
|
return {"kind": "BOM_ITEM", "id": bi["id"], "name": mat["name"],
|
|||
|
|
"beforeQty": before_qty, "afterQty": p["quantity"]}
|
|||
|
|
if action == "master.routing.upsert":
|
|||
|
|
p = normalize_routing_payload(world, payload)
|
|||
|
|
step = next(s for s in world["routingSteps"] if s["id"] == p["stepId"])
|
|||
|
|
before_ext = step.get("isExternal", False)
|
|||
|
|
step["setupTime"] = p["setupTime"]
|
|||
|
|
step["runTimePerUnit"] = p["runTimePerUnit"]
|
|||
|
|
step["isExternal"] = p["isExternal"]
|
|||
|
|
op = next((o for o in world["operations"] if o["id"] == step["operationId"]), {})
|
|||
|
|
return {"kind": "ROUTING_STEP", "id": step["id"], "name": op.get("name", f"步骤{step['sequenceNo']}"),
|
|||
|
|
"beforeExternal": before_ext, "afterExternal": p["isExternal"]}
|
|||
|
|
raise ValueError(f"不支持的主数据动作:{action}")
|