from datetime import date, datetime from pydantic import BaseModel, ConfigDict, Field, computed_field, model_validator from pydantic.alias_generators import to_camel from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState, ReturnStatus def _parse_report_query_date(value: str) -> datetime: try: parsed = datetime.strptime(value, '%Y-%m-%d') except ValueError as exc: raise ValueError('日期必须为有效的 YYYY-MM-DD 格式') from exc if parsed.strftime('%Y-%m-%d') != value: raise ValueError('日期必须为有效的 YYYY-MM-DD 格式') return parsed def _validate_report_query_date_range(value: BaseModel) -> BaseModel: begin_value = value.begin_time end_value = value.end_time begin_date = _parse_report_query_date(begin_value) if begin_value else None end_date = _parse_report_query_date(end_value) if end_value else None if end_date and end_date.date() == datetime.max.date(): raise ValueError('结束日期必须早于 9999-12-31') if begin_date and end_date and begin_date > end_date: raise ValueError('开始日期不能晚于结束日期') return value class TunnelReportQueryModel(BaseModel): """隧道报表共享筛选;编号/名称可直接输入,溯源号按完整值匹配。""" model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True) tunnel_id: int | None = Field(default=None, gt=0, description='隧道ID') tunnel_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='隧道编号') tunnel_name: str | None = Field(default=None, min_length=1, max_length=100, pattern=r'\S', description='隧道名称') trace_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='内部溯源号(精确匹配)') class PalletInventoryReportQueryModel(TunnelReportQueryModel): """托盘库存报表查询模型。""" model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True) page_num: int = Field(default=1, gt=0, description='当前页码') page_size: int = Field(default=10, gt=0, le=100, description='每页记录数') begin_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='开始时间') end_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='结束时间') pallet_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='托盘编号') flow_state: PalletFlowState | None = Field(default=None, description='托盘流转状态') warehouse_id: int | None = Field(default=None, gt=0, description='仓库ID') site_id: int | None = Field(default=None, gt=0, description='现场ID') line_id: int | None = Field(default=None, gt=0, description='产线ID') strain_id: int | None = Field(default=None, gt=0, description='菌种ID') batch_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='生产批次') _validate_date_range = model_validator(mode='after')(_validate_report_query_date_range) class PalletInventoryReportModel(BaseModel): """托盘库存报表行模型。""" model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True) pallet_id: int = Field(gt=0, description='托盘ID') pallet_code: str = Field(min_length=1, max_length=64, pattern=r'\S', description='托盘编号') flow_state: PalletFlowState = Field(description='托盘流转状态') warehouse_id: int | None = Field(default=None, gt=0, description='当前仓库ID') warehouse_name: str | None = Field(default=None, max_length=100, description='当前仓库名称') site_id: int | None = Field(default=None, gt=0, description='当前现场ID') site_name: str | None = Field(default=None, max_length=100, description='当前现场名称') line_id: int | None = Field(default=None, gt=0, description='当前产线ID') line_name: str | None = Field(default=None, max_length=100, description='当前产线名称') strain_id: int | None = Field(default=None, gt=0, description='当前菌种ID') strain_code: str | None = Field(default=None, max_length=64, description='当前菌种编码快照') strain_name: str | None = Field(default=None, max_length=100, description='当前菌种名称快照') batch_no: str | None = Field(default=None, max_length=64, description='当前生产批次') initial_packages: int | None = Field(default=None, strict=True, gt=0, description='初始包数') remaining_packages: int = Field(strict=True, ge=0, description='当前剩余包数') updated_at: datetime = Field(description='库存更新时间') tunnel_id: int | None = Field(default=None, gt=0, description='当前隧道ID') tunnel_code: str | None = Field(default=None, max_length=64, description='当前隧道编号') tunnel_name: str | None = Field(default=None, max_length=100, description='当前隧道名称') trace_no: str | None = Field(default=None, max_length=64, description='当前内部溯源号') production_date: date | None = Field(default=None, description='当前物料生产日期,历史未登记为空') expiry_date: date | None = Field(default=None, description='当前物料有效期至,历史未登记为空') class FillingReportQueryModel(BaseModel): """装填记录报表查询模型。""" model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True) page_num: int = Field(default=1, gt=0, description='当前页码') page_size: int = Field(default=10, gt=0, le=100, description='每页记录数') begin_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='开始时间') end_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='结束时间') warehouse_id: int | None = Field(default=None, gt=0, description='仓库ID') pallet_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='托盘编号') strain_id: int | None = Field(default=None, gt=0, description='菌种ID') batch_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='生产批次') _validate_date_range = model_validator(mode='after')(_validate_report_query_date_range) class FillingReportModel(BaseModel): """装填记录报表行模型。""" model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True) filling_id: int = Field(gt=0, description='装填记录ID') filling_no: str = Field(min_length=1, max_length=64, pattern=r'\S', description='装填单号') operated_at: datetime = Field(description='装填时间') warehouse_id: int = Field(gt=0, description='仓库ID') warehouse_name: str | None = Field(default=None, max_length=100, description='仓库名称') pallet_id: int = Field(gt=0, description='托盘ID') pallet_code: str = Field(min_length=1, max_length=64, pattern=r'\S', description='托盘编号') strain_id: int = Field(gt=0, description='菌种ID') strain_code_snapshot: str = Field(min_length=1, max_length=64, pattern=r'\S', description='菌种编码快照') strain_name_snapshot: str = Field(min_length=1, max_length=100, pattern=r'\S', description='菌种名称快照') batch_no: str = Field(min_length=1, max_length=64, pattern=r'\S', description='生产批次') package_spec: str = Field(min_length=1, max_length=64, pattern=r'\S', description='包装规格') initial_packages: int = Field(strict=True, gt=0, description='初始包数') operator_id: int = Field(gt=0, description='操作人ID') operator_name: str | None = Field(default=None, max_length=100, description='操作人名称') class ShipmentReportQueryModel(BaseModel): """发货出库报表查询模型。""" model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True) page_num: int = Field(default=1, gt=0, description='当前页码') page_size: int = Field(default=10, gt=0, le=100, description='每页记录数') begin_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='开始时间') end_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='结束时间') shipment_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='发货单号') source_warehouse_id: int | None = Field(default=None, gt=0, description='来源仓库ID') target_site_id: int | None = Field(default=None, gt=0, description='目标现场ID') pallet_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='托盘编号') status: DocumentStatus | None = Field(default=None, description='单据状态') _validate_date_range = model_validator(mode='after')(_validate_report_query_date_range) class ShipmentReportModel(BaseModel): """发货出库报表行模型。""" model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True) shipment_id: int = Field(gt=0, description='发货单ID') shipment_no: str = Field(min_length=1, max_length=64, pattern=r'\S', description='发货单号') confirmed_at: datetime = Field(description='确认发货时间') source_warehouse_id: int = Field(gt=0, description='来源仓库ID') source_warehouse_name: str | None = Field(default=None, max_length=100, description='来源仓库名称') target_site_id: int = Field(gt=0, description='目标现场ID') target_site_name: str | None = Field(default=None, max_length=100, description='目标现场名称') customer_id: int = Field(gt=0, description='客户ID') customer_name: str | None = Field(default=None, max_length=100, description='客户名称') pallet_id: int = Field(gt=0, description='托盘ID') pallet_code: str = Field(min_length=1, max_length=64, pattern=r'\S', description='托盘编号') active_filling_id: int | None = Field(default=None, gt=0, description='发货时活动装填记录ID') package_qty: int = Field(strict=True, gt=0, description='发货时包数快照') class ConsumptionReportQueryModel(TunnelReportQueryModel): """隧道消耗报表查询模型,兼容历史产线筛选。""" model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True) page_num: int = Field(default=1, gt=0, description='当前页码') page_size: int = Field(default=10, gt=0, le=100, description='每页记录数') begin_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='开始时间') end_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='结束时间') customer_id: int | None = Field(default=None, gt=0, description='客户ID') site_id: int | None = Field(default=None, gt=0, description='现场ID') line_id: int | None = Field(default=None, gt=0, description='产线ID') pallet_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='托盘编号') strain_id: int | None = Field(default=None, gt=0, description='菌种ID') batch_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='生产批次') _validate_date_range = model_validator(mode='after')(_validate_report_query_date_range) class ConsumptionReportModel(BaseModel): """隧道消耗报表行模型。""" model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True) usage_id: int = Field(gt=0, description='使用记录ID') usage_no: str = Field(min_length=1, max_length=64, pattern=r'\S', description='使用记录号') occurred_at: datetime = Field(description='使用发生时间') customer_id: int = Field(gt=0, description='客户ID') customer_name: str | None = Field(default=None, max_length=100, description='客户名称') site_id: int = Field(gt=0, description='现场ID') site_name: str | None = Field(default=None, max_length=100, description='现场名称') line_id: int | None = Field(default=None, gt=0, description='产线ID(可选)') line_name: str | None = Field(default=None, max_length=100, description='产线名称') pallet_id: int = Field(gt=0, description='托盘ID') pallet_code: str = Field(min_length=1, max_length=64, pattern=r'\S', description='托盘编号') strain_id: int = Field(gt=0, description='菌种ID') strain_code: str | None = Field(default=None, max_length=64, description='菌种编码快照') strain_name: str | None = Field(default=None, max_length=100, description='菌种名称快照') batch_no: str = Field(min_length=1, max_length=64, pattern=r'\S', description='生产批次') used_packages: int = Field(strict=True, gt=0, description='本次使用包数') remaining_packages_after: int = Field(strict=True, ge=0, description='使用后剩余包数') operator_id: int = Field(gt=0, description='操作人ID') operator_name: str | None = Field(default=None, max_length=100, description='操作人名称') tunnel_id: int | None = Field(default=None, gt=0, description='使用时隧道ID') tunnel_code: str | None = Field(default=None, max_length=64, description='使用时隧道编号') tunnel_name: str | None = Field(default=None, max_length=100, description='使用时隧道名称') trace_no: str | None = Field(default=None, max_length=64, description='使用时内部溯源号') batch_no_snapshot: str | None = Field(default=None, max_length=64, description='使用时生产批次快照') production_date: date | None = Field(default=None, description='使用时原装填物料生产日期') expiry_date: date | None = Field(default=None, description='使用时原装填物料有效期至') class ReturnReportQueryModel(TunnelReportQueryModel): """退回仓库报表查询模型。""" model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True) page_num: int = Field(default=1, gt=0, description='当前页码') page_size: int = Field(default=10, gt=0, le=100, description='每页记录数') begin_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='开始时间') end_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='结束时间') return_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='退回单号') source_site_id: int | None = Field(default=None, gt=0, description='来源现场ID') target_warehouse_id: int | None = Field(default=None, gt=0, description='目标仓库ID') pallet_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='托盘编号') status: ReturnStatus | None = Field(default=None, description='退回状态') batch_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='退回生产批次') _validate_date_range = model_validator(mode='after')(_validate_report_query_date_range) class ReturnReportModel(BaseModel): """退回仓库报表行模型。""" model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True) return_id: int = Field(gt=0, description='退回单ID') return_no: str = Field(min_length=1, max_length=64, pattern=r'\S', description='退回单号') status: ReturnStatus = Field(description='退回状态') requested_at: datetime = Field(description='申请时间') confirmed_at: datetime | None = Field(default=None, description='确认时间') source_site_id: int = Field(gt=0, description='来源现场ID') source_site_name: str | None = Field(default=None, max_length=100, description='来源现场名称') target_warehouse_id: int = Field(gt=0, description='目标仓库ID') target_warehouse_name: str | None = Field(default=None, max_length=100, description='目标仓库名称') pallet_id: int = Field(gt=0, description='托盘ID') pallet_code: str = Field(min_length=1, max_length=64, pattern=r'\S', description='托盘编号') active_filling_id_snapshot: int | None = Field(default=None, gt=0, description='活动装填记录快照') remaining_packages_snapshot: int = Field(strict=True, ge=0, description='剩余包数快照') tunnel_id: int | None = Field(default=None, gt=0, description='退回时隧道ID快照') tunnel_code: str | None = Field(default=None, max_length=64, description='退回隧道编号') tunnel_name: str | None = Field(default=None, max_length=100, description='退回隧道名称') trace_no: str | None = Field(default=None, max_length=64, description='退回时内部溯源号快照') batch_no_snapshot: str | None = Field(default=None, max_length=64, description='退回时生产批次快照') batch_no: str | None = Field(default=None, max_length=64, description='生产批次,历史记录回查原装填记录') production_date: date | None = Field(default=None, description='退回时原装填物料生产日期') expiry_date: date | None = Field(default=None, description='退回时原装填物料有效期至') class PalletTraceReportQueryModel(TunnelReportQueryModel): """托盘追溯报表查询模型。""" model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True) page_num: int = Field(default=1, gt=0, description='当前页码') page_size: int = Field(default=10, gt=0, le=100, description='每页记录数') begin_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='开始时间') end_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='结束时间') pallet_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='托盘编号') event_type: PalletEventType | None = Field(default=None, description='事件类型') batch_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='事件生产批次快照') _validate_date_range = model_validator(mode='after')(_validate_report_query_date_range) class PalletTraceReportModel(BaseModel): """托盘追溯报表行模型。""" model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True) event_id: int = Field(gt=0, description='事件ID') event_seq: int = Field(gt=0, description='托盘内事件序号') event_type: PalletEventType = Field(description='事件类型') source_type: str = Field(min_length=1, max_length=32, pattern=r'\S', description='来源类型') source_id: int = Field(gt=0, description='来源记录ID') pallet_id: int = Field(gt=0, description='托盘ID') pallet_code: str = Field(min_length=1, max_length=64, pattern=r'\S', description='托盘编号') before_state: PalletFlowState = Field(description='变更前状态') after_state: PalletFlowState = Field(description='变更后状态') before_warehouse_id: int | None = Field(default=None, gt=0, description='变更前仓库ID') before_warehouse_name: str | None = Field(default=None, max_length=100, description='变更前仓库名称') before_site_id: int | None = Field(default=None, gt=0, description='变更前现场ID') before_site_name: str | None = Field(default=None, max_length=100, description='变更前现场名称') before_line_id: int | None = Field(default=None, gt=0, description='变更前产线ID') before_line_name: str | None = Field(default=None, max_length=100, description='变更前产线名称') after_warehouse_id: int | None = Field(default=None, gt=0, description='变更后仓库ID') after_warehouse_name: str | None = Field(default=None, max_length=100, description='变更后仓库名称') after_site_id: int | None = Field(default=None, gt=0, description='变更后现场ID') after_site_name: str | None = Field(default=None, max_length=100, description='变更后现场名称') after_line_id: int | None = Field(default=None, gt=0, description='变更后产线ID') after_line_name: str | None = Field(default=None, max_length=100, description='变更后产线名称') before_remaining_packages: int = Field(strict=True, ge=0, description='变更前剩余包数') after_remaining_packages: int = Field(strict=True, ge=0, description='变更后剩余包数') operator_id: int = Field(gt=0, description='操作人ID') operator_name: str | None = Field(default=None, max_length=100, description='操作人名称') occurred_at: datetime = Field(description='发生时间') tunnel_id: int | None = Field(default=None, gt=0, description='事件隧道ID,优先变更后隧道') tunnel_code: str | None = Field(default=None, max_length=64, description='事件隧道编号') tunnel_name: str | None = Field(default=None, max_length=100, description='事件隧道名称') trace_no: str | None = Field(default=None, max_length=64, description='事件内部溯源号') batch_no_snapshot: str | None = Field(default=None, max_length=64, description='事件生产批次快照') before_tunnel_id: int | None = Field(default=None, gt=0, description='变更前隧道ID') before_tunnel_code: str | None = Field(default=None, max_length=64, description='变更前隧道编号') before_tunnel_name: str | None = Field(default=None, max_length=100, description='变更前隧道名称') after_tunnel_id: int | None = Field(default=None, gt=0, description='变更后隧道ID') after_tunnel_code: str | None = Field(default=None, max_length=64, description='变更后隧道编号') after_tunnel_name: str | None = Field(default=None, max_length=100, description='变更后隧道名称') production_date: date | None = Field(default=None, description='事件原装填物料生产日期') expiry_date: date | None = Field(default=None, description='事件原装填物料有效期至') @computed_field(description='本事件实际使用包数,仅使用事件计入') @property def used_packages(self) -> int: return ( self.before_remaining_packages - self.after_remaining_packages if self.event_type == PalletEventType.USAGE_RECORDED else 0 ) @computed_field(description='本事件确认退回包数,退回申请不重复计入') @property def returned_packages(self) -> int: return self.before_remaining_packages if self.event_type == PalletEventType.RETURN_CONFIRMED else 0