【直接完结:保留动作,剥离状态】 - transfer_task 权限硬拦截:仅 SUPER_ADMIN / SUPERVISOR。判据取签名保护的 operator_role,刻意不用客户端可通过 ?operator_id= 伪造的 operator_id - 直接完结【不再改写】overall_status —— 它只是任务闭环动作,不改变物理状态。 已出库设备完结后依然是「已出库」,回归 WIP 矩阵的已出库列 - 物理终态保护:create_task / receive_task / transfer_task 三处写入点统一加 _is_physical_terminal 守卫,禁止用任务名覆写 已入库/在库/已出库。 历史缺陷:「发货测试」会把设备的「已出库」标识静默抹掉,跨报表口径随之打架 【位置与工序口径】 - _recalc_product_location:已出库且闲置 → 位置清空(货发走就离场)。 只清「已出库」;已入库/在库的设备确实还在仓库里,位置必须保留 - MOM 出库回调同步清空 current_location_id - 新增 ProductResponse.current_step:只认活跃主干任务,无活跃任务时返回 宏观终态或空。不再让已 COMPLETED 的历史工序(扫码出库/测试…)冒充"当前工序" 【售后烙印去死锁】 - 拆分 OUTBOUND_QC_STEPS(发货测试) / AFTER_SALES_REPAIR_STEPS(售后维修) - resolve_phase_for_step 与 _mark_after_sales_if_reactivated 双双改为仅 「售后维修」触发 AFTER_SALES,解除「已出库 + 发货测试」被永久烙印的死锁 - PRODUCTION_OVERALL_STEPS 补入「发货测试」,使出厂质检在生产阶段合法 (否则 _enforce_step_isolation 会把已出库设备的发货测试直接 400) 【游魂收口】 - get_wip_matrix / get_wip_matrix_detail 的 is_terminal 增加"名下再无任何活跃 任务则视同终结态",让被直接完结的生产设备接受时间筛选,不再恒挂在看板上 冒充在制。两处必须一字不差同步,否则会出现"矩阵有数、下钻为空"
112 lines
5.0 KiB
Python
112 lines
5.0 KiB
Python
"""产品 Pydantic Schemas"""
|
||
from __future__ import annotations
|
||
import uuid
|
||
from datetime import datetime
|
||
from pydantic import BaseModel, Field
|
||
|
||
|
||
class ProductCreate(BaseModel):
|
||
"""创建产品 — serial_number 由后端自动生成 16 位 HEX"""
|
||
# MOM 物料挂载(必填)
|
||
material_id: str = Field(..., max_length=64, description="MOM物料ID(必选)")
|
||
material_name: str = Field("", max_length=255, description="物料名称")
|
||
spec_model: str = Field("", max_length=255, description="规格型号")
|
||
category: str = Field("", max_length=100, description="物料类别")
|
||
material_type: str = Field("", max_length=100, description="物料类型")
|
||
# 可选
|
||
external_serial: str | None = Field(None, max_length=64, description="产品序列号(用户自定义,选填)")
|
||
order_id: uuid.UUID | None = Field(None, description="所属订单ID(选填)")
|
||
order_no: str | None = Field(None, max_length=64, description="订单号(自由键入,选填)")
|
||
parent_product_id: uuid.UUID | None = Field(None, description="父产品ID")
|
||
|
||
model_config = {"from_attributes": True}
|
||
|
||
|
||
class ProductUpdate(BaseModel):
|
||
"""更新产品"""
|
||
serial_number: str | None = Field(None, min_length=16, max_length=16)
|
||
external_serial: str | None = Field(None, max_length=64)
|
||
material_id: str | None = Field(None, max_length=64)
|
||
material_name: str | None = Field(None, max_length=255)
|
||
spec_model: str | None = Field(None, max_length=255)
|
||
order_no: str | None = Field(None, max_length=64, description="订单号")
|
||
status: str | None = Field(None, max_length=50, description="产品状态")
|
||
parent_product_id: uuid.UUID | None = Field(None)
|
||
|
||
model_config = {"from_attributes": True}
|
||
|
||
|
||
class ProductResponse(BaseModel):
|
||
"""产品响应"""
|
||
id: uuid.UUID
|
||
serial_number: str
|
||
external_serial: str | None = None
|
||
order_id: uuid.UUID | None = None
|
||
order_no: str = ""
|
||
material_id: str | None
|
||
material_name: str | None = None
|
||
spec_model: str | None = None
|
||
category: str | None = None
|
||
material_type: str | None = None
|
||
parent_product_id: uuid.UUID | None
|
||
current_location_id: str | None = None
|
||
current_location_name: str | None = None
|
||
macro_status: str | None = None # 🔧 后端预计算的任务树状态(免前端逐条展开)
|
||
overall_status: str | None = None
|
||
# 【当前工序】—— 只反映"此刻在做什么",绝不拿历史工序冒充。
|
||
# · 有活跃主干任务(WIP/PENDING) → 该任务工序名
|
||
# · 否则产品处于宏观终态(待仓库收货/已入库/在库/已出库) → 该终态(表达"货在哪")
|
||
# · 否则(活已干完、只剩工序名残留)→ ""(前端显示「—」)
|
||
# ⚠️ 必须与 overall_status 分开:ProductResponse.overall_status 会被"最新主干任务名"
|
||
# 覆盖(见 product_service 的 overall_names),于是已 COMPLETED 的历史工序
|
||
# (扫码出库 / 测试 / 发货测试…)会被当成"当前工序"长期展示。
|
||
current_step: str = ""
|
||
status: str
|
||
# 🔧 生命周期阶段:PRODUCTION(生产制造/发货测试) | AFTER_SALES(出库后返厂售后维修)
|
||
lifecycle_phase: str = "PRODUCTION"
|
||
created_at: datetime
|
||
# 🔧 最新动态 — 该产品活跃任务的最新记录
|
||
latest_record_time: datetime | None = None
|
||
latest_record_content: str | None = None
|
||
latest_record_has_images: bool = False
|
||
latest_record_assignee_id: str | None = None # 🔧 最新记录操作人(消除并发张冠李戴)
|
||
latest_record_assignee_name: str | None = None
|
||
# 🔧 当前人滞留时长 — 活跃任务(WIP/PENDING)最早接手时间到现在的时长(小时)
|
||
active_duration_hours: float | None = None
|
||
# 🔧 生产总天数(自创建至今)
|
||
production_days: int = 0 # 自然天
|
||
production_days_workdays: int = 0 # 工作日(排除周末/节假日)
|
||
|
||
model_config = {"from_attributes": True}
|
||
|
||
|
||
class ProductScanResponse(BaseModel):
|
||
"""扫码查询响应 — 产品信息 + 完整任务树(递归嵌套)"""
|
||
id: uuid.UUID
|
||
serial_number: str
|
||
external_serial: str | None = None
|
||
order_id: uuid.UUID | None = None
|
||
order_no: str = ""
|
||
material_id: str | None
|
||
material_name: str | None = None
|
||
spec_model: str | None = None
|
||
category: str | None = None
|
||
material_type: str | None = None
|
||
parent_product_id: uuid.UUID | None
|
||
current_location_id: str | None = None
|
||
overall_status: str | None = None
|
||
status: str
|
||
# 🔧 生命周期阶段:PRODUCTION(生产制造/发货测试) | AFTER_SALES(出库后返厂售后维修)
|
||
lifecycle_phase: str = "PRODUCTION"
|
||
created_at: datetime
|
||
top_level_tasks: list[TaskSummaryResponse] = []
|
||
task_tree: list[TaskResponse] = []
|
||
assignee_names: dict[str, str] = {} # 🔧 username→中文姓名映射
|
||
|
||
model_config = {"from_attributes": True}
|
||
|
||
|
||
# 延迟导入,避免循环引用
|
||
from app.schemas.task import TaskSummaryResponse, TaskResponse # noqa: E402
|
||
ProductScanResponse.model_rebuild()
|