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