Files
ShiErFeng/shierfeng-fastapi-backend/module_business/entity/vo/master_summary_vo.py
T
yujiangandClaude Opus 5.5 3050ccdc4e Feat: 后端新增基础资料与业务管理只读汇总接口
为 Web 端改版提供名称、统计和看板数据。只新增只读查询、扩展列表与详情
返回字段;写接口、托盘状态机和幂等键规则均未改动,未新增权限标识。

- 基础资料:列表补充名称与汇总,客户详情返回现场树,托盘详情返回最近流转,
  托盘列表支持按流转阶段筛选和二维码精确匹配;新增现场、产线、隧道的上级
  资料选项接口;Excel 导入支持 preview 只校验不写入
- 发货出库:列表支持 keyword、receiptState 筛选并补充名称、托盘数、包数和
  菌种构成;详情明细补充托盘号、菌种、批次
- 首次入库:列表与详情补充名称、托盘数、托盘当前状态;新增 /summary
- 托盘退回:列表支持 keyword 并补充名称与托盘汇总,详情补充托盘号和隧道;
  新增 /summary、/site-options
- 工厂装填:列表支持 keyword 并补充托盘号、仓库、操作人;新增 /summary、
  /batches、/batches/detail
- 客户现场使用:列表支持 keyword、fifoSkipped 并补充名称和被跳过的先到托盘号;
  新增 /site-options、/site-board 以及 usage-fifo 的 /queue
- 先到先用 DAO 抽出公共候选条件与排序,建议查询生成的 SQL 与改动前一致
- 新增各汇总 DAO 的单元测试,同步更新路由权限与模型字段契约测试

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 05:03:10 -07:00

218 lines
11 KiB
Python

from datetime import date, datetime
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from module_business.entity.vo.master_vo import (
CustomerModel,
LineModel,
PalletModel,
SiteModel,
StrainModel,
TunnelModel,
WarehouseModel,
)
class _ReadModel(BaseModel):
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
class MasterUserBriefModel(_ReadModel):
"""已分配到仓库或客户现场的用户摘要。"""
user_id: int = Field(description='用户ID')
user_name: str = Field(description='用户账号')
nick_name: str | None = Field(default=None, description='用户昵称')
class StrainListItemModel(StrainModel):
"""菌种列表行:主数据加当前在库与在外的物料量。"""
warehouse_pallet_count: int = Field(default=0, description='仓库有料托盘数')
warehouse_package_qty: int = Field(default=0, description='仓库有料包数')
field_pallet_count: int = Field(default=0, description='在途与客户现场有料托盘数')
field_package_qty: int = Field(default=0, description='在途与客户现场剩余包数')
nearest_expiry_date: date | None = Field(default=None, description='仓库在库物料最近有效期')
last_filled_at: datetime | None = Field(default=None, description='最近装填时间')
filling_count_30d: int = Field(default=0, description='近30天装填次数')
class CustomerListItemModel(CustomerModel):
"""客户列表行:主数据加现场层级与在用托盘规模。"""
site_count: int = Field(default=0, description='现场数')
line_count: int = Field(default=0, description='产线数')
tunnel_count: int = Field(default=0, description='隧道数')
site_pallet_count: int = Field(default=0, description='客户现场托盘数')
transit_pallet_count: int = Field(default=0, description='在途托盘数')
user_count: int = Field(default=0, description='已分配现场用户数')
class CustomerSiteLineModel(_ReadModel):
line_id: int = Field(description='产线ID')
line_code: str = Field(description='产线编码')
line_name: str = Field(description='产线名称')
status: str = Field(description='状态')
active_pallet_count: int = Field(default=0, description='产线上的托盘数')
class CustomerSiteTunnelModel(_ReadModel):
tunnel_id: int = Field(description='隧道ID')
tunnel_code: str = Field(description='隧道编号')
tunnel_name: str = Field(description='隧道名称')
status: str = Field(description='状态')
current_trace_no: str | None = Field(default=None, description='当前物料溯源号')
current_trace_started_at: datetime | None = Field(default=None, description='当前溯源批次开始时间')
active_pallet_count: int = Field(default=0, description='隧道内托盘数')
class CustomerSiteNodeModel(_ReadModel):
"""客户详情中的一个现场节点。"""
site_id: int = Field(description='现场ID')
site_code: str = Field(description='现场编码')
site_name: str = Field(description='现场名称')
address: str | None = Field(default=None, description='现场地址')
status: str = Field(description='状态')
unused_pallet_count: int = Field(default=0, description='现场未使用托盘数')
partial_pallet_count: int = Field(default=0, description='现场部分使用托盘数')
empty_pallet_count: int = Field(default=0, description='现场空托盘数')
transit_pallet_count: int = Field(default=0, description='在途托盘数')
remaining_package_qty: int = Field(default=0, description='现场剩余包数')
lines: list[CustomerSiteLineModel] = Field(default_factory=list, description='产线')
tunnels: list[CustomerSiteTunnelModel] = Field(default_factory=list, description='隧道')
users: list[MasterUserBriefModel] = Field(default_factory=list, description='已分配用户')
class CustomerDetailModel(CustomerModel):
"""客户详情:主数据加现场、产线、隧道、托盘与用户层级。"""
sites: list[CustomerSiteNodeModel] = Field(default_factory=list, description='现场层级')
class SiteListItemModel(SiteModel):
"""客户现场列表行:主数据加所属客户、层级规模、托盘与授权用户。"""
customer_code: str | None = Field(default=None, description='客户编码')
customer_name: str | None = Field(default=None, description='客户名称')
line_count: int = Field(default=0, description='产线数')
tunnel_count: int = Field(default=0, description='隧道数')
unused_pallet_count: int = Field(default=0, description='现场未使用托盘数')
partial_pallet_count: int = Field(default=0, description='现场部分使用托盘数')
empty_pallet_count: int = Field(default=0, description='现场空托盘数')
transit_pallet_count: int = Field(default=0, description='在途托盘数')
remaining_package_qty: int = Field(default=0, description='现场剩余包数')
user_count: int = Field(default=0, description='已分配用户数')
users: list[MasterUserBriefModel] = Field(default_factory=list, description='已分配用户(前5位)')
class LineListItemModel(LineModel):
"""产线列表行:主数据加所属现场与近期使用。"""
site_code: str | None = Field(default=None, description='现场编码')
site_name: str | None = Field(default=None, description='现场名称')
customer_name: str | None = Field(default=None, description='客户名称')
active_pallet_count: int = Field(default=0, description='产线上的托盘数')
usage_count_30d: int = Field(default=0, description='近30天使用次数')
used_package_qty_30d: int = Field(default=0, description='近30天使用包数')
last_used_at: datetime | None = Field(default=None, description='最近使用时间')
class TunnelListItemModel(TunnelModel):
"""隧道列表行:主数据加所属现场、隧道内托盘与近期使用。"""
site_code: str | None = Field(default=None, description='现场编码')
site_name: str | None = Field(default=None, description='现场名称')
customer_name: str | None = Field(default=None, description='客户名称')
active_pallet_count: int = Field(default=0, description='隧道内托盘数')
active_package_qty: int = Field(default=0, description='隧道内剩余包数')
usage_count_30d: int = Field(default=0, description='近30天使用次数')
last_used_at: datetime | None = Field(default=None, description='最近使用时间')
class WarehouseListItemModel(WarehouseModel):
"""仓库列表行:主数据加库存、今日作业、待处理与授权用户。"""
filled_pallet_count: int = Field(default=0, description='有料托盘数')
filled_package_qty: int = Field(default=0, description='库存包数')
empty_pallet_count: int = Field(default=0, description='空托盘数')
expiring_pallet_count: int = Field(default=0, description='7天内到期托盘数')
today_filling_count: int = Field(default=0, description='今日装填托盘数')
today_shipment_pallet_count: int = Field(default=0, description='今日发货托盘数')
draft_count: int = Field(default=0, description='待确认入库与发货单数')
incoming_return_count: int = Field(default=0, description='待确认退回单数')
user_count: int = Field(default=0, description='已分配用户数')
users: list[MasterUserBriefModel] = Field(default_factory=list, description='已分配用户(前5位)')
class PalletListItemModel(PalletModel):
"""托盘列表行:资产主数据加当前流转状态与位置。"""
flow_state: str | None = Field(default=None, description='当前流转状态')
remaining_packages: int = Field(default=0, description='当前剩余包数')
warehouse_name: str | None = Field(default=None, description='所在仓库')
customer_name: str | None = Field(default=None, description='所在或目标客户')
site_name: str | None = Field(default=None, description='所在或目标现场')
line_name: str | None = Field(default=None, description='当前产线')
tunnel_name: str | None = Field(default=None, description='当前隧道')
return_warehouse_name: str | None = Field(default=None, description='退回目标仓库')
strain_name: str | None = Field(default=None, description='当前菌种')
batch_no: str | None = Field(default=None, description='当前生产批次')
expiry_date: date | None = Field(default=None, description='当前物料有效期')
cycle_count: int = Field(default=0, description='累计发货周转次数')
last_shipped_at: datetime | None = Field(default=None, description='最近一次发货时间')
last_event_at: datetime | None = Field(default=None, description='最近一次状态变化时间')
class PalletEventBriefModel(_ReadModel):
event_id: int = Field(description='事件ID')
event_type: str = Field(description='事件类型')
occurred_at: datetime = Field(description='发生时间')
after_state: str = Field(description='变更后状态')
before_remaining_packages: int = Field(description='变更前剩余包数')
after_remaining_packages: int = Field(description='变更后剩余包数')
trace_no: str | None = Field(default=None, description='物料溯源号')
warehouse_name: str | None = Field(default=None, description='仓库')
customer_name: str | None = Field(default=None, description='客户')
site_name: str | None = Field(default=None, description='现场')
line_name: str | None = Field(default=None, description='产线')
tunnel_name: str | None = Field(default=None, description='隧道')
operator_name: str | None = Field(default=None, description='操作人')
class PalletDetailModel(PalletListItemModel):
"""托盘详情:列表行信息加最近流转事件。"""
recent_events: list[PalletEventBriefModel] = Field(default_factory=list, description='最近流转事件')
class MasterImportPreviewRowModel(_ReadModel):
excel_row: int = Field(description='Excel 行号')
outcome: Literal['add', 'update', 'skip', 'error'] = Field(description='导入时将发生的结果')
message: str = Field(description='校验说明')
values: list[str] = Field(description='按表头顺序的单元格文本')
class MasterImportPreviewModel(_ReadModel):
"""导入校验预览:按真实导入流程逐行校验后整体回滚,不写入任何数据。"""
headers: list[str] = Field(description='模板表头')
update_support: bool = Field(description='编码已存在时是否更新')
total_rows: int = Field(description='数据行数')
add_count: int = Field(description='将新增行数')
update_count: int = Field(description='将更新行数')
skip_count: int = Field(description='将跳过行数')
error_count: int = Field(description='有错误行数')
rows: list[MasterImportPreviewRowModel] = Field(description='逐行结果')
class MasterParentOptionModel(_ReadModel):
"""基础资料表单中可选的上级资料(客户或客户现场)。"""
id: int = Field(description='上级资料ID')
code: str = Field(description='上级资料编码')
name: str = Field(description='上级资料名称')
parent_name: str | None = Field(default=None, description='所属客户名称(现场选项)')