为 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>
218 lines
11 KiB
Python
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='所属客户名称(现场选项)')
|