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