refactor(outbound): 出库单据与领用物料合并成一张表
这两者本来就是同一件事(这台设备对应 MOM 的哪些出库单、领了哪些料),
却因为粒度不同被拆成两张表、界面上两张卡:用户要面对两个入口两个删除按钮,
还会问「我在那边挂的怎么这边看不见」。更糟的是**单据级那张没有 mom_line_id,
挂上去的料根本报不了废**。
- 新建 product_outbound_materials,统一到**明细级**(只有它带 mom_line_id,
而报废要用它定位)。单据级信息(申请单号/备注/撤回)作为冗余列落在每条明细上。
task_id 改为可空 —— 任务只是溯源信息,不再是组织维度,展示/报废/删除按设备走。
- 接口从 7 个收敛成 3 个(GET/POST/DELETE /products/{id}/outbound-materials,
外加整单删 by-order)。任务级那套连同 TaskResponse.outbound_materials 一起删掉:
保留第二个入口只会让「同一个东西两个地方」重新长出来。
- MOM 回调存档改为按 outbound_no 去 MOM **现查明细**逐行落 —— 不查的话
这台设备「领了什么料」永远是空的,也就报不了废。查不到时退化成单据级存档,
宁可显示「有这张单但看不到明细」,也不要静默丢掉这张单。
- 扫码响应补 outbound_materials(附「谁挂上去的」中文名,服务端解析)。
⚠️ 依赖 task_tree_loader 的 selectinload —— 异步 session 下懒加载会
MissingGreenlet。
- 前端两张卡合并成一张:按出库单号分组、点开看明细,明细行才有报废/删除。
This commit is contained in:
@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
class ProductCreate(BaseModel):
|
||||
@ -18,6 +18,14 @@ class ProductCreate(BaseModel):
|
||||
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")
|
||||
# 建档时一并挂钩的 MOM 出库**明细行** ID(trans_outbound.id)。
|
||||
# 前端按整张出库单勾选,提交时把该单全部明细 ID 带过来;后端归并回单据后
|
||||
# 写进 product_outbounds(一行 = 一张单,source=manual)。
|
||||
# ⚠️ 默认空列表:其它调用方(旧前端、脚本)不带该字段,必须保持行为不变。
|
||||
mom_line_ids: list[int] = Field(
|
||||
default_factory=list,
|
||||
description="建档时挂钩的 MOM 出库明细行ID(trans_outbound.id)",
|
||||
)
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@ -80,6 +88,139 @@ class ProductResponse(BaseModel):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class ProductOutboundResponse(BaseModel):
|
||||
"""产品出库记录 — 来自 MOM 出库回调的单据存档
|
||||
|
||||
一次出库一行,按出库时间倒序返回。**已撤回的记录照常留在列表里**
|
||||
(is_revoked=True),由前端打标记 —— 不在后端过滤掉,「出过又撤了」
|
||||
本身就是要看得见的历史。
|
||||
"""
|
||||
id: uuid.UUID
|
||||
outbound_no: str # MOM 出库单号
|
||||
request_no: str | None = None # MOM 出库申请单号
|
||||
consumer_name: str | None = None # 领用人/客户
|
||||
applicant_name: str | None = None # 申请人姓名
|
||||
operator: str | None = None # MOM 侧实际扫码出库人
|
||||
outbound_type: str | None = None # SALES / USE / PRODUCTION(只展示,不做业务判断)
|
||||
outbound_time: datetime | None = None # MOM 记录的出库时间
|
||||
remark: str | None = None
|
||||
is_revoked: bool = False
|
||||
revoked_at: datetime | None = None
|
||||
# 这一行怎么来的:webhook(MOM 回调自动存档) | manual(人工在界面挂的)
|
||||
source: str = "webhook"
|
||||
created_at: datetime # 本行写入时间
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class ProductOutboundMaterialResponse(BaseModel):
|
||||
"""设备的一条 MOM 出库明细(合并后的统一形态)
|
||||
|
||||
一行 = 设备上的一条出库明细。`mom_line_id` 为空表示这条是**单据级存档**
|
||||
(MOM 回调时查不到明细),能看、能标撤回,但**不能报废** —— 报废要用它定位。
|
||||
"""
|
||||
id: int
|
||||
product_id: uuid.UUID
|
||||
serial_number: str | None = None
|
||||
task_id: uuid.UUID | None = None # 仅溯源,不参与展示/报废/删除
|
||||
mom_line_id: int | None = None # MOM trans_outbound.id;为空=无明细的存档
|
||||
outbound_no: str
|
||||
# ---- 单据级(同单内一致,冗余在每条明细上) ----
|
||||
request_no: str | None = None
|
||||
applicant_name: str | None = None
|
||||
remark: str | None = None
|
||||
# ---- 明细级快照 ----
|
||||
sku: str | None = None
|
||||
material_name: str | None = None
|
||||
spec_model: str | None = None
|
||||
quantity: float | None = None # 出库单原值,**不是**本设备用量
|
||||
unit_price: float | None = None
|
||||
outbound_type: str | None = None
|
||||
outbound_type_label: str = "" # 服务端下发的中文名
|
||||
consumer_name: str | None = None # 领用人/客户
|
||||
operator_name: str | None = None
|
||||
warehouse_location: str | None = None
|
||||
outbound_time: datetime | None = None
|
||||
# ---- 来源与撤回 ----
|
||||
source: str = "manual" # manual(可删) | webhook(系统事实,不可删)
|
||||
is_revoked: bool = False
|
||||
revoked_at: datetime | None = None
|
||||
added_by: str | None = None # 谁挂上去的(Track 用户名)
|
||||
# 谁挂上去的(中文姓名)。由服务端解析下发 —— 前端不做 username→姓名映射,
|
||||
# 否则移动端/网页端各抄一份,迟早漂移。
|
||||
added_by_name: str = ""
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _fill_type_label(self):
|
||||
"""出库类型码 → 中文名。统一在 schema 派生,避免各构造点漏填。"""
|
||||
if not self.outbound_type_label and self.outbound_type:
|
||||
from app.services.mom_outbound_service import describe_outbound_type
|
||||
self.outbound_type_label = describe_outbound_type(self.outbound_type)
|
||||
return self
|
||||
|
||||
|
||||
class ProductScrapCreate(BaseModel):
|
||||
"""提交生产报废的请求体。
|
||||
|
||||
⚠️ 只传 `mom_line_id`,物料快照一律由后端从 Track 已挂的出库物料里取 ——
|
||||
不接受前端传快照,否则前端可以伪造「报废了什么」。
|
||||
"""
|
||||
# MOM trans_outbound.id,也就是 Track 侧 task_outbound_materials.mom_line_id。
|
||||
# 后端会校验它**确实挂在本产品上** —— 这是跨设备乱报的唯一防线
|
||||
# (可见范围是整台设备、不是「谁领的」,所以不能靠隐藏来防)。
|
||||
mom_line_id: int
|
||||
# 本次报废数量。上限由 MOM 判(不能超过该出库明细的可退额度),
|
||||
# 这里不重复校验,避免两处口径漂移。
|
||||
quantity: float
|
||||
# 用户填的原因说明(选填)
|
||||
reason: str | None = None
|
||||
# 幂等锚点:前端在**打开弹层时**生成一次,重试时复用同一个。
|
||||
# 后端拼成 <公司>:<track_ref> 发给 MOM,两边同一口径。
|
||||
track_ref: str
|
||||
|
||||
|
||||
class ProductScrapResponse(BaseModel):
|
||||
"""生产报废记录 — Track 发起、MOM 受理的报废单。
|
||||
|
||||
`mom_status` / `total_loss` 是**实时回查 MOM** 的结果,不是本地快照
|
||||
(本地那列只在 MOM 暂时查不到时兜底)。
|
||||
· `mom_status_label`:「待审批 / 已通过(待执行)/ 已执行 / 已驳回 / 已撤回」
|
||||
· `mom_executed=False` 时 `total_loss` 是 **None 而不是 0** ——
|
||||
区分「还没执行」和「执行了但损失为 0」,别让用户把未审批看成 0 元损失
|
||||
"""
|
||||
id: uuid.UUID
|
||||
product_id: uuid.UUID
|
||||
serial_number: str | None = None
|
||||
task_id: uuid.UUID | None = None
|
||||
mom_line_id: int
|
||||
# 快照:MOM 侧数据被清理后仍要能显示「报了什么」
|
||||
outbound_no: str | None = None
|
||||
material_name: str | None = None
|
||||
spec_model: str | None = None
|
||||
sku: str | None = None
|
||||
consumer_name: str | None = None # 原领用人,前端据此提示「代报」
|
||||
quantity: float
|
||||
reason_category: str = "PRODUCTION"
|
||||
reason: str | None = None
|
||||
scrap_request_no: str
|
||||
defective_goods_id: int | None = None
|
||||
submitted_by: str | None = None
|
||||
created_at: datetime
|
||||
# ---- 以下为实时回查 MOM 的结果 ----
|
||||
mom_status: int = 0
|
||||
mom_status_label: str = ""
|
||||
mom_approved_at: str | None = None
|
||||
mom_executor_name: str = ""
|
||||
mom_executed: bool = False
|
||||
total_loss: float | None = None # 报废损失(单价 × 实报废数量)
|
||||
scrapped_quantity: float | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class ProductScanResponse(BaseModel):
|
||||
"""扫码查询响应 — 产品信息 + 完整任务树(递归嵌套)"""
|
||||
id: uuid.UUID
|
||||
@ -102,6 +243,9 @@ class ProductScanResponse(BaseModel):
|
||||
top_level_tasks: list[TaskSummaryResponse] = []
|
||||
task_tree: list[TaskResponse] = []
|
||||
assignee_names: dict[str, str] = {} # 🔧 username→中文姓名映射
|
||||
# 🔧 出库单据存档(来自 MOM 出库回调),按出库时间倒序。
|
||||
# 本次功能上线前出库的设备没有存档,这里是空列表 —— 不是错误。
|
||||
outbound_records: list[ProductOutboundMaterialResponse] = []
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
@ -19,6 +19,15 @@ class TaskCreate(BaseModel):
|
||||
notify_parent_on_complete: bool = Field(False, description="完成后是否通知父任务")
|
||||
is_rework: bool = Field(False, description="是否为返工任务")
|
||||
remark: str | None = Field(None, max_length=2000, description="初始描述/交接备注")
|
||||
# 创建时一并挂载的 MOM 出库明细行 ID(MOM trans_outbound.id)。
|
||||
# 粒度是**明细行**,但前端是按整张出库单勾选的 —— 提交时把该单的全部明细
|
||||
# ID 一起带过来。
|
||||
# ⚠️ 默认空列表:移动端的 doCreateFirstTask 仍在调本接口且不带该字段,
|
||||
# 必须保持「不传就等同于不挂载」的行为不变。
|
||||
mom_line_ids: list[int] = Field(
|
||||
default_factory=list,
|
||||
description="创建时挂载的 MOM 出库明细行ID(trans_outbound.id)",
|
||||
)
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@ -153,6 +162,55 @@ class TaskRecordResponse(BaseModel):
|
||||
# 响应模型
|
||||
# ============================================================
|
||||
|
||||
class TaskOutboundMaterialResponse(BaseModel):
|
||||
"""任务挂载的一条 MOM 出库物料明细(挂载时从 MOM 取的快照)
|
||||
|
||||
一次挂载会展开成多行(挂一张出库单 = 该单的全部明细各一行),
|
||||
前端按 outbound_no 分组展示。
|
||||
"""
|
||||
id: int
|
||||
# ★ 料挂在哪条任务上。前端按任务分组展示时必须拿它做 key ——
|
||||
# 不能用 task_name:同一台设备可能有两个同名任务(例如两道「生产」),
|
||||
# 按名字分会把它们并成一组,看起来像一条任务领了两遍料。
|
||||
task_id: uuid.UUID
|
||||
mom_line_id: int # MOM trans_outbound.id,供反查比对
|
||||
outbound_no: str # MOM 出库单号
|
||||
sku: str | None = None
|
||||
material_name: str | None = None
|
||||
spec_model: str | None = None
|
||||
# 用 float 而非 Decimal:Pydantic v2 会把 Decimal 序列化成字符串,
|
||||
# 前端拿到 "5.0000" 不好直接用。数量量级很小(实测 1~186),float 足够。
|
||||
quantity: float | None = None # 出库单原值,**不是**本任务用量
|
||||
unit_price: float | None = None
|
||||
outbound_type: str | None = None
|
||||
# 出库类型中文名。与 MOM 出库单查询(mom_outbounds)同一套码表、同一份实现,
|
||||
# 由服务端下发 —— 前端不再自建映射,否则两边会开始漂移。
|
||||
# 这里用 model_validator 自动派生而不是每个构造点手填:构造点有 3 处
|
||||
# (task_service 两处 + product_service 一处),漏一个就是空白徽标。
|
||||
outbound_type_label: str = ""
|
||||
consumer_name: str | None = None # 领用人/客户
|
||||
operator_name: str | None = None
|
||||
warehouse_location: str | None = None
|
||||
outbound_time: datetime | None = None
|
||||
added_by: str | None = None # 挂载人(逻辑外键→MOM sys_user)
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _fill_outbound_type_label(self):
|
||||
"""出库类型码 → 中文名(PRODUCTION→生产出库 等)。
|
||||
|
||||
在 schema 上统一派生,而不是让 3 个构造点各自记得填 ——
|
||||
漏一个就是空白徽标,而且不会报错,只能靠肉眼发现。
|
||||
延迟 import:schemas 被 services 依赖,模块级 import 会形成环。
|
||||
"""
|
||||
if not self.outbound_type_label and self.outbound_type:
|
||||
from app.services.mom_outbound_service import describe_outbound_type
|
||||
self.outbound_type_label = describe_outbound_type(self.outbound_type)
|
||||
return self
|
||||
|
||||
|
||||
class TaskSummaryResponse(BaseModel):
|
||||
"""任务摘要 — 扫码时用,不含嵌套子任务"""
|
||||
id: uuid.UUID
|
||||
@ -195,6 +253,9 @@ class TaskResponse(BaseModel):
|
||||
child_tasks: list[TaskResponse] = []
|
||||
records: list[TaskRecordResponse] = []
|
||||
created_by: str | None = None # 谁创建的(从task_logs追溯)
|
||||
# 本任务挂载的 MOM 出库物料(明细级快照)。创建任务时可选、之后可追加,
|
||||
# 见 models/task_outbound_material.py。
|
||||
outbound_materials: list[TaskOutboundMaterialResponse] = []
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user