2026-07-21 11:05:57 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 世界数据视图(moduleId: domain-views, 可重生 ✅)
|
|
|
|
|
|
# 把世界状态投影为前端三个视口所需的数据(P0 只读):
|
|
|
|
|
|
# summary(KPI) / gantt(甘特) / load(负荷热力) / due(交期承诺看板)
|
|
|
|
|
|
# 过滤/高亮所需的标记(客户、超期、冲突)由后端一次性算好附在数据上
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
from __future__ import annotations # 前向类型引用
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Any # 类型标注
|
|
|
|
|
|
|
|
|
|
|
|
from server.engines.queries import get_available_minutes # 班次可用分钟(热力图)
|
|
|
|
|
|
from server.timeutil import add_minutes, fmt_date, parse_dt, today0 # 日期工具
|
|
|
|
|
|
|
|
|
|
|
|
# 世界状态类型别名
|
|
|
|
|
|
World = dict[str, Any]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _latest_version(world: World) -> dict | None:
|
|
|
|
|
|
"""取最新排产版本(无版本返回 None)。"""
|
|
|
|
|
|
versions = world["scheduleVersions"] # 版本表
|
|
|
|
|
|
return versions[-1] if versions else None # 末尾即最新
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _version_pos(world: World, version_id: int) -> list[dict]:
|
|
|
|
|
|
"""取某版本的全部生产订单。"""
|
|
|
|
|
|
return [p for p in world["productionOrders"] if p["schedulingVersionId"] == version_id] # 按版本过滤
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _is_overdue_po(world: World, po: dict) -> bool:
|
|
|
|
|
|
"""判断生产订单是否超期:计划完成晚于销售订单交期当日 18:00。"""
|
|
|
|
|
|
so = next((s for s in world["salesOrders"] if s["id"] == po["salesOrderId"]), None) # 关联订单
|
|
|
|
|
|
if not so or not po.get("plannedEndDate"): # 数据缺失视为不超期
|
|
|
|
|
|
return False
|
|
|
|
|
|
return parse_dt(po["plannedEndDate"]) > parse_dt(so["deliveryDate"] + " 18:00") # 比较交期
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def world_summary(world: World) -> dict[str, Any]:
|
|
|
|
|
|
"""KPI 摘要:最新版本核心指标(视口工具栏 KPI 卡组数据源)。"""
|
|
|
|
|
|
v = _latest_version(world) # 最新版本
|
|
|
|
|
|
if not v: # 尚无版本
|
|
|
|
|
|
return {"hasVersion": False}
|
|
|
|
|
|
return { # 有版本 → 摘要字段
|
|
|
|
|
|
"hasVersion": True, "versionNo": v["versionNo"], "status": v["status"],
|
|
|
|
|
|
"woCount": v["woCount"], "poCount": v["poCount"], "conflictCount": v["conflictCount"],
|
|
|
|
|
|
"totalTardiness": round(v["totalTardiness"], 1),
|
|
|
|
|
|
"avgUtilization": round(v["avgUtilization"], 3),
|
|
|
|
|
|
"totalCost": round(v["totalCost"], 0),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def gantt_view(world: World) -> dict[str, Any]:
|
|
|
|
|
|
"""甘特视图数据:产线/工位骨架 + 最新版本工单(附过滤与高亮标记)。"""
|
|
|
|
|
|
v = _latest_version(world) # 最新版本
|
|
|
|
|
|
pos = _version_pos(world, v["id"]) if v else [] # 该版本 PO
|
|
|
|
|
|
po_by_id = {p["id"]: p for p in pos} # PO 索引
|
|
|
|
|
|
so_by_id = {s["id"]: s for s in world["salesOrders"]} # SO 索引
|
|
|
|
|
|
wos_out: list[dict] = [] # 输出工单集
|
|
|
|
|
|
for wo in world["workOrders"]: # 遍历全部工单
|
|
|
|
|
|
po = po_by_id.get(wo["productionOrderId"]) # 关联 PO(仅最新版本)
|
|
|
|
|
|
if not po: # 非本版本工单不输出
|
|
|
|
|
|
continue
|
|
|
|
|
|
so = so_by_id.get(po["salesOrderId"], {}) # 关联 SO(客户标记)
|
|
|
|
|
|
wos_out.append({ # 投影为前端 GanttWO 契约
|
|
|
|
|
|
"id": wo["id"], "orderNo": wo["orderNo"], "productId": wo["productId"],
|
|
|
|
|
|
"productName": wo["productName"], "quantity": wo["quantity"],
|
|
|
|
|
|
"operationName": wo["operationName"], "lineId": wo["lineId"],
|
|
|
|
|
|
"workstationId": wo["workstationId"], "workstationName": wo["workstationName"],
|
|
|
|
|
|
"start": wo["plannedStartTime"], "end": wo["plannedEndTime"],
|
|
|
|
|
|
"customerName": so.get("customerName", ""), "customerLevel": so.get("customerLevel", ""),
|
|
|
|
|
|
"overdue": _is_overdue_po(world, po), # 超期标记(高亮用)
|
|
|
|
|
|
# 冲突标记:齐套非通过 或 工单/PO 冲突计数 > 0
|
|
|
|
|
|
"conflict": wo["kitStatus"] != "PASSED" or wo["conflictCount"] > 0 or po["conflictCount"] > 0,
|
|
|
|
|
|
})
|
|
|
|
|
|
return { # 视图整体响应
|
|
|
|
|
|
"lines": [{"id": l["id"], "code": l["code"], "name": l["name"]} for l in world["lines"]], # 产线骨架
|
|
|
|
|
|
"workstations": [{"id": w["id"], "lineId": w["lineId"], "name": w["name"], "seq": w["sequenceNo"]}
|
|
|
|
|
|
for w in world["workstations"]], # 工位骨架
|
|
|
|
|
|
"workOrders": wos_out, # 工单条
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_view(world: World, days: int = 14) -> dict[str, Any]:
|
|
|
|
|
|
"""负荷热力视图:产线 × 未来 N 天的负荷率(占用/可用分钟)。"""
|
|
|
|
|
|
v = _latest_version(world) # 最新版本
|
|
|
|
|
|
pos = _version_pos(world, v["id"]) if v else [] # 该版本 PO
|
|
|
|
|
|
po_ids = {p["id"] for p in pos} # PO ID 集
|
|
|
|
|
|
day_list = [fmt_date(add_minutes(today0(), i * 24 * 60)) for i in range(days)] # 日期列
|
|
|
|
|
|
rows: list[dict] = [] # 输出行集
|
|
|
|
|
|
for line in world["lines"]: # 逐产线
|
|
|
|
|
|
cells: list[dict] = [] # 该产线的逐日单元格
|
|
|
|
|
|
for d in day_list: # 逐日
|
|
|
|
|
|
used = sum( # 当日占用分钟:本版本、本线、当日开工的工单
|
|
|
|
|
|
(parse_dt(w["plannedEndTime"]) - parse_dt(w["plannedStartTime"])).total_seconds() / 60
|
|
|
|
|
|
for w in world["workOrders"]
|
|
|
|
|
|
if w["productionOrderId"] in po_ids and w["lineId"] == line["id"]
|
|
|
|
|
|
and w["plannedStartTime"][:10] == d)
|
|
|
|
|
|
avail = get_available_minutes(world, line["id"], d) # 当日可用分钟
|
|
|
|
|
|
ratio = (used / avail) if avail else 0.0 # 负荷率(无班次记 0,前端按 avail=0 显示"休")
|
|
|
|
|
|
cells.append({"ratio": round(ratio, 3), "used": round(used), "avail": avail}) # 单元格
|
2026-07-23 13:38:43 +08:00
|
|
|
|
rows.append({"lineId": line["id"], "lineCode": line["code"], # 产线行(带编码供聚焦匹配)
|
|
|
|
|
|
"lineName": line["name"], "cells": cells})
|
2026-07-21 11:05:57 +08:00
|
|
|
|
return {"days": day_list, "rows": rows} # 视图响应
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def due_view(world: World) -> list[dict[str, Any]]:
|
|
|
|
|
|
"""交期承诺看板:逐销售订单的承诺完成、裕量与风险(PPT 交期承诺看板的 M1 形态)。"""
|
|
|
|
|
|
v = _latest_version(world) # 最新版本
|
|
|
|
|
|
pos = _version_pos(world, v["id"]) if v else [] # 该版本 PO
|
|
|
|
|
|
rows: list[dict] = [] # 输出行
|
|
|
|
|
|
for so in world["salesOrders"]: # 遍历订单
|
|
|
|
|
|
if so["status"] == "CANCELLED": # 已取消不展示
|
|
|
|
|
|
continue
|
|
|
|
|
|
my_pos = [p for p in pos if p["salesOrderId"] == so["id"]] # 该订单的 PO
|
|
|
|
|
|
promised = max((p["plannedEndDate"] for p in my_pos), default=None) # 承诺完成=各 PO 最晚完成
|
|
|
|
|
|
margin: float | None = None # 裕量小时
|
|
|
|
|
|
risk = "none" # 默认:未排产
|
|
|
|
|
|
if promised: # 已排产 → 计算裕量与风险
|
|
|
|
|
|
due = parse_dt(so["deliveryDate"] + " 18:00") # 交期基准
|
|
|
|
|
|
margin = round((due - parse_dt(promised)).total_seconds() / 3600, 1) # 正=提前
|
|
|
|
|
|
risk = "high" if margin < 0 else ("mid" if margin < 24 else "low") # 风险分档
|
|
|
|
|
|
rows.append({ # 投影为 DueRow 契约
|
|
|
|
|
|
"orderNo": so["orderNo"], "customerName": so["customerName"],
|
|
|
|
|
|
"customerLevel": so["customerLevel"], "isRush": so["isRush"],
|
|
|
|
|
|
"deliveryDate": so["deliveryDate"], "promisedEnd": promised,
|
|
|
|
|
|
"marginHours": margin, "risk": risk,
|
|
|
|
|
|
})
|
|
|
|
|
|
return rows # 看板行集
|