aps-agent/server/aps_domain/readiness.py

306 lines
13 KiB
Python
Raw Normal View History

# ============================================================
# 数据齐备度检查器(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