Files
shanxia2.0/backend/app/schemas/pl/plan.py
T
2026-08-30 09:30:02 +08:00

701 lines
41 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""运输计划管理 - 计划 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]