501 lines
22 KiB
Python
501 lines
22 KiB
Python
# ============================================================
|
||
# 主数据/订单自然语言只读查询(moduleId: domain-master-query, 可重生 ✅)
|
||
# 让对话能检索 world 中的销售订单、物料、工序、BOM、工艺路线、柔性资源。
|
||
# ============================================================
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import Any
|
||
|
||
from server.engines.queries import find_bom_items, find_routing_steps
|
||
|
||
World = dict[str, Any]
|
||
|
||
ENTITIES = ("order", "material", "operation", "bom", "routing", "overview", "flex", "mrp")
|
||
|
||
|
||
def detect_entity(text: str) -> str:
|
||
t = text or ""
|
||
if re.search(r"概览|主数据总览|资源树|工厂有什么", t):
|
||
return "overview"
|
||
if re.search(r"委外|采购建议|分解建议|外协|MRP建议", t, re.I):
|
||
return "mrp"
|
||
if re.search(r"柔性|能力池|虚拟产线|flex", t, re.I):
|
||
return "flex"
|
||
if re.search(r"BOM|物料清单|用料|子件", t, re.I):
|
||
return "bom"
|
||
if re.search(r"工艺路线|路线步骤|工序流|怎么排工序", t):
|
||
return "routing"
|
||
if re.search(r"工序库|有哪些工序|查工序|工序编码", t):
|
||
return "operation"
|
||
if re.search(r"物料|成品|原料|库存|料号", t):
|
||
return "material"
|
||
if re.search(r"订单|交期|客户|WBS", t):
|
||
return "order"
|
||
return "overview"
|
||
|
||
|
||
def extract_code(text: str) -> str | None:
|
||
"""抽订单号 / 物料码 / 工序码。"""
|
||
t = text or ""
|
||
m = re.search(r"(SO\d{6,}|\d{6,}|FO-?\d{3,}|(?:CTRL|PCB|HV|PDU)-[A-Z0-9\-]+|"
|
||
r"[0-9A-Za-z]{6,}Z1M|[LM]\d{6,}|\d{10,})", t, re.I)
|
||
if m:
|
||
return m.group(1)
|
||
m2 = re.search(r"(?:订单|物料|料号|工序|产品)\s*[::]?\s*([0-9A-Za-z\-]{4,})", t)
|
||
return m2.group(1) if m2 else None
|
||
|
||
|
||
def _find_product(world: World, code_or_name: str | None) -> dict | None:
|
||
mats = world.get("materials") or []
|
||
if not code_or_name:
|
||
return next((m for m in mats if m.get("type") == "FINISHED_PRODUCT"), None)
|
||
key = code_or_name.strip().lower()
|
||
for m in mats:
|
||
if str(m.get("code", "")).lower() == key:
|
||
return m
|
||
if key in str(m.get("name", "")).lower():
|
||
return m
|
||
# flex 成品
|
||
for m in world.get("flexMaterials") or []:
|
||
if m.get("type") == "FINISHED_PRODUCT" and (
|
||
str(m.get("code", "")).lower() == key or key in str(m.get("name", "")).lower()
|
||
):
|
||
return {"id": None, "code": m["code"], "name": m["name"], "type": "FINISHED_PRODUCT",
|
||
"_flex": True}
|
||
return None
|
||
|
||
|
||
def query_orders(world: World, code: str | None = None, limit: int = 20) -> dict[str, Any]:
|
||
rows = []
|
||
for so in world.get("salesOrders") or []:
|
||
if code and code not in str(so.get("orderNo", "")) and code not in str(so.get("customerName", "")):
|
||
continue
|
||
item = (so.get("items") or [{}])[0]
|
||
rows.append({
|
||
"orderNo": so.get("orderNo"),
|
||
"status": so.get("status"),
|
||
"customer": so.get("customerName"),
|
||
"level": so.get("customerLevel"),
|
||
"productCode": item.get("productCode"),
|
||
"productName": item.get("productName"),
|
||
"quantity": item.get("quantity"),
|
||
"deliveryDate": so.get("deliveryDate") or item.get("deliveryDate"),
|
||
"wbs": so.get("wbs") or "",
|
||
"special": so.get("specialRequirements") or "",
|
||
})
|
||
# 柔性订单补充(若固定轨无命中)
|
||
if not rows or (code and not any(r["orderNo"] == code for r in rows)):
|
||
for fo in world.get("flexOrders") or []:
|
||
if code and code not in str(fo.get("orderNo", "")):
|
||
continue
|
||
rows.append({
|
||
"orderNo": fo.get("orderNo"),
|
||
"status": fo.get("status"),
|
||
"customer": fo.get("project") or "柔性订单",
|
||
"level": "",
|
||
"productCode": fo.get("productCode"),
|
||
"productName": fo.get("productCode"),
|
||
"quantity": fo.get("quantity"),
|
||
"deliveryDate": fo.get("dueDate"),
|
||
"wbs": fo.get("wbs") or "",
|
||
"special": "flex",
|
||
})
|
||
return {"entity": "order", "count": len(rows), "rows": rows[:limit]}
|
||
|
||
|
||
def query_materials(world: World, code: str | None = None, limit: int = 40) -> dict[str, Any]:
|
||
rows = []
|
||
for m in world.get("materials") or []:
|
||
if code:
|
||
c = code.lower()
|
||
if c not in str(m.get("code", "")).lower() and c not in str(m.get("name", "")).lower():
|
||
continue
|
||
rows.append({
|
||
"code": m.get("code"), "name": m.get("name"), "type": m.get("type"),
|
||
"unit": m.get("unit"), "stock": m.get("stock"), "inTransit": m.get("inTransit"),
|
||
"spec": m.get("spec") or "",
|
||
})
|
||
if not rows:
|
||
for m in world.get("flexMaterials") or []:
|
||
if code:
|
||
c = code.lower()
|
||
if c not in str(m.get("code", "")).lower() and c not in str(m.get("name", "")).lower():
|
||
continue
|
||
rows.append({
|
||
"code": m.get("code"), "name": m.get("name"), "type": m.get("type"),
|
||
"unit": m.get("unit"), "stock": m.get("stock"), "inTransit": m.get("inTransit"),
|
||
"spec": "",
|
||
})
|
||
return {"entity": "material", "count": len(rows), "rows": rows[:limit]}
|
||
|
||
|
||
def query_operations(world: World, code: str | None = None, limit: int = 40) -> dict[str, Any]:
|
||
rows = []
|
||
ops = world.get("operations") or []
|
||
if not ops:
|
||
ops = [
|
||
{"code": o.get("code"), "name": o.get("name"),
|
||
"type": "INTERNAL", "standardTime": o.get("changeoverMin")}
|
||
for o in (world.get("flexOperations") or [])
|
||
]
|
||
for o in ops:
|
||
if code:
|
||
c = code.lower()
|
||
if c not in str(o.get("code", "")).lower() and c not in str(o.get("name", "")).lower():
|
||
continue
|
||
rows.append({
|
||
"code": o.get("code"), "name": o.get("name"),
|
||
"type": o.get("type"), "standardTime": o.get("standardTime"),
|
||
})
|
||
return {"entity": "operation", "count": len(rows), "rows": rows[:limit]}
|
||
|
||
|
||
def query_bom(world: World, code: str | None = None, limit: int = 50) -> dict[str, Any]:
|
||
product = _find_product(world, code)
|
||
if not product:
|
||
return {"entity": "bom", "count": 0, "rows": [], "hint": "未找到成品,请带料号如 28200003654300"}
|
||
if product.get("_flex") or product.get("id") is None:
|
||
pc = product["code"]
|
||
rows = []
|
||
mats = {m["code"]: m for m in (world.get("flexMaterials") or [])}
|
||
for b in world.get("flexBom") or []:
|
||
if b.get("productCode") != pc:
|
||
continue
|
||
mat = mats.get(b.get("materialCode") or "", {})
|
||
rows.append({
|
||
"materialCode": b.get("materialCode"),
|
||
"materialName": mat.get("name") or b.get("materialCode"),
|
||
"quantity": b.get("quantity"),
|
||
"consumeOp": b.get("consumeOp"),
|
||
"isKey": b.get("isKey"),
|
||
})
|
||
return {"entity": "bom", "productCode": pc, "productName": product.get("name"),
|
||
"count": len(rows), "rows": rows[:limit]}
|
||
pid = product["id"]
|
||
mats = {m["id"]: m for m in world.get("materials") or []}
|
||
ops = {o["id"]: o for o in world.get("operations") or []}
|
||
rows = []
|
||
for it in find_bom_items(world, pid):
|
||
mat = mats.get(it["materialId"], {})
|
||
op = ops.get(it.get("operationId"), {})
|
||
rows.append({
|
||
"materialCode": mat.get("code"),
|
||
"materialName": mat.get("name"),
|
||
"quantity": it.get("quantity"),
|
||
"consumeOp": op.get("code") or op.get("name"),
|
||
"isKey": it.get("isKeyMaterial"),
|
||
})
|
||
return {"entity": "bom", "productCode": product.get("code"), "productName": product.get("name"),
|
||
"count": len(rows), "rows": rows[:limit]}
|
||
|
||
|
||
def query_routing(world: World, code: str | None = None, limit: int = 40) -> dict[str, Any]:
|
||
product = _find_product(world, code)
|
||
if not product:
|
||
return {"entity": "routing", "count": 0, "rows": [], "hint": "未找到成品"}
|
||
if product.get("_flex") or product.get("id") is None:
|
||
pc = product["code"]
|
||
ops = {o["code"]: o for o in (world.get("flexOperations") or [])}
|
||
rows = []
|
||
for r in sorted((world.get("flexRoutings") or []), key=lambda x: x.get("seq", 0)):
|
||
if r.get("productCode") != pc:
|
||
continue
|
||
op = ops.get(r.get("operationCode") or "", {})
|
||
rows.append({
|
||
"seq": r.get("seq"),
|
||
"operationCode": r.get("operationCode"),
|
||
"operationName": op.get("name") or r.get("operationCode"),
|
||
"stdTimePerUnit": r.get("stdTimePerUnit"),
|
||
"requireMold": r.get("requireMold"),
|
||
})
|
||
return {"entity": "routing", "productCode": pc, "productName": product.get("name"),
|
||
"count": len(rows), "rows": rows[:limit]}
|
||
pid = product["id"]
|
||
ops = {o["id"]: o for o in world.get("operations") or []}
|
||
rows = []
|
||
for s in find_routing_steps(world, pid):
|
||
op = ops.get(s["operationId"], {})
|
||
rows.append({
|
||
"seq": s.get("sequenceNo"),
|
||
"operationCode": op.get("code"),
|
||
"operationName": op.get("name"),
|
||
"setupTime": s.get("setupTime"),
|
||
"runTimePerUnit": s.get("runTimePerUnit"),
|
||
"isExternal": s.get("isExternal"),
|
||
})
|
||
return {"entity": "routing", "productCode": product.get("code"), "productName": product.get("name"),
|
||
"count": len(rows), "rows": rows[:limit]}
|
||
|
||
|
||
def query_overview(world: World) -> dict[str, Any]:
|
||
factories = [f.get("name") for f in (world.get("factories") or [])]
|
||
return {
|
||
"entity": "overview",
|
||
"factory": factories[0] if factories else "(无)",
|
||
"counts": {
|
||
"factories": len(world.get("factories") or []),
|
||
"lines": len(world.get("lines") or []),
|
||
"workstations": len(world.get("workstations") or []),
|
||
"materials": len(world.get("materials") or []),
|
||
"operations": len(world.get("operations") or []),
|
||
"boms": len(world.get("boms") or []),
|
||
"routings": len(world.get("routings") or []),
|
||
"salesOrders": len(world.get("salesOrders") or []),
|
||
"flexOrders": len(world.get("flexOrders") or []),
|
||
"flexEquipment": len(world.get("flexEquipment") or []),
|
||
},
|
||
}
|
||
|
||
|
||
def query_flex(world: World, limit: int = 20) -> dict[str, Any]:
|
||
return {
|
||
"entity": "flex",
|
||
"orders": [
|
||
{"orderNo": o.get("orderNo"), "productCode": o.get("productCode"),
|
||
"qty": o.get("quantity"), "due": o.get("dueDate")}
|
||
for o in (world.get("flexOrders") or [])[:limit]
|
||
],
|
||
"equipment": [
|
||
{"code": e.get("code"), "name": e.get("name"), "zone": e.get("zone"),
|
||
"caps": len(e.get("capabilities") or [])}
|
||
for e in (world.get("flexEquipment") or [])[:limit]
|
||
],
|
||
"operations": [
|
||
{"code": o.get("code"), "name": o.get("name"), "bottleneck": o.get("isBottleneck")}
|
||
for o in (world.get("flexOperations") or [])[:limit]
|
||
],
|
||
}
|
||
|
||
|
||
def _mrp_aspect(text: str) -> str:
|
||
t = text or ""
|
||
if re.search(r"委外|外协", t) and not re.search(r"采购", t):
|
||
return "outsource"
|
||
if re.search(r"采购", t) and not re.search(r"委外|外协", t):
|
||
return "purchase"
|
||
return "all"
|
||
|
||
|
||
def query_mrp(world: World, order_no: str | None = None, *, aspect: str = "all",
|
||
text: str = "") -> dict[str, Any]:
|
||
"""查某单(或全部)的采购/委外建议,并核对工艺是否有外协步骤。"""
|
||
aspect = aspect if aspect in ("outsource", "purchase", "all") else _mrp_aspect(text)
|
||
order_no = (order_no or "").strip() or None
|
||
|
||
def _match_so(row: dict) -> bool:
|
||
if not order_no:
|
||
return True
|
||
return str(row.get("salesOrderNo") or "") == order_no
|
||
|
||
purchase = [r for r in (world.get("purchaseOrders") or []) if _match_so(r)]
|
||
outsource = [r for r in (world.get("outsourceOrders") or []) if _match_so(r)]
|
||
|
||
# 工艺外协核对(即使尚未分解也能答「有没有委外」)
|
||
external_steps: list[dict] = []
|
||
product_code = None
|
||
if order_no:
|
||
so = next((s for s in (world.get("salesOrders") or [])
|
||
if str(s.get("orderNo")) == order_no), None)
|
||
fo = next((o for o in (world.get("flexOrders") or [])
|
||
if str(o.get("orderNo")) == order_no), None)
|
||
product_id = None
|
||
if so:
|
||
item = (so.get("items") or [{}])[0]
|
||
product_code = item.get("productCode")
|
||
product_id = item.get("productId")
|
||
if product_id is None and product_code:
|
||
mat = next((m for m in (world.get("materials") or [])
|
||
if m.get("code") == product_code), None)
|
||
product_id = mat["id"] if mat else None
|
||
elif fo:
|
||
product_code = fo.get("productCode")
|
||
mat = next((m for m in (world.get("materials") or [])
|
||
if m.get("code") == product_code), None)
|
||
product_id = mat["id"] if mat else None
|
||
for s in (world.get("flexRoutings") or []):
|
||
if s.get("productCode") == product_code and s.get("isExternal"):
|
||
external_steps.append({
|
||
"seq": s.get("seq"), "operationCode": s.get("operationCode"),
|
||
"operationName": s.get("operationName") or s.get("operationCode"),
|
||
})
|
||
if product_id is not None and not external_steps:
|
||
for step in find_routing_steps(world, product_id):
|
||
if step.get("isExternal"):
|
||
op = next((o for o in (world.get("operations") or [])
|
||
if o.get("id") == step.get("operationId")), {})
|
||
external_steps.append({
|
||
"seq": step.get("sequenceNo"),
|
||
"operationCode": op.get("code"),
|
||
"operationName": op.get("name") or op.get("code"),
|
||
})
|
||
|
||
return {
|
||
"entity": "mrp",
|
||
"aspect": aspect,
|
||
"orderNo": order_no,
|
||
"productCode": product_code,
|
||
"purchase": [
|
||
{"orderNo": r.get("orderNo"), "materialCode": r.get("materialCode"),
|
||
"materialName": r.get("materialName"), "quantity": r.get("quantity"),
|
||
"status": r.get("status"), "suggestedOrderDate": r.get("suggestedOrderDate")}
|
||
for r in purchase
|
||
],
|
||
"outsource": [
|
||
{"orderNo": r.get("orderNo"), "operationName": r.get("operationName"),
|
||
"quantity": r.get("quantity"), "status": r.get("status"),
|
||
"requiredDate": r.get("requiredDate")}
|
||
for r in outsource
|
||
],
|
||
"externalSteps": external_steps,
|
||
"decomposed": bool(purchase or outsource),
|
||
}
|
||
|
||
|
||
def run_master_query(world: World, *, entity: str | None = None, code: str | None = None,
|
||
text: str = "", limit: int = 40, aspect: str | None = None) -> dict[str, Any]:
|
||
ent = entity or detect_entity(text)
|
||
if ent not in ENTITIES:
|
||
ent = "overview"
|
||
code = code or extract_code(text)
|
||
if ent == "order":
|
||
return query_orders(world, code, limit=limit)
|
||
if ent == "material":
|
||
return query_materials(world, code, limit=limit)
|
||
if ent == "operation":
|
||
return query_operations(world, code, limit=limit)
|
||
if ent == "bom":
|
||
return query_bom(world, code, limit=limit)
|
||
if ent == "routing":
|
||
return query_routing(world, code, limit=limit)
|
||
if ent == "flex":
|
||
return query_flex(world, limit=limit)
|
||
if ent == "mrp":
|
||
return query_mrp(world, code, aspect=aspect or _mrp_aspect(text), text=text)
|
||
return query_overview(world)
|
||
|
||
|
||
def format_master_query(result: dict[str, Any]) -> str:
|
||
"""把查询结果格式化为对话可读文本。"""
|
||
ent = result.get("entity")
|
||
if ent == "overview":
|
||
c = result.get("counts") or {}
|
||
return (
|
||
f"主数据概览 · 工厂【{result.get('factory')}】\n"
|
||
f"· 产线 {c.get('lines', 0)} / 工位 {c.get('workstations', 0)}\n"
|
||
f"· 物料 {c.get('materials', 0)} / 工序 {c.get('operations', 0)} / "
|
||
f"BOM {c.get('boms', 0)} / 路线 {c.get('routings', 0)}\n"
|
||
f"· 销售订单 {c.get('salesOrders', 0)} / 柔性订单 {c.get('flexOrders', 0)} / "
|
||
f"柔性设备 {c.get('flexEquipment', 0)}\n"
|
||
"可继续说:「查订单」「查物料」「查 BOM 28200003654300」「查工艺路线」。"
|
||
)
|
||
if ent == "order":
|
||
rows = result.get("rows") or []
|
||
if not rows:
|
||
return "未找到匹配订单。可说「有哪些订单」或带订单号如「查订单 102285668」。"
|
||
lines = [f"订单共 {result.get('count')} 条:"]
|
||
for r in rows:
|
||
lines.append(
|
||
f"· {r['orderNo']} [{r.get('status')}] {r.get('productName') or r.get('productCode')}"
|
||
f" ×{r.get('quantity')} 交期 {r.get('deliveryDate')} "
|
||
f"客户 {r.get('customer')}"
|
||
+ (f" WBS={r['wbs']}" if r.get("wbs") else "")
|
||
)
|
||
return "\n".join(lines)
|
||
if ent == "material":
|
||
rows = result.get("rows") or []
|
||
if not rows:
|
||
return "未找到匹配物料。"
|
||
lines = [f"物料共 {result.get('count')} 条(展示前 {len(rows)}):"]
|
||
for r in rows:
|
||
lines.append(
|
||
f"· {r['code']} {r.get('name')} [{r.get('type')}] "
|
||
f"库存 {r.get('stock')}{r.get('unit') or ''}"
|
||
)
|
||
return "\n".join(lines)
|
||
if ent == "operation":
|
||
rows = result.get("rows") or []
|
||
if not rows:
|
||
return "工序库为空。"
|
||
lines = [f"工序共 {result.get('count')} 条:"]
|
||
for r in rows:
|
||
st = r.get("standardTime")
|
||
lines.append(f"· {r['code']} {r.get('name')} 标准工时={st}")
|
||
return "\n".join(lines)
|
||
if ent == "bom":
|
||
if result.get("hint") and not result.get("rows"):
|
||
return result["hint"]
|
||
rows = result.get("rows") or []
|
||
head = f"产品 {result.get('productCode')} {result.get('productName') or ''} BOM · {result.get('count')} 项:"
|
||
lines = [head]
|
||
for r in rows:
|
||
key = "★" if r.get("isKey") else " "
|
||
lines.append(
|
||
f"·{key} {r.get('materialCode')} {r.get('materialName')} ×{r.get('quantity')}"
|
||
+ (f" @{r.get('consumeOp')}" if r.get("consumeOp") else "")
|
||
)
|
||
return "\n".join(lines) if rows else head + "(空)"
|
||
if ent == "routing":
|
||
if result.get("hint") and not result.get("rows"):
|
||
return result["hint"]
|
||
rows = result.get("rows") or []
|
||
head = f"产品 {result.get('productCode')} 工艺路线 · {result.get('count')} 步:"
|
||
lines = [head]
|
||
for r in rows:
|
||
t = r.get("runTimePerUnit") or r.get("stdTimePerUnit")
|
||
lines.append(
|
||
f"· seq={r.get('seq')} {r.get('operationCode')} {r.get('operationName')} "
|
||
f"单件 {t} 分"
|
||
)
|
||
return "\n".join(lines) if rows else head + "(空)"
|
||
if ent == "flex":
|
||
lines = ["柔性资源:"]
|
||
for o in result.get("orders") or []:
|
||
lines.append(f"· 订单 {o['orderNo']} {o['productCode']} ×{o['qty']} 交期 {o['due']}")
|
||
for e in result.get("equipment") or []:
|
||
lines.append(f"· 设备 {e['code']} {e['name']} 区={e.get('zone')} 能力数={e.get('caps')}")
|
||
for o in result.get("operations") or []:
|
||
flag = "★瓶颈" if o.get("bottleneck") else ""
|
||
lines.append(f"· 工序 {o['code']} {o['name']} {flag}")
|
||
return "\n".join(lines) if len(lines) > 1 else "柔性资源为空。"
|
||
if ent == "mrp":
|
||
order_no = result.get("orderNo") or "全部订单"
|
||
aspect = result.get("aspect") or "all"
|
||
purchase = result.get("purchase") or []
|
||
outsource = result.get("outsource") or []
|
||
external = result.get("externalSteps") or []
|
||
lines = [f"订单 {order_no} 的采购/委外情况:"]
|
||
if aspect in ("outsource", "all"):
|
||
if outsource:
|
||
lines.append(f"· 委外建议:{len(outsource)} 条")
|
||
for r in outsource[:8]:
|
||
lines.append(
|
||
f" - {r.get('orderNo')} {r.get('operationName')} ×{r.get('quantity')} "
|
||
f"[{r.get('status')}] 需求日 {r.get('requiredDate')}"
|
||
)
|
||
elif external:
|
||
lines.append(
|
||
f"· 委外建议:尚未分解,但工艺有 {len(external)} 道外协步骤"
|
||
f"({ '、'.join(str(s.get('operationName') or s.get('operationCode')) for s in external[:5]) })。"
|
||
"可说「分解订单」生成委外建议单。"
|
||
)
|
||
else:
|
||
lines.append("· 委外建议:无。工艺路线未标记外协步骤,当前没有委外订单。")
|
||
if aspect in ("purchase", "all"):
|
||
if purchase:
|
||
lines.append(f"· 采购建议:{len(purchase)} 条")
|
||
for r in purchase[:8]:
|
||
lines.append(
|
||
f" - {r.get('orderNo')} {r.get('materialName') or r.get('materialCode')} "
|
||
f"×{r.get('quantity')} [{r.get('status')}] 建议下单 {r.get('suggestedOrderDate')}"
|
||
)
|
||
else:
|
||
tip = "(可先说「分解订单」生成建议)" if not result.get("decomposed") else ""
|
||
lines.append(f"· 采购建议:无{tip}")
|
||
if aspect == "outsource" and not outsource and not external:
|
||
lines.append("结论:这个订单没有委外。")
|
||
elif aspect == "outsource" and (outsource or external):
|
||
lines.append("结论:这个订单涉及委外。")
|
||
return "\n".join(lines)
|
||
return str(result)
|