"""运输计划管理 - 计划 Schema""" from datetime import date, datetime from decimal import Decimal from typing import Optional from pydantic import BaseModel, Field from app.schemas.pl.datetime_parse import OptionalPlanDateTime from app.schemas.pl.resource_snapshot import PlanResourceSnapshotMixin # ============================================================================= # 计划产品明细 Schema # ============================================================================= class PlanProductBase(BaseModel): product_id: str = Field(..., alias="productId", description="产品ID") product_no: str = Field(..., max_length=50, alias="productNo", description="产品编号") product_name: str = Field(..., max_length=200, alias="productName", description="产品名称") brand_mode: Optional[str] = Field(default=None, alias="brandMode", description="required/any") brand_id: Optional[str] = Field(default=None, alias="brandId", description="指定品牌ID") brand_name: Optional[str] = Field(default=None, alias="brandName", description="品牌名快照") model: Optional[str] = Field(default=None, max_length=100, description="规格型号") unit: str = Field(default="吨", max_length=20, description="单位") category_name: Optional[str] = Field(default=None, max_length=200, alias="categoryName", description="产品分类") package_spec: Optional[str] = Field(default=None, max_length=200, alias="packageSpec", description="包装规格") delivery_method: Optional[str] = Field(default=None, max_length=50, alias="deliveryMethod", description="送货方式") quantity: Optional[Decimal] = Field(default=None, description="合同数量") actual_qty: Optional[Decimal] = Field(default=None, alias="actualQty", description="实际数量") tax_price: Optional[Decimal] = Field(default=None, alias="taxPrice", description="含税单价") tax_amount: Optional[Decimal] = Field(default=None, alias="taxAmount", description="税费") tax_subtotal: Optional[Decimal] = Field(default=None, alias="taxSubtotal", description="含税总价") is_explosive: int = Field(default=0, alias="isExplosive", description="是否易制爆") is_toxic: int = Field(default=0, alias="isToxic", description="是否易制毒") purchase_cert_url: Optional[str] = Field( default=None, alias="purchaseCertUrl", description="购买证URL,逗号分隔" ) transport_cert_url: Optional[str] = Field( default=None, alias="transportCertUrl", description="运输证URL,逗号分隔" ) legal_use_cert_url: Optional[str] = Field( default=None, alias="legalUseCertUrl", description="合法使用证明URL,逗号分隔" ) inbound_estimate_qty: Optional[Decimal] = Field(default=None, alias="inboundEstimateQty", description="入库预估数") inbound_qty: Optional[Decimal] = Field(default=None, alias="inboundQty", description="入库数") inbound_price: Optional[Decimal] = Field(default=None, alias="inboundPrice", description="入库价") inbound_amount: Optional[Decimal] = Field(default=None, alias="inboundAmount", description="入库金额") outbound_estimate_qty: Optional[Decimal] = Field(default=None, alias="outboundEstimateQty", description="出库预估数") outbound_qty: Optional[Decimal] = Field(default=None, alias="outboundQty", description="出库数") outbound_price: Optional[Decimal] = Field(default=None, alias="outboundPrice", description="出库单价") outbound_amount: Optional[Decimal] = Field(default=None, alias="outboundAmount", description="出库金额") remark: Optional[str] = Field(default=None, max_length=512, description="备注") class PlanProductCreate(PlanProductBase): model_config = {"populate_by_name": True} class PlanProductUpdate(BaseModel): product_id: Optional[str] = Field(default=None, alias="productId") product_no: Optional[str] = Field(default=None, max_length=50, alias="productNo") product_name: Optional[str] = Field(default=None, max_length=200, alias="productName") model: Optional[str] = Field(default=None, max_length=100) unit: Optional[str] = Field(default=None, max_length=20) is_explosive: Optional[int] = Field(default=None, alias="isExplosive") is_toxic: Optional[int] = Field(default=None, alias="isToxic") purchase_cert_url: Optional[str] = Field(default=None, alias="purchaseCertUrl") transport_cert_url: Optional[str] = Field(default=None, alias="transportCertUrl") legal_use_cert_url: Optional[str] = Field(default=None, alias="legalUseCertUrl") inbound_estimate_qty: Optional[Decimal] = Field(default=None, alias="inboundEstimateQty") inbound_qty: Optional[Decimal] = Field(default=None, alias="inboundQty") inbound_price: Optional[Decimal] = Field(default=None, alias="inboundPrice") inbound_amount: Optional[Decimal] = Field(default=None, alias="inboundAmount") outbound_estimate_qty: Optional[Decimal] = Field(default=None, alias="outboundEstimateQty") outbound_qty: Optional[Decimal] = Field(default=None, alias="outboundQty") outbound_price: Optional[Decimal] = Field(default=None, alias="outboundPrice") outbound_amount: Optional[Decimal] = Field(default=None, alias="outboundAmount") model_config = {"populate_by_name": True} class PlanArchiveBrandQtyItem(BaseModel): brand_id: str = Field(default="", alias="brandId") brand_name: Optional[str] = Field(default=None, alias="brandName") qty: Decimal = Field(..., ge=0) unit_price: Optional[Decimal] = Field(default=None, alias="unitPrice", ge=0, description="本品牌单价") model_config = {"populate_by_name": True} class PlanProductRead(PlanProductBase): id: str plan_id: str = Field(alias="planId") tenant_no: str = Field(alias="tenantNo") create_time: datetime = Field(alias="createTime") update_time: datetime = Field(alias="updateTime") brand_qtys: Optional[list[PlanArchiveBrandQtyItem]] = Field( default=None, alias="brandQtys", description="归档品牌数量明细" ) model_config = {"from_attributes": True, "populate_by_name": True} # ============================================================================= # 装货/送货段 Schema # ============================================================================= class PlanStopProductBody(BaseModel): plan_product_id: Optional[str] = Field( default=None, alias="planProductId", description="计划产品行ID(新建时可空,由后端按 productId 匹配)", ) product_id: str = Field(..., alias="productId", description="产品ID") product_no: str = Field(..., max_length=50, alias="productNo", description="产品编号") product_name: str = Field(..., max_length=200, alias="productName", description="产品名称") model: Optional[str] = Field(default=None, max_length=100, description="规格型号") unit: str = Field(default="吨", max_length=20, description="单位") package_spec: Optional[str] = Field(default=None, max_length=200, alias="packageSpec", description="包装规格") delivery_method: Optional[str] = Field(default=None, max_length=50, alias="deliveryMethod", description="送货方式") plan_qty: Optional[Decimal] = Field(default=None, alias="planQty", description="本段计划数量") remark: Optional[str] = Field(default=None, max_length=512, description="备注") model_config = {"populate_by_name": True} class PlanStopProductRead(PlanStopProductBody): id: str plan_id: str = Field(alias="planId") inbound_qty: Optional[Decimal] = Field(default=None, alias="inboundQty", description="入库数量(装货段归档)") inbound_price: Optional[Decimal] = Field(default=None, alias="inboundPrice", description="入库单价(装货段归档)") inbound_amount: Optional[Decimal] = Field(default=None, alias="inboundAmount", description="入库金额(装货段归档)") outbound_qty: Optional[Decimal] = Field(default=None, alias="outboundQty", description="出库数量(送货段归档)") outbound_price: Optional[Decimal] = Field(default=None, alias="outboundPrice", description="出库单价(送货段归档)") outbound_amount: Optional[Decimal] = Field(default=None, alias="outboundAmount", description="出库金额(送货段归档)") brand_qtys: Optional[list[PlanArchiveBrandQtyItem]] = Field( default=None, alias="brandQtys", description="本段归档品牌数量" ) tenant_no: str = Field(alias="tenantNo") order_num: Optional[int] = Field(default=None, alias="orderNum") create_time: datetime = Field(alias="createTime") update_time: datetime = Field(alias="updateTime") model_config = {"from_attributes": True, "populate_by_name": True} class PlanLoadBody(BaseModel): customer_id: Optional[str] = Field(default=None, alias="customerId", description="装货客户ID") customer_name: Optional[str] = Field(default=None, max_length=200, alias="customerName", description="装货客户名称") address: Optional[str] = Field(default=None, max_length=500, description="装货地址") lng: Optional[Decimal] = Field(default=None, description="经度") lat: Optional[Decimal] = Field(default=None, description="纬度") load_time: OptionalPlanDateTime = Field(default=None, alias="loadTime", description="装货时间") contact: Optional[str] = Field(default=None, max_length=200, description="联系方式") notes: Optional[str] = Field(default=None, max_length=500, description="注意事项") generate_dibang: int = Field(default=0, alias="generateDibang", description="是否生成地磅单 0-否 1-是") dibang_direction: Optional[str] = Field( default=None, alias="dibangDirection", description="地磅方向 outbound-出库 inbound-入库 none-无" ) products: list[PlanStopProductBody] = Field(default_factory=list, description="装货段产品明细") contracts: Optional[list["PlanContractLink"]] = Field(default=None, description="装货段合同列表") remark: Optional[str] = Field(default=None, max_length=512, description="备注") model_config = {"populate_by_name": True} class PlanDeliveryBody(BaseModel): seq: int = Field(default=1, description="顺序号(打印、链接展示)") customer_id: Optional[str] = Field(default=None, alias="customerId", description="送货客户ID") customer_name: Optional[str] = Field(default=None, max_length=200, alias="customerName", description="送货客户名称") address: Optional[str] = Field(default=None, max_length=500, description="送货地址") lng: Optional[Decimal] = Field(default=None, description="经度") lat: Optional[Decimal] = Field(default=None, description="纬度") delivery_time: OptionalPlanDateTime = Field(default=None, alias="deliveryTime", description="送货时间") contact: Optional[str] = Field(default=None, max_length=200, description="联系方式") notes: Optional[str] = Field(default=None, max_length=500, description="注意事项") generate_dibang: int = Field(default=0, alias="generateDibang", description="是否生成地磅单 0-否 1-是") dibang_direction: Optional[str] = Field( default=None, alias="dibangDirection", description="地磅方向 outbound-出库 inbound-入库 none-无" ) products: list[PlanStopProductBody] = Field(default_factory=list, description="送货段产品明细") contracts: Optional[list["PlanContractLink"]] = Field(default=None, description="送货段合同列表") remark: Optional[str] = Field(default=None, max_length=512, description="备注") model_config = {"populate_by_name": True} class PlanLoadRead(PlanLoadBody): id: str plan_id: str = Field(alias="planId") products: list[PlanStopProductRead] = Field(default_factory=list, description="装货段产品明细") tenant_no: str = Field(alias="tenantNo") order_num: Optional[int] = Field(default=None, alias="orderNum") create_time: datetime = Field(alias="createTime") update_time: datetime = Field(alias="updateTime") model_config = {"from_attributes": True, "populate_by_name": True} class PlanDeliveryRead(PlanDeliveryBody): id: str plan_id: str = Field(alias="planId") products: list[PlanStopProductRead] = Field(default_factory=list, description="送货段产品明细") tenant_no: str = Field(alias="tenantNo") order_num: Optional[int] = Field(default=None, alias="orderNum") create_time: datetime = Field(alias="createTime") update_time: datetime = Field(alias="updateTime") model_config = {"from_attributes": True, "populate_by_name": True} # ============================================================================= # 计划主表 Schema # ============================================================================= class PlanBase(BaseModel): belongs_company: str = Field(..., alias="belongsCompany", description="归属公司 shanxia-安庆山峡 yundong-安徽云栋") plan_type: str = Field( default="", alias="planType", description="单据类型 direct-直销单 inbound-入库单 outbound-出库单;未选择可为空", ) follow_status: str = Field( default="pending_arrange", alias="followStatus", description="跟进状态 wait_notice/pending_arrange/arranged/completed/cancelled", ) transport_status: str = Field(default="pending", alias="transportStatus", description="运输安排状态") archive_status: str = Field(default="unarchived", alias="archiveStatus", description="归档状态 unarchived/draft/archived") complete_status: str = Field(default="no", alias="completeStatus", description="完成状态") contract_id: Optional[str] = Field(default=None, alias="contractId", description="CRM合同ID") contract_no: Optional[str] = Field(default=None, max_length=50, alias="contractNo", description="合同编号") supplier_id: Optional[str] = Field(default=None, alias="supplierId", description="供应商ID") supplier_name: Optional[str] = Field(default=None, max_length=200, alias="supplierName", description="供应商名称") destination_id: Optional[str] = Field(default=None, alias="destinationId", description="目的地ID") destination_name: Optional[str] = Field(default=None, max_length=200, alias="destinationName", description="目的地名称") customer_id: Optional[str] = Field(default=None, alias="customerId", description="客户ID") customer_name: Optional[str] = Field(default=None, max_length=200, alias="customerName", description="客户名称") contact_name: Optional[str] = Field(default=None, max_length=100, alias="contactName", description="联系人") contact_phone: Optional[str] = Field(default=None, max_length=32, alias="contactPhone", description="联系电话") party_a: Optional[str] = Field(default=None, max_length=200, alias="partyA", description="甲方") party_b: Optional[str] = Field(default=None, max_length=200, alias="partyB", description="乙方") contract_start_date: Optional[date] = Field(default=None, alias="contractStartDate", description="合同开始日期") contract_end_date: Optional[date] = Field(default=None, alias="contractEndDate", description="合同结束日期") delivery_method: Optional[str] = Field(default=None, alias="deliveryMethod", description="送货方式") settlement_method: Optional[str] = Field(default=None, max_length=50, alias="settlementMethod", description="结算方式") has_freight: int = Field(default=0, alias="hasFreight", description="是否有运输费用") freight_remark: Optional[str] = Field(default=None, max_length=512, alias="freightRemark", description="运费备注") pickup_address: Optional[str] = Field(default=None, max_length=500, alias="pickupAddress", description="取货地址") pickup_lng: Optional[Decimal] = Field(default=None, alias="pickupLng", description="取货经度") pickup_lat: Optional[Decimal] = Field(default=None, alias="pickupLat", description="取货纬度") pickup_time: OptionalPlanDateTime = Field(default=None, alias="pickupTime", description="取货时间") pickup_contact: Optional[str] = Field(default=None, max_length=200, alias="pickupContact", description="取货联系方式") pickup_notes: Optional[str] = Field(default=None, max_length=500, alias="pickupNotes", description="取货注意事项") destination_address: Optional[str] = Field(default=None, max_length=500, alias="destinationAddress", description="目的地地址") destination_lng: Optional[Decimal] = Field(default=None, alias="destinationLng", description="送货经度") destination_lat: Optional[Decimal] = Field(default=None, alias="destinationLat", description="送货纬度") delivery_time: OptionalPlanDateTime = Field(default=None, alias="deliveryTime", description="送达时间") delivery_contact: Optional[str] = Field(default=None, max_length=200, alias="deliveryContact", description="送货联系方式") destination_notes: Optional[str] = Field(default=None, max_length=500, alias="destinationNotes", description="目的地注意事项") notify_message: Optional[str] = Field(default=None, alias="notifyMessage", description="通知信息") follow_user_id: Optional[str] = Field(default=None, alias="followUserId", description="跟进人ID") follow_user_name: Optional[str] = Field(default=None, max_length=100, alias="followUserName", description="跟进人姓名") remark: Optional[str] = None class PlanContractLink(BaseModel): contract_id: str = Field(..., alias="contractId", description="CRM合同ID") contract_no: Optional[str] = Field(default=None, max_length=50, alias="contractNo", description="合同编号") stop_id: Optional[str] = Field(default=None, alias="stopId", description="停靠段ID(装货段或送货段)") model_config = {"populate_by_name": True} class PlanCreate(PlanBase): create_date: Optional[date] = Field( default=None, alias="createDate", description="创建日期;缺省则服务端用当天" ) plan_date: Optional[date] = Field(default=None, alias="planDate", description="计划日期(非必填)") products: Optional[list[PlanProductCreate]] = Field(default=None, description="产品明细列表") supplier_contracts: Optional[list[PlanContractLink]] = Field( default=None, alias="supplierContracts", description="供方合同列表" ) demander_contracts: Optional[list[PlanContractLink]] = Field( default=None, alias="demanderContracts", description="需方合同列表" ) load: Optional[PlanLoadBody] = Field(default=None, description="装货段") deliveries: Optional[list[PlanDeliveryBody]] = Field(default=None, description="送货段列表") model_config = {"populate_by_name": True} class PlanTransportCreateBlock(BaseModel): vehicle_id: str = Field(..., alias="vehicleId", description="车辆ID") driver_id: Optional[str] = Field(default=None, alias="driverId", description="司机/人员ID") carrier_id: Optional[str] = Field(default=None, alias="carrierId", description="承运商ID") distance_km: Optional[Decimal] = Field(default=None, alias="distanceKm", description="运输公里数") freight_type: str = Field(default="by_weight", alias="freightType", description="运费计算方式") transport_qty: Optional[Decimal] = Field(default=None, alias="transportQty", description="运输量") freight_price: Optional[Decimal] = Field(default=None, alias="freightPrice", description="运输单价") freight_amount: Optional[Decimal] = Field(default=None, alias="freightAmount", description="运费金额") has_invoice: Optional[int] = Field(default=None, alias="hasInvoice", description="是否开票(已废弃,由运费结算维护)") remark: Optional[str] = None model_config = {"populate_by_name": True} class PlanUpdate(BaseModel): create_date: Optional[date] = Field(default=None, alias="createDate") plan_date: Optional[date] = Field(default=None, alias="planDate") belongs_company: Optional[str] = Field(default=None, alias="belongsCompany") plan_type: Optional[str] = Field(default=None, alias="planType") follow_status: Optional[str] = Field(default=None, alias="followStatus") transport_status: Optional[str] = Field(default=None, alias="transportStatus") supplier_id: Optional[str] = Field(default=None, alias="supplierId") supplier_name: Optional[str] = Field(default=None, max_length=200, alias="supplierName") destination_id: Optional[str] = Field(default=None, alias="destinationId") destination_name: Optional[str] = Field(default=None, max_length=200, alias="destinationName") customer_id: Optional[str] = Field(default=None, alias="customerId") customer_name: Optional[str] = Field(default=None, max_length=200, alias="customerName") contact_name: Optional[str] = Field(default=None, max_length=100, alias="contactName") contact_phone: Optional[str] = Field(default=None, max_length=32, alias="contactPhone") party_a: Optional[str] = Field(default=None, max_length=200, alias="partyA") party_b: Optional[str] = Field(default=None, max_length=200, alias="partyB") contract_start_date: Optional[date] = Field(default=None, alias="contractStartDate") contract_end_date: Optional[date] = Field(default=None, alias="contractEndDate") delivery_method: Optional[str] = Field(default=None, alias="deliveryMethod") settlement_method: Optional[str] = Field(default=None, max_length=50, alias="settlementMethod") has_freight: Optional[int] = Field(default=None, alias="hasFreight") freight_remark: Optional[str] = Field(default=None, max_length=512, alias="freightRemark") pickup_address: Optional[str] = Field(default=None, max_length=500, alias="pickupAddress") pickup_lng: Optional[Decimal] = Field(default=None, alias="pickupLng") pickup_lat: Optional[Decimal] = Field(default=None, alias="pickupLat") pickup_time: OptionalPlanDateTime = Field(default=None, alias="pickupTime") pickup_contact: Optional[str] = Field(default=None, max_length=200, alias="pickupContact") pickup_notes: Optional[str] = Field(default=None, max_length=500, alias="pickupNotes") destination_address: Optional[str] = Field(default=None, max_length=500, alias="destinationAddress") destination_lng: Optional[Decimal] = Field(default=None, alias="destinationLng") destination_lat: Optional[Decimal] = Field(default=None, alias="destinationLat") delivery_time: OptionalPlanDateTime = Field(default=None, alias="deliveryTime") delivery_contact: Optional[str] = Field(default=None, max_length=200, alias="deliveryContact") destination_notes: Optional[str] = Field(default=None, max_length=500, alias="destinationNotes") notify_message: Optional[str] = Field(default=None, alias="notifyMessage") follow_user_id: Optional[str] = Field(default=None, alias="followUserId") follow_user_name: Optional[str] = Field(default=None, alias="followUserName") remark: Optional[str] = None products: Optional[list[PlanProductCreate]] = None supplier_contracts: Optional[list[PlanContractLink]] = Field( default=None, alias="supplierContracts", description="供方合同列表" ) demander_contracts: Optional[list[PlanContractLink]] = Field( default=None, alias="demanderContracts", description="需方合同列表" ) transport: Optional[PlanTransportCreateBlock] = Field(default=None, description="运输预选/更新(有车牌时同步车辆与运费)") load: Optional[PlanLoadBody] = Field(default=None, description="装货段") deliveries: Optional[list[PlanDeliveryBody]] = Field(default=None, description="送货段列表") model_config = {"populate_by_name": True} class HazardCertReadyBody(BaseModel): ready: bool = Field(..., description="true=确认证书齐备 false=取消齐备") model_config = {"populate_by_name": True} class ErpEnteredBody(BaseModel): entered: bool = Field(..., description="true=已录入ERP false=未录入") model_config = {"populate_by_name": True} class PlanDocumentAttachmentItem(BaseModel): url: str = Field(..., description="附件URL") label: str = Field(..., description="来源标签,如司机/现场") model_config = {"populate_by_name": True} class PlanRead(PlanBase, PlanResourceSnapshotMixin): id: str plan_no: str = Field(alias="planNo") create_date: date = Field(alias="createDate") plan_date: Optional[date] = Field(default=None, alias="planDate") hazard_cert_ready: int = Field(default=0, alias="hazardCertReady", description="危化品证书是否已人工确认齐备") hazard_cert_ready_by: Optional[str] = Field(default=None, alias="hazardCertReadyBy", description="危化品证书确认人") hazard_cert_ready_time: Optional[datetime] = Field(default=None, alias="hazardCertReadyTime", description="危化品证书确认时间") erp_entered: int = Field(default=0, alias="erpEntered", description="是否已录入ERP 0-否 1-是") vehicle_id: Optional[str] = Field(default=None, alias="vehicleId") license_plate: Optional[str] = Field(default=None, alias="licensePlate") driver_id: Optional[str] = Field(default=None, alias="driverId") driver_name: Optional[str] = Field(default=None, alias="driverName") personnel_summary: Optional[str] = Field(default=None, alias="personnelSummary") transport_id: Optional[str] = Field(default=None, alias="transportId") transport_no: Optional[str] = Field(default=None, alias="transportNo") total_amount: Decimal = Field(alias="totalAmount") freight_amount: Decimal = Field(alias="freightAmount") tenant_no: str = Field(alias="tenantNo") order_num: Optional[int] = Field(default=None, alias="orderNum") create_by: Optional[str] = Field(default=None, alias="createBy") create_dept_id: Optional[str] = Field(default=None, alias="createDeptId") create_time: datetime = Field(alias="createTime") update_by: Optional[str] = Field(default=None, alias="updateBy") update_time: datetime = Field(alias="updateTime") del_flag: int = Field(default=0, alias="delFlag") supplier_contracts: list[PlanContractLink] = Field( default_factory=list, alias="supplierContracts", description="供方合同列表" ) demander_contracts: list[PlanContractLink] = Field( default_factory=list, alias="demanderContracts", description="需方合同列表" ) model_config = {"from_attributes": True, "populate_by_name": True} class PlanWithProducts(PlanRead): products: list[PlanProductRead] = [] load: Optional[PlanLoadRead] = Field(default=None, description="装货段") deliveries: list[PlanDeliveryRead] = Field(default_factory=list, description="送货段列表") load_customer_name: Optional[str] = Field( default=None, alias="loadCustomerName", description="装货客户名称摘要(列表展示)", ) delivery_customer_summary: Optional[str] = Field( default=None, alias="deliveryCustomerSummary", description="送货客户摘要(列表展示)", ) warnings: list[str] = Field(default_factory=list, description="保存告警(如数量不平衡)") can_copy_to_driver: bool = Field( default=False, alias="canCopyToDriver", description="列表/查看:是否可「复制给司机」(已安排且有司机链接或现场/地磅关联)", ) can_copy_to_site: bool = Field( default=False, alias="canCopyToSite", description="是否可「复制给现场」", ) can_copy_pound_qrcode: bool = Field( default=False, alias="canCopyPoundQrcode", description="是否可「复制磅单二维码」", ) document_attachments: list[PlanDocumentAttachmentItem] = Field( default_factory=list, alias="documentAttachments", description="单据附件(司机/现场/出入库现场等)", ) # ============================================================================= # 送(提)货单打印(按送货客户聚合) # ============================================================================= class DeliveryPrintCustomerOption(BaseModel): customer_id: str = Field(alias="customerId", description="送货客户ID或名称键") customer_name: str = Field(alias="customerName", description="送货客户名称") delivery_count: int = Field(default=1, alias="deliveryCount", description="送货段数量") model_config = {"populate_by_name": True} class DeliverySlipPrintRead(BaseModel): plan_id: str = Field(alias="planId") plan_no: str = Field(alias="planNo") plan_date: Optional[str] = Field(default=None, alias="planDate") customer_id: Optional[str] = Field(default=None, alias="customerId") receiving_unit: Optional[str] = Field(default=None, alias="receivingUnit") receive_contact: Optional[str] = Field(default=None, alias="receiveContact") delivery_address: Optional[str] = Field(default=None, alias="deliveryAddress") products: list[PlanStopProductRead] = Field(default_factory=list) customers: list[DeliveryPrintCustomerOption] = Field(default_factory=list) model_config = {"populate_by_name": True} class PlanInboundSlipTemplateSave(BaseModel): slip_template_content: str = Field(..., alias="slipTemplateContent", description="入库单打印模板HTML") model_config = {"populate_by_name": True} class PlanDeliverySlipTemplateSave(BaseModel): delivery_slip_template_content: str = Field( ..., alias="slipTemplateContent", description="送(提)货单打印模板HTML" ) model_config = {"populate_by_name": True} class PlanSubpackSlipTemplateSave(BaseModel): subpack_slip_template_content: str = Field( ..., alias="slipTemplateContent", description="分装单打印模板HTML" ) model_config = {"populate_by_name": True} # ============================================================================= # 从合同创建计划 # ============================================================================= class CreateFromContractRequest(BaseModel): contract_id: str = Field(..., alias="contractId", description="CRM合同ID") create_date: Optional[date] = Field( default=None, alias="createDate", description="创建日期;缺省当天" ) plan_date: Optional[date] = Field(default=None, alias="planDate", description="计划日期(非必填)") follow_user_id: Optional[str] = Field(default=None, alias="followUserId", description="跟进人ID") follow_user_name: Optional[str] = Field(default=None, max_length=100, alias="followUserName", description="跟进人姓名") model_config = {"populate_by_name": True} class PlanCreateFull(PlanCreate): contract_id: Optional[str] = Field(default=None, alias="contractId", description="旧字段:单一合同ID,已废弃为唯一源") transport: Optional[PlanTransportCreateBlock] = Field(default=None, description="运输安排(有车牌时创建运输)") model_config = {"populate_by_name": True} # ============================================================================= # 计划列表查询 # ============================================================================= class PlanListQuery(BaseModel): q: Optional[str] = Field(default=None, description="模糊搜索计划号/客户名称/产品名称/车牌号") plan_no: Optional[str] = Field(default=None, alias="planNo", description="计划编号") plan_type: Optional[str] = Field(default=None, alias="planType", description="单据类型") follow_status: Optional[str] = Field(default=None, alias="followStatus", description="跟进状态") transport_status: Optional[str] = Field(default=None, alias="transportStatus", description="运输状态") archive_status: Optional[str] = Field(default=None, alias="archiveStatus", description="归档状态") customer_name: Optional[str] = Field(default=None, alias="customerName", description="客户名称") product_name: Optional[str] = Field(default=None, alias="productName", description="产品名称") license_plate: Optional[str] = Field(default=None, alias="licensePlate", description="车牌号") plan_date_from: Optional[date] = Field(default=None, alias="planDateFrom", description="计划日期开始") plan_date_to: Optional[date] = Field(default=None, alias="planDateTo", description="计划日期结束") has_hazard_product: Optional[int] = Field( default=None, alias="hasHazardProduct", description="1=仅含易制毒/易制爆产品的计划" ) cert_upload_status: Optional[str] = Field( default=None, alias="certUploadStatus", description="incomplete=至少一张必需危化证书未上传", ) page: int = Field(default=1, ge=1, description="页码") page_size: int = Field(default=20, ge=1, le=500, alias="pageSize", description="每页数量") model_config = {"populate_by_name": True} class PlanProductCertItem(BaseModel): id: str = Field(..., description="计划产品明细ID") purchase_cert_url: Optional[str] = Field(default=None, alias="purchaseCertUrl", description="购买证URL,逗号分隔") transport_cert_url: Optional[str] = Field(default=None, alias="transportCertUrl", description="运输证URL,逗号分隔") legal_use_cert_url: Optional[str] = Field(default=None, alias="legalUseCertUrl", description="合法使用证明URL,逗号分隔") model_config = {"populate_by_name": True} class PlanProductCertsUpdate(BaseModel): products: list[PlanProductCertItem] = Field(..., description="危化品证书更新列表") model_config = {"populate_by_name": True} class PlanArchiveProductItem(BaseModel): id: str = Field(..., description="计划产品明细ID pl_plan_product.id") actual_qty: Optional[Decimal] = Field(default=None, alias="actualQty", ge=0, description="实际数量") inbound_qty: Optional[Decimal] = Field(default=None, alias="inboundQty", ge=0, description="入库数量") inbound_price: Optional[Decimal] = Field(default=None, alias="inboundPrice", ge=0, description="入库价") outbound_qty: Optional[Decimal] = Field(default=None, alias="outboundQty", ge=0, description="出库数量") outbound_price: Optional[Decimal] = Field(default=None, alias="outboundPrice", ge=0, description="出库价") brand_qtys: Optional[list[PlanArchiveBrandQtyItem]] = Field(default=None, alias="brandQtys") model_config = {"populate_by_name": True} class PlanArchiveStopProductItem(BaseModel): id: str = Field(..., description="段产品行ID(pl_plan_load_product / pl_plan_delivery_product)") inbound_qty: Optional[Decimal] = Field(default=None, alias="inboundQty", ge=0, description="入库数量(装货段)") inbound_price: Optional[Decimal] = Field(default=None, alias="inboundPrice", ge=0, description="入库单价(装货段)") outbound_qty: Optional[Decimal] = Field(default=None, alias="outboundQty", ge=0, description="出库数量(送货段)") outbound_price: Optional[Decimal] = Field(default=None, alias="outboundPrice", ge=0, description="出库单价(送货段)") brand_qtys: Optional[list[PlanArchiveBrandQtyItem]] = Field( default=None, alias="brandQtys", description="本段归档品牌数量(与归档弹窗填写一致)" ) model_config = {"populate_by_name": True} class PlanArchiveLoadBlock(BaseModel): stop_id: str = Field(alias="stopId", description="装货段ID") products: list[PlanArchiveStopProductItem] = Field(default_factory=list) model_config = {"populate_by_name": True} class PlanArchiveDeliveryBlock(BaseModel): stop_id: str = Field(alias="stopId", description="送货段ID") products: list[PlanArchiveStopProductItem] = Field(default_factory=list) model_config = {"populate_by_name": True} class PlanArchiveFreightBlock(BaseModel): has_freight: int = Field(default=1, alias="hasFreight", description="是否有运输费用 0-否 1-是") freight_remark: Optional[str] = Field(default=None, max_length=512, alias="freightRemark", description="运费备注") distance_km: Optional[Decimal] = Field(default=None, alias="distanceKm") freight_type: str = Field(default="by_weight", alias="freightType") freight_price: Optional[Decimal] = Field(default=None, alias="freightPrice") freight_amount: Optional[Decimal] = Field(default=None, alias="freightAmount") transport_qty: Optional[Decimal] = Field(default=None, alias="transportQty") has_invoice: Optional[int] = Field(default=None, alias="hasInvoice") model_config = {"populate_by_name": True} class PlanArchiveStopFeedbackAttachmentItem(BaseModel): stop_type: str = Field(alias="stopType", description="停靠段类型 load/delivery") stop_id: str = Field(alias="stopId", description="停靠段ID") attachment_urls: list[str] = Field(default_factory=list, alias="attachmentUrls", description="附件URLs") model_config = {"populate_by_name": True} class PlanArchiveFeedbacksBlock(BaseModel): driver_stops: list[PlanArchiveStopFeedbackAttachmentItem] = Field( default_factory=list, alias="driverStops", description="司机按段回传附件" ) site_stops: list[PlanArchiveStopFeedbackAttachmentItem] = Field( default_factory=list, alias="siteStops", description="现场按段回传附件" ) model_config = {"populate_by_name": True} class PlanArchiveBody(BaseModel): products: list[PlanArchiveProductItem] = Field(default_factory=list) load: Optional[PlanArchiveLoadBlock] = None deliveries: Optional[list[PlanArchiveDeliveryBlock]] = None freight: Optional[PlanArchiveFreightBlock] = None feedbacks: Optional[PlanArchiveFeedbacksBlock] = None model_config = {"populate_by_name": True} class PlanDriverFeedbackProductItem(BaseModel): plan_product_id: str = Field(alias="planProductId") actual_qty: Decimal = Field(alias="actualQty", ge=0) brand_qtys: Optional[list[PlanArchiveBrandQtyItem]] = Field(default=None, alias="brandQtys") model_config = {"populate_by_name": True} class PlanDriverFeedbackUpdateBody(BaseModel): feedback_remark: Optional[str] = Field(default=None, alias="feedbackRemark", max_length=500, description="回传备注") feedback_images: Optional[list[str]] = Field(default=None, alias="feedbackImages", description="回传附件URLs") feedback_time: OptionalPlanDateTime = Field(default=None, alias="feedbackTime", description="回传时间") products: Optional[list[PlanDriverFeedbackProductItem]] = None feedback_qty: Optional[Decimal] = Field(default=None, alias="feedbackQty", ge=0, description="回传数量") model_config = {"populate_by_name": True} class PlanSiteFeedbackUpdateBody(BaseModel): site_feedback_remark: Optional[str] = Field( default=None, alias="siteFeedbackRemark", max_length=500, description="现场回执备注" ) site_feedback_images: Optional[list[str]] = Field( default=None, alias="siteFeedbackImages", description="现场回执附件URLs" ) site_feedback_time: OptionalPlanDateTime = Field( default=None, alias="siteFeedbackTime", description="现场回执保存时间" ) model_config = {"populate_by_name": True} class PlanListResponse(BaseModel): total: int list: list[PlanWithProducts] class PlanForceFollowStatusBody(BaseModel): """运维纠错:强制改跟进状态(仅改字段,不联动业务)。""" follow_status: str = Field( ..., alias="followStatus", description="跟进状态 wait_notice/pending_arrange/arranged/completed/cancelled", ) model_config = {"populate_by_name": True} class PlanSideContractsUpdateBody(BaseModel): contracts: list[PlanContractLink] = Field(default_factory=list) model_config = {"populate_by_name": True} class PlanStopContractsUpdateBody(BaseModel): stop_type: str = Field(..., alias="stopType", description="load | delivery") stop_id: str = Field(..., alias="stopId", description="装货/送货段ID") contracts: list[PlanContractLink] = Field(default_factory=list) model_config = {"populate_by_name": True} class PlanContractGapRead(BaseModel): id: str plan_no: str = Field(..., alias="planNo") plan_date: Optional[date] = Field(None, alias="planDate") plan_type: Optional[str] = Field(None, alias="planType") follow_status: str = Field(..., alias="followStatus") supplier_id: Optional[str] = Field(None, alias="supplierId") supplier_name: Optional[str] = Field(None, alias="supplierName") customer_id: Optional[str] = Field(None, alias="customerId") customer_name: Optional[str] = Field(None, alias="customerName") gap_sides: list[str] = Field(default_factory=list, alias="gapSides") supplier_contract_exempt: bool = Field(..., alias="supplierContractExempt") demander_contract_exempt: bool = Field(..., alias="demanderContractExempt") supplier_contracts: list[PlanContractLink] = Field(default_factory=list, alias="supplierContracts") demander_contracts: list[PlanContractLink] = Field(default_factory=list, alias="demanderContracts") products: list[PlanProductRead] = Field(default_factory=list, description="产品明细") load: Optional["PlanLoadRead"] = Field(default=None, description="装货段") deliveries: list["PlanDeliveryRead"] = Field(default_factory=list, alias="deliveries", description="送货段列表") model_config = {"populate_by_name": True, "from_attributes": True} class PlanContractGapListResponse(BaseModel): total: int list: list[PlanContractGapRead]