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