aps-agent/server/aps_domain/readiness.py

306 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================
# 数据齐备度检查器(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}。"""
issues: list[dict[str, str]] = []
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"] == 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": "warn",
"detail": f"BOM 物料 {b.get('materialCode')} 缺主数据"})
continue
need = float(b.get("quantity") or 0) * qty
have = float(mat.get("stock") or 0) + float(mat.get("inTransit") or 0)
if need > have:
issues.append({"type": "MATERIAL_GAP", "severity": "warn",
"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 world.get("flexCalendar"):
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": sum(1 for r in results if not r["ready"]),
"timePending": len(pending),
"timeInferred": len(inferred),
"timeTotal": len(matrix),
},
}
def readiness_text(report: dict[str, Any]) -> str:
"""齐备度报告 → 人话(车间能听懂)。"""
s = report["summary"]
lines = [
f"我帮你看了看:一共 {s['total']} 张要排的单子,"
f"大概 {s['ready']} 张能直接开排,"
f"{s['blocked']} 张还卡着,"
f"{s['withWarnings']} 张能排但有点风险。",
f"工时这边:一共 {s['timeTotal']} 道步骤,"
f"还有 {s['timePending']} 道没填好,"
f"{s['timeInferred']} 道是估出来的(不太准)。",
]
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(
"工时不会填也没关系,你可以跟我说:"
"「把某某工序改成多少分钟」,我按你说的记下来。"
)
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.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"用料清单 **{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("- 这会儿还没有要排的单子。你可以附一张订单表,或者说「新建订单」。")
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("看起来东西比较齐了。你要是愿意,直接跟我说「直接排一版」,我马上给你出一稿。")
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