fix: WIP矩阵口径重构-在制无条件计入,终结态按完工时间锚点(completed_at兜底created_at)过滤,多维终结归类(含扫码入库/出库节点),下钻同步

This commit is contained in:
2026-09-07 14:46:58 +08:00
parent 2925eb8e0b
commit d757c7985b

View File

@ -652,7 +652,9 @@ async def get_wip_matrix(
- 其他已完成工序(如「测试」完成)按原工序显示,不强制归仓库态 - 其他已完成工序(如「测试」完成)按原工序显示,不强制归仓库态
这样一台设备在「上一步已完成 + 下一步待确认」时只算一次(待确认),不会重复计数。 这样一台设备在「上一步已完成 + 下一步待确认」时只算一次(待确认),不会重复计数。
since/until 按设备最新主任务的创建时间过滤。 since/until 时间过滤为“动静分离”:仅对 终结(收口/在库/出库) 设备过滤,锚点取
最新主线任务的 completed_at完工/变动时间,兜底 created_atschema 无 updated_at
在制(未收口)设备无条件全量计入,保证与人员看板实时在制不脱节。
dimension: dimension:
- assignee: 按当前任务负责人聚合dimension_key 为中文姓名) - assignee: 按当前任务负责人聚合dimension_key 为中文姓名)
@ -671,6 +673,7 @@ async def get_wip_matrix(
Task.task_name, Task.task_name,
Task.assignee_id, Task.assignee_id,
Task.created_at, Task.created_at,
Task.completed_at,
Product.current_location_id, Product.current_location_id,
Product.overall_status, Product.overall_status,
Product.status, Product.status,
@ -679,7 +682,7 @@ async def get_wip_matrix(
.where( .where(
or_( or_(
Task.parent_task_id.is_(None), Task.parent_task_id.is_(None),
Task.task_type.in_(["TRANSFER", "RECOVERY"]), Task.task_type.in_(["TRANSFER", "RECOVERY", "WAREHOUSE"]),
) )
) )
.order_by(Product.id, Task.created_at.desc()) .order_by(Product.id, Task.created_at.desc())
@ -689,33 +692,43 @@ async def get_wip_matrix(
# 每台设备 → (spec, 当前工序/负责人, 当前负责人ID) # 每台设备 → (spec, 当前工序/负责人, 当前负责人ID)
device_cur: dict[str, tuple] = {} device_cur: dict[str, tuple] = {}
seen: set[str] = set() seen: set[str] = set()
for pid, spec, product_name, task_name, assignee, created, loc, overall_status, product_status in rows: for pid, spec, product_name, task_name, assignee, created, completed, loc, overall_status, product_status in rows:
if pid in seen: if pid in seen:
continue continue
seen.add(pid) seen.add(pid)
# 时间筛选:设备最新主任务的创建时间 # ── 真实产出时间锚点:完工/变动时间优先,创建时间仅兜底 ──
if created is not None and created.tzinfo is None: # 说明tasks 表无 updated_at 列(已核实 information_schema故锚点取
created = created.replace(tzinfo=dt_timezone.utc) # completed_at or created_at若后续新增 updated_at 可并入。
if since and created is not None and created < since: anchor = completed or created
continue
if until and created is not None and created > until: # ── 动静分离:仅终结(收口/在库/出库)设备按 since~until 过滤;在制设备无条件全量 ──
continue is_terminal = (
loc == "virtual_warehouse"
or overall_status in ("待仓库收货", "已入库", "在库", "已出库")
or str(product_status).upper() in ("ARCHIVED", "OUTBOUND")
or task_name in ("扫码入库", "扫码出库")
)
if is_terminal:
if anchor is not None and anchor.tzinfo is None:
anchor = anchor.replace(tzinfo=dt_timezone.utc)
if since and anchor is not None and anchor < since:
continue
if until and anchor is not None and anchor > until:
continue
# else: 在制设备无视时间参数,保证与人员看板“实时在制”总数一致
if dimension == "task_name": if dimension == "task_name":
# 🔧 在仓库的设备按"已入库/已出库/待仓库收货"区分: # ── 终结三列判定(多维防御,不依赖单一 loc / overall──
# 已入库 = MOM 已扫码实收 (overall_status=='已入库'/'在库' 或 status=='ARCHIVED') # 已入库: 整体实收(已入库/在库) 或 status==ARCHIVED 或 收口节点「扫码入库」
# 已出库 = MOM 已发货出库 (overall_status=='已出库' 或 status=='OUTBOUND') if overall_status in ("已入库", "在库") or str(product_status).upper() == "ARCHIVED" or task_name == "扫码入库":
# 待仓库收货 = 完工已转交仓库、MOM 尚未扫码实收 key = "已入库"
# 否则按最新主任务工序名(活跃/完成态都归该工序) # 已出库: 整体已出库 或 status==OUTBOUND 或 收口节点「扫码出库」
if loc == "virtual_warehouse": elif overall_status == "已出库" or str(product_status).upper() == "OUTBOUND" or task_name == "扫码出库":
# 统一术语:待仓库收货(车间完工) → "已完成";扫码实收 → "已入库";已出库保持 key = "已出库"
if overall_status in ("已入库", "在库") or str(product_status).upper() == "ARCHIVED": # 已完成: 车间完工待实收(待仓库收货) 或 处于仓库(未实收/未出库)
key = "已入库" elif overall_status == "待仓库收货" or loc == "virtual_warehouse":
elif overall_status == "出库" or str(product_status).upper() == "OUTBOUND": key = "完成"
key = "已出库"
else:
key = "已完成"
else: else:
key = task_name or "" key = task_name or ""
else: else:
@ -796,13 +809,14 @@ async def get_wip_matrix_detail(
Task.assignee_id, Task.assignee_id,
Task.status.label("task_status"), Task.status.label("task_status"),
Task.created_at, Task.created_at,
Task.completed_at,
Task.received_at, Task.received_at,
) )
.join(Task, Task.product_id == Product.id) .join(Task, Task.product_id == Product.id)
.where( .where(
or_( or_(
Task.parent_task_id.is_(None), Task.parent_task_id.is_(None),
Task.task_type.in_(["TRANSFER", "RECOVERY"]), Task.task_type.in_(["TRANSFER", "RECOVERY", "WAREHOUSE"]),
) )
) )
.order_by(Product.id, Task.created_at.desc()) .order_by(Product.id, Task.created_at.desc())
@ -814,7 +828,7 @@ async def get_wip_matrix_detail(
matched: list[WipMatrixDetailRow] = [] matched: list[WipMatrixDetailRow] = []
raw_names: set[str] = set() raw_names: set[str] = set()
for pid, serial, ext, mat_name, spec, loc, overall, pstatus, task_name, assignee, tstatus, created, received in rows: for pid, serial, ext, mat_name, spec, loc, overall, pstatus, task_name, assignee, tstatus, created, completed, received in rows:
if pid in seen: if pid in seen:
continue continue
seen.add(pid) seen.add(pid)
@ -822,22 +836,32 @@ async def get_wip_matrix_detail(
if spec_model and (spec or "") != spec_model: if spec_model and (spec or "") != spec_model:
continue continue
# 时间筛选(设备最新主任务创建时间) # ── 真实产出时间锚点:完工/变动时间优先创建时间仅兜底schema 无 updated_at──
if created is not None and created.tzinfo is None: anchor = completed or created
created = created.replace(tzinfo=dt_timezone.utc)
if since and created is not None and created < since:
continue
if until and created is not None and created > until:
continue
# 当前工序归类(与 get_wip_matrix 一致) # ── 动静分离:仅终结(收口/在库/出库)设备按 since~until 过滤;在制设备无条件全量 ──
if loc == "virtual_warehouse": is_terminal = (
if overall in ("已入库", "在库") or str(pstatus).upper() == "ARCHIVED": loc == "virtual_warehouse"
key = "已入库" or overall in ("待仓库收货", "已入库", "在库", "已出库")
elif overall == "已出库" or str(pstatus).upper() == "OUTBOUND": or str(pstatus).upper() in ("ARCHIVED", "OUTBOUND")
key = "出库" or task_name in ("扫码入库", "扫码出库")
else: )
key = "已完成" if is_terminal:
if anchor is not None and anchor.tzinfo is None:
anchor = anchor.replace(tzinfo=dt_timezone.utc)
if since and anchor is not None and anchor < since:
continue
if until and anchor is not None and anchor > until:
continue
# else: 在制设备无视时间参数,保证与人员看板“实时在制”总数一致
# ── 终结三列判定(与 get_wip_matrix 一字不差同步,多维防御)──
if overall in ("已入库", "在库") or str(pstatus).upper() == "ARCHIVED" or task_name == "扫码入库":
key = "已入库"
elif overall == "已出库" or str(pstatus).upper() == "OUTBOUND" or task_name == "扫码出库":
key = "已出库"
elif overall == "待仓库收货" or loc == "virtual_warehouse":
key = "已完成"
else: else:
key = task_name or "" key = task_name or ""