324 lines
15 KiB
Python
324 lines
15 KiB
Python
# ============================================================
|
||
# 数据齐备度检查器(moduleId: domain-readiness, 可重生 ✅)
|
||
# 排产前置校验:对每个待排订单检查
|
||
# 产品 → 工艺路线 → 每步工时(含来源) → 可用资源 → 班次日历
|
||
# 输出缺失清单 + 严重度;供 /api/readiness、排产前置校验、
|
||
# 引导式排产向导(M-F)与「工时维护情况」口令共用。
|
||
# 严重度:error=会阻塞或严重失真(引擎会默默用 1 分钟兜底);
|
||
# warn=可排但结果可信度低(推断工时/缺料)。
|
||
# ============================================================
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
World = dict[str, Any]
|
||
|
||
# 工时来源常量(与导入器/维护视图对齐)
|
||
SRC_MEASURED = "实测"
|
||
SRC_INFERRED = "推断"
|
||
SRC_TEMPLATE = "模板"
|
||
SRC_EQUIPMENT = "设备默认"
|
||
SRC_PENDING = "待维护"
|
||
|
||
|
||
def resolve_step_time(step: dict, equipment: list[dict]) -> dict[str, Any]:
|
||
"""解析一条柔性路线步骤的单件工时与来源。
|
||
|
||
优先级:路线覆盖值 → 设备 opStdTime → 无(引擎会用 1 分钟兜底=待维护)。
|
||
"""
|
||
op_code = step.get("operationCode")
|
||
std = step.get("stdTimePerUnit")
|
||
if std:
|
||
return {"stdMin": float(std), "source": step.get("stdTimeSource") or SRC_MEASURED}
|
||
eq_times = [
|
||
float(e["opStdTime"][op_code])
|
||
for e in equipment
|
||
if op_code in (e.get("opStdTime") or {}) and e["opStdTime"][op_code]
|
||
]
|
||
if eq_times:
|
||
return {"stdMin": min(eq_times), "source": SRC_EQUIPMENT}
|
||
return {"stdMin": None, "source": SRC_PENDING}
|
||
|
||
|
||
def time_matrix(world: World) -> list[dict[str, Any]]:
|
||
"""产品×工序工时矩阵(工时维护视图数据源)。"""
|
||
equipment = world.get("flexEquipment") or []
|
||
rows: list[dict[str, Any]] = []
|
||
for step in world.get("flexRoutings") or []:
|
||
info = resolve_step_time(step, equipment)
|
||
rows.append({
|
||
"productCode": step.get("productCode"),
|
||
"productName": step.get("productName") or step.get("productCode"),
|
||
"seq": step.get("seq"),
|
||
"operationCode": step.get("operationCode"),
|
||
"stdMin": info["stdMin"],
|
||
"source": info["source"],
|
||
"requireMold": bool(step.get("requireMold")),
|
||
})
|
||
return rows
|
||
|
||
|
||
def check_order(world: World, order: dict) -> dict[str, Any]:
|
||
"""单订单齐备度:返回 {orderNo, productCode, issues[], ready}。"""
|
||
from server.aps_domain.masterdata_consumption import as_datetime, bom_requirement, order_execution_issues, uses_masterdata_constraints
|
||
issues: list[dict[str, str]] = order_execution_issues(world, order)
|
||
material_severity = "error" if uses_masterdata_constraints(world) else "warn"
|
||
pc = order.get("productCode") or ""
|
||
equipment = [e for e in world.get("flexEquipment") or [] if e.get("status") == "RUNNING"]
|
||
materials = {m["code"]: m for m in world.get("flexMaterials") or []}
|
||
|
||
# ① 产品主数据
|
||
if pc not in materials:
|
||
issues.append({"type": "NO_PRODUCT", "severity": "error",
|
||
"detail": f"产品 {pc} 不在物料主数据中"})
|
||
|
||
# ② 工艺路线
|
||
steps = sorted([r for r in world.get("flexRoutings") or [] if r.get("productCode") == pc],
|
||
key=lambda r: r.get("seq") or 0)
|
||
if not steps:
|
||
issues.append({"type": "NO_ROUTING", "severity": "error",
|
||
"detail": f"产品 {pc} 无工艺路线(可用行业模板生成)"})
|
||
|
||
# ③ 每步工时 + 能力设备
|
||
for step in steps:
|
||
op = step.get("operationCode")
|
||
info = resolve_step_time(step, world.get("flexEquipment") or [])
|
||
if info["source"] == SRC_PENDING:
|
||
issues.append({"type": "TIME_UNMAINTAINED", "severity": "error",
|
||
"detail": f"工序 {op} 无工时(引擎将按 1 分钟/件失真兜底)"})
|
||
elif info["source"] in ("demo", "演示", "演示标准工时"):
|
||
issues.append({"type": "TIME_DEMO", "severity": "warn",
|
||
"detail": f"工序 {op} 工时为演示补充 {info['stdMin']} 分钟/件,尚未现场确认,仅用于试排"})
|
||
elif info["source"] == SRC_INFERRED:
|
||
issues.append({"type": "TIME_INFERRED", "severity": "warn",
|
||
"detail": f"工序 {op} 工时为推断值 {info['stdMin']} 分钟/件,建议实测后维护"})
|
||
if not any(op in (e.get("capabilities") or []) for e in equipment):
|
||
issues.append({"type": "NO_CAPABLE_EQUIPMENT", "severity": "error",
|
||
"detail": f"工序 {op} 无运行中的能力设备"})
|
||
|
||
# ④ 物料齐套(软性提示)
|
||
qty = float(order.get("quantity") or 0)
|
||
for b in world.get("flexBom") or []:
|
||
if b.get("productCode") != pc:
|
||
continue
|
||
mat = materials.get(b.get("materialCode"))
|
||
if not mat:
|
||
issues.append({"type": "MATERIAL_GAP", "severity": material_severity,
|
||
"detail": f"BOM 物料 {b.get('materialCode')} 缺主数据"})
|
||
continue
|
||
need = bom_requirement(b, qty)
|
||
have = float(mat.get("stock") or 0) + float(mat.get("inTransit") or 0)
|
||
if (uses_masterdata_constraints(world) and need > float(mat.get("stock") or 0)
|
||
and float(mat.get("inTransit") or 0) > 0 and as_datetime(mat.get("expectedArrivalDate")) is None):
|
||
issues.append({"type": "MATERIAL_ETA_UNKNOWN", "severity": "error",
|
||
"detail": f"物料 {mat.get('name')} 需要在途补料,但未确认到货日期"})
|
||
if need > have:
|
||
issues.append({"type": "MATERIAL_GAP", "severity": material_severity,
|
||
"detail": f"物料 {mat.get('name')} 缺口 {need - have:.0f} {mat.get('unit') or ''}"})
|
||
|
||
errors = [i for i in issues if i["severity"] == "error"]
|
||
return {
|
||
"orderNo": order.get("orderNo"),
|
||
"productCode": pc,
|
||
"quantity": order.get("quantity"),
|
||
"dueDate": order.get("dueDate"),
|
||
"issues": issues,
|
||
"ready": not errors,
|
||
"hasWarnings": any(i["severity"] == "warn" for i in issues),
|
||
}
|
||
|
||
|
||
def check_readiness(world: World, order_ids: list[int] | None = None) -> dict[str, Any]:
|
||
"""全量/指定订单齐备度检查(P0 只读)。"""
|
||
orders = [o for o in world.get("flexOrders") or []
|
||
if (o.get("status") or "") not in ("DONE", "CANCELLED")]
|
||
if order_ids:
|
||
orders = [o for o in orders if o.get("id") in set(order_ids)]
|
||
|
||
results = [check_order(world, o) for o in orders]
|
||
|
||
# 全局项:班次日历
|
||
global_issues: list[dict[str, str]] = []
|
||
if not any(s.get("enabled", True) for s in world.get("flexCalendar") or []) and not world.get("flexCalendarOverrides"):
|
||
global_issues.append({"type": "NO_CALENDAR", "severity": "error",
|
||
"detail": "无班次日历(不知道每天几班、几点开工)"})
|
||
|
||
# 工时维护总览
|
||
matrix = time_matrix(world)
|
||
pending = [r for r in matrix if r["source"] == SRC_PENDING]
|
||
inferred = [r for r in matrix if r["source"] == SRC_INFERRED]
|
||
|
||
return {
|
||
"orders": results,
|
||
"globalIssues": global_issues,
|
||
"summary": {
|
||
"total": len(results),
|
||
"ready": sum(1 for r in results if r["ready"]) if not global_issues else 0,
|
||
"withWarnings": sum(1 for r in results if r["hasWarnings"]),
|
||
"blocked": len(results) if global_issues else sum(1 for r in results if not r["ready"]),
|
||
"timePending": len(pending),
|
||
"timeInferred": len(inferred),
|
||
"timeDemo": sum(r["source"] in ("demo", "演示", "演示标准工时") for r in matrix),
|
||
"timeTotal": len(matrix),
|
||
},
|
||
}
|
||
|
||
|
||
def readiness_text(report: dict[str, Any]) -> str:
|
||
"""齐备度报告:结论、关键指标、阻断项与下一步。"""
|
||
s = report["summary"]
|
||
if not s["total"]:
|
||
conclusion = "当前没有待排订单,暂时无法生成试排方案。"
|
||
elif s["blocked"]:
|
||
conclusion = f"当前有 {s['blocked']} 张订单存在阻断项,暂不建议直接排产。"
|
||
else:
|
||
conclusion = f"当前 {s['total']} 张订单已通过基础齐备度检查,可以生成试排方案。"
|
||
lines = [
|
||
"## 数据齐备度",
|
||
f"**结论:{conclusion}**",
|
||
"",
|
||
f"- 待排订单:{s['total']} 张;可直接排产:{s['ready']} 张;存在阻断:{s['blocked']} 张。",
|
||
f"- 标准工时:共 {s['timeTotal']} 道工序;待维护 {s['timePending']} 道;推断值 {s['timeInferred']} 道。",
|
||
]
|
||
if s["withWarnings"]:
|
||
lines.append(f"- 风险提示:{s['withWarnings']} 张订单包含推断工时或缺料告警。")
|
||
for gi in report["globalIssues"]:
|
||
lines.append(f"- **阻断项**:{gi['detail']}")
|
||
shown = 0
|
||
for r in report["orders"]:
|
||
if not r["issues"] or shown >= 6:
|
||
continue
|
||
mark = "阻断" if not r["ready"] else "风险"
|
||
heads = ";".join(i["detail"] for i in r["issues"][:3])
|
||
more = f"(另外还有 {len(r['issues']) - 3} 处)" if len(r["issues"]) > 3 else ""
|
||
lines.append(f"- **{mark}** · 订单 {r['orderNo']}:{heads}{more}")
|
||
shown += 1
|
||
rest = sum(1 for r in report["orders"] if r["issues"]) - shown
|
||
if rest > 0:
|
||
lines.append(f"- 另有 {rest} 张订单存在类似问题,详情可在订单齐备度明细中查看。")
|
||
if s["timePending"] or s["timeInferred"]:
|
||
lines.append("")
|
||
lines.append(
|
||
"**建议操作**:补充待维护工时,或提供实测工时(例如“将工序 OP-10 设置为 45 分钟/件”)。"
|
||
)
|
||
return "\n".join(lines)
|
||
|
||
|
||
def analyze_project_data(world: World, query: str = "") -> str:
|
||
"""分析当前项目/附件:结论 → 数据概览 → 阻断项 → 建议操作。"""
|
||
lines: list[str] = []
|
||
q = query or ""
|
||
if "【文件解析】" in q:
|
||
chunk = q.split("【文件解析】", 1)[1]
|
||
chunk = chunk.strip()
|
||
if "\n\n请" in chunk:
|
||
chunk = chunk.split("\n\n请", 1)[0].strip()
|
||
elif "\n\n给我" in chunk:
|
||
chunk = chunk.split("\n\n给我", 1)[0].strip()
|
||
lines.append("## 附件解析结果")
|
||
lines.append("已完成附件的初步解析:")
|
||
lines.extend(chunk.splitlines()[:40])
|
||
lines.append("")
|
||
if "可入库" in chunk:
|
||
lines.append("部分记录已通过校验,可以提交入库;错误记录需要修正后重新上传。")
|
||
lines.append("")
|
||
|
||
active = [o for o in (world.get("flexOrders") or [])
|
||
if (o.get("status") or "") not in ("DONE", "CANCELLED")]
|
||
mats = world.get("flexMaterials") or []
|
||
routes = world.get("flexRoutings") or []
|
||
equip = [e for e in (world.get("flexEquipment") or []) if e.get("status") == "RUNNING"]
|
||
cal = world.get("flexCalendar") or []
|
||
bom = world.get("flexBom") or []
|
||
|
||
lines.append("## 项目数据概览")
|
||
lines.append(
|
||
f"- 待排订单:**{len(active)}** 张;产品/物料:**{len(mats)}** 条;"
|
||
f"BOM 明细:**{len(bom)}** 条;工艺步骤:**{len(routes)}** 条;"
|
||
f"可用设备:**{len(equip)}** 台;班次日历:**{len(cal)}** 条。"
|
||
)
|
||
if active:
|
||
preview = "、".join(
|
||
f"{o.get('orderNo')}({o.get('productCode')} × {o.get('quantity')})"
|
||
for o in active[:5]
|
||
)
|
||
more = f"……一共 {len(active)} 张" if len(active) > 5 else ""
|
||
lines.append(f"- 订单示例:{preview}{more}")
|
||
else:
|
||
lines.append("- 待排订单为 0 张。请先导入订单表或新建订单。")
|
||
|
||
report = check_readiness(world)
|
||
lines.append("")
|
||
lines.append(readiness_text(report))
|
||
|
||
asks: list[str] = []
|
||
if not active:
|
||
asks.append("导入订单表(至少包含单号、产品、数量和交期)")
|
||
if not mats:
|
||
asks.append("补充产品/物料主数据(至少包含待排产品)")
|
||
if not routes:
|
||
asks.append("补充工艺路线和工序顺序,或请求系统推荐行业模板")
|
||
if not equip:
|
||
asks.append("补充可用设备及其工序能力")
|
||
if not cal:
|
||
asks.append("补充班次日历(工作日、班次和起止时间)")
|
||
s = report["summary"]
|
||
if s.get("timePending"):
|
||
asks.append(f"补充 {s['timePending']} 道工序的标准工时")
|
||
if s.get("blocked"):
|
||
asks.append(f"处理 {s['blocked']} 张订单的阻断项(产品、工艺或设备能力缺失)")
|
||
|
||
seen: set[str] = set()
|
||
uniq = []
|
||
for a in asks:
|
||
if a not in seen:
|
||
seen.add(a)
|
||
uniq.append(a)
|
||
|
||
lines.append("")
|
||
if uniq:
|
||
lines.append("## 阻断项与建议操作")
|
||
for i, a in enumerate(uniq, 1):
|
||
lines.append(f"{i}. {a}")
|
||
lines.append("完成上述补充后,请再次执行“检查数据齐备度”;通过后即可生成试排方案。")
|
||
else:
|
||
lines.append("## 建议操作")
|
||
lines.append("数据已满足基础排产条件。可以选择排产策略并生成试排方案。")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def annotate_time_conflicts(world: World, version_id: int, next_id, order_nos: list[str] | None = None) -> int:
|
||
"""排产后置标注:把推断/待维护工时写成冲突(TIME_INFERRED/TIME_UNMAINTAINED),不再默默兜底。"""
|
||
equipment = world.get("flexEquipment") or []
|
||
seen: set[tuple[str, str]] = set()
|
||
count = 0
|
||
order_products = {o.get("productCode") for o in world.get("flexOrders") or []
|
||
if not order_nos or o.get("orderNo") in set(order_nos)}
|
||
for step in world.get("flexRoutings") or []:
|
||
pc = step.get("productCode")
|
||
if pc not in order_products:
|
||
continue
|
||
key = (pc, step.get("operationCode") or "")
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
info = resolve_step_time(step, equipment)
|
||
if info["source"] == SRC_PENDING:
|
||
ctype, sev = "TIME_UNMAINTAINED", "MAJOR"
|
||
desc = f"产品 {pc} 工序 {step.get('operationCode')} 工时未维护,排产按 1 分钟/件兜底(严重失真)"
|
||
fix = "在工时维护视图补录实测工时"
|
||
elif info["source"] == SRC_INFERRED:
|
||
ctype, sev = "TIME_INFERRED", "MINOR"
|
||
desc = f"产品 {pc} 工序 {step.get('operationCode')} 采用推断工时 {info['stdMin']} 分钟/件"
|
||
fix = "实测后更新工时(来源改为实测)"
|
||
else:
|
||
continue
|
||
world.setdefault("flexConflicts", []).append({
|
||
"id": next_id("flexConflict"), "versionId": version_id,
|
||
"conflictType": ctype, "severity": sev,
|
||
"resourceType": "TIME", "orderNo": "",
|
||
"description": desc, "suggestedSolution": fix, "isResolved": False,
|
||
})
|
||
count += 1
|
||
return count
|