Files
shanxia2.0/backend/app/schemas/pl/plan.py
T
2026-08-06 22:49:00 +08:00

398 lines
24 KiB
Python

"""运输计划管理 - 计划 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)
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 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="归档状态")
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="是否有运输费用")
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 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="产品明细列表")
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")
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
transport: Optional[PlanTransportCreateBlock] = 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 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="危化品证书确认时间")
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")
model_config = {"from_attributes": True, "populate_by_name": True}
class PlanWithProducts(PlanRead):
products: list[PlanProductRead] = []
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: str = Field(..., alias="contractId", description="CRM合同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=100, 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: Decimal = Field(..., alias="actualQty", ge=0, description="实际数量")
brand_qtys: Optional[list[PlanArchiveBrandQtyItem]] = Field(default=None, alias="brandQtys")
model_config = {"populate_by_name": True}
class PlanArchiveFreightBlock(BaseModel):
has_freight: int = Field(default=1, alias="hasFreight", description="是否有运输费用 0-否 1-是")
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 PlanArchiveBody(BaseModel):
products: list[PlanArchiveProductItem] = Field(..., min_length=1)
freight: Optional[PlanArchiveFreightBlock] = 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]