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='所属客户名称(现场选项)')