plan.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. """运输计划管理 - 计划 Schema"""
  2. from datetime import date, datetime
  3. from decimal import Decimal
  4. from typing import Optional
  5. from pydantic import BaseModel, Field
  6. from app.schemas.pl.datetime_parse import OptionalPlanDateTime
  7. from app.schemas.pl.resource_snapshot import PlanResourceSnapshotMixin
  8. # =============================================================================
  9. # 计划产品明细 Schema
  10. # =============================================================================
  11. class PlanProductBase(BaseModel):
  12. product_id: str = Field(..., alias="productId", description="产品ID")
  13. product_no: str = Field(..., max_length=50, alias="productNo", description="产品编号")
  14. product_name: str = Field(..., max_length=200, alias="productName", description="产品名称")
  15. brand_mode: Optional[str] = Field(default=None, alias="brandMode", description="required/any")
  16. brand_id: Optional[str] = Field(default=None, alias="brandId", description="指定品牌ID")
  17. brand_name: Optional[str] = Field(default=None, alias="brandName", description="品牌名快照")
  18. model: Optional[str] = Field(default=None, max_length=100, description="规格型号")
  19. unit: str = Field(default="吨", max_length=20, description="单位")
  20. category_name: Optional[str] = Field(default=None, max_length=200, alias="categoryName", description="产品分类")
  21. package_spec: Optional[str] = Field(default=None, max_length=200, alias="packageSpec", description="包装规格")
  22. delivery_method: Optional[str] = Field(default=None, max_length=50, alias="deliveryMethod", description="送货方式")
  23. quantity: Optional[Decimal] = Field(default=None, description="合同数量")
  24. actual_qty: Optional[Decimal] = Field(default=None, alias="actualQty", description="实际数量")
  25. tax_price: Optional[Decimal] = Field(default=None, alias="taxPrice", description="含税单价")
  26. tax_amount: Optional[Decimal] = Field(default=None, alias="taxAmount", description="税费")
  27. tax_subtotal: Optional[Decimal] = Field(default=None, alias="taxSubtotal", description="含税总价")
  28. is_explosive: int = Field(default=0, alias="isExplosive", description="是否易制爆")
  29. is_toxic: int = Field(default=0, alias="isToxic", description="是否易制毒")
  30. purchase_cert_url: Optional[str] = Field(default=None, alias="purchaseCertUrl", description="购买证URL")
  31. transport_cert_url: Optional[str] = Field(default=None, alias="transportCertUrl", description="运输证URL")
  32. legal_use_cert_url: Optional[str] = Field(default=None, alias="legalUseCertUrl", description="合法使用证明URL")
  33. inbound_estimate_qty: Optional[Decimal] = Field(default=None, alias="inboundEstimateQty", description="入库预估数")
  34. inbound_qty: Optional[Decimal] = Field(default=None, alias="inboundQty", description="入库数")
  35. inbound_price: Optional[Decimal] = Field(default=None, alias="inboundPrice", description="入库价")
  36. inbound_amount: Optional[Decimal] = Field(default=None, alias="inboundAmount", description="入库金额")
  37. outbound_estimate_qty: Optional[Decimal] = Field(default=None, alias="outboundEstimateQty", description="出库预估数")
  38. outbound_qty: Optional[Decimal] = Field(default=None, alias="outboundQty", description="出库数")
  39. outbound_price: Optional[Decimal] = Field(default=None, alias="outboundPrice", description="出库单价")
  40. outbound_amount: Optional[Decimal] = Field(default=None, alias="outboundAmount", description="出库金额")
  41. remark: Optional[str] = Field(default=None, max_length=512, description="备注")
  42. class PlanProductCreate(PlanProductBase):
  43. model_config = {"populate_by_name": True}
  44. class PlanProductUpdate(BaseModel):
  45. product_id: Optional[str] = Field(default=None, alias="productId")
  46. product_no: Optional[str] = Field(default=None, max_length=50, alias="productNo")
  47. product_name: Optional[str] = Field(default=None, max_length=200, alias="productName")
  48. model: Optional[str] = Field(default=None, max_length=100)
  49. unit: Optional[str] = Field(default=None, max_length=20)
  50. is_explosive: Optional[int] = Field(default=None, alias="isExplosive")
  51. is_toxic: Optional[int] = Field(default=None, alias="isToxic")
  52. purchase_cert_url: Optional[str] = Field(default=None, alias="purchaseCertUrl")
  53. transport_cert_url: Optional[str] = Field(default=None, alias="transportCertUrl")
  54. legal_use_cert_url: Optional[str] = Field(default=None, alias="legalUseCertUrl")
  55. inbound_estimate_qty: Optional[Decimal] = Field(default=None, alias="inboundEstimateQty")
  56. inbound_qty: Optional[Decimal] = Field(default=None, alias="inboundQty")
  57. inbound_price: Optional[Decimal] = Field(default=None, alias="inboundPrice")
  58. inbound_amount: Optional[Decimal] = Field(default=None, alias="inboundAmount")
  59. outbound_estimate_qty: Optional[Decimal] = Field(default=None, alias="outboundEstimateQty")
  60. outbound_qty: Optional[Decimal] = Field(default=None, alias="outboundQty")
  61. outbound_price: Optional[Decimal] = Field(default=None, alias="outboundPrice")
  62. outbound_amount: Optional[Decimal] = Field(default=None, alias="outboundAmount")
  63. model_config = {"populate_by_name": True}
  64. class PlanProductRead(PlanProductBase):
  65. id: str
  66. plan_id: str = Field(alias="planId")
  67. tenant_no: str = Field(alias="tenantNo")
  68. create_time: datetime = Field(alias="createTime")
  69. update_time: datetime = Field(alias="updateTime")
  70. model_config = {"from_attributes": True, "populate_by_name": True}
  71. # =============================================================================
  72. # 计划主表 Schema
  73. # =============================================================================
  74. class PlanBase(BaseModel):
  75. belongs_company: str = Field(..., alias="belongsCompany", description="归属公司 shanxia-安庆山峡 yundong-安徽云栋")
  76. plan_type: str = Field(
  77. default="",
  78. alias="planType",
  79. description="单据类型 direct-直销单 inbound-入库单 outbound-出库单;未选择可为空",
  80. )
  81. follow_status: str = Field(
  82. default="pending_arrange",
  83. alias="followStatus",
  84. description="跟进状态 wait_notice/pending_arrange/arranged/completed/cancelled",
  85. )
  86. transport_status: str = Field(default="pending", alias="transportStatus", description="运输安排状态")
  87. archive_status: str = Field(default="unarchived", alias="archiveStatus", description="归档状态")
  88. complete_status: str = Field(default="no", alias="completeStatus", description="完成状态")
  89. contract_id: Optional[str] = Field(default=None, alias="contractId", description="CRM合同ID")
  90. contract_no: Optional[str] = Field(default=None, max_length=50, alias="contractNo", description="合同编号")
  91. supplier_id: Optional[str] = Field(default=None, alias="supplierId", description="供应商ID")
  92. supplier_name: Optional[str] = Field(default=None, max_length=200, alias="supplierName", description="供应商名称")
  93. destination_id: Optional[str] = Field(default=None, alias="destinationId", description="目的地ID")
  94. destination_name: Optional[str] = Field(default=None, max_length=200, alias="destinationName", description="目的地名称")
  95. customer_id: Optional[str] = Field(default=None, alias="customerId", description="客户ID")
  96. customer_name: Optional[str] = Field(default=None, max_length=200, alias="customerName", description="客户名称")
  97. contact_name: Optional[str] = Field(default=None, max_length=100, alias="contactName", description="联系人")
  98. contact_phone: Optional[str] = Field(default=None, max_length=32, alias="contactPhone", description="联系电话")
  99. party_a: Optional[str] = Field(default=None, max_length=200, alias="partyA", description="甲方")
  100. party_b: Optional[str] = Field(default=None, max_length=200, alias="partyB", description="乙方")
  101. contract_start_date: Optional[date] = Field(default=None, alias="contractStartDate", description="合同开始日期")
  102. contract_end_date: Optional[date] = Field(default=None, alias="contractEndDate", description="合同结束日期")
  103. delivery_method: Optional[str] = Field(default=None, alias="deliveryMethod", description="送货方式")
  104. settlement_method: Optional[str] = Field(default=None, max_length=50, alias="settlementMethod", description="结算方式")
  105. has_freight: int = Field(default=0, alias="hasFreight", description="是否有运输费用")
  106. pickup_address: Optional[str] = Field(default=None, max_length=500, alias="pickupAddress", description="取货地址")
  107. pickup_lng: Optional[Decimal] = Field(default=None, alias="pickupLng", description="取货经度")
  108. pickup_lat: Optional[Decimal] = Field(default=None, alias="pickupLat", description="取货纬度")
  109. pickup_time: OptionalPlanDateTime = Field(default=None, alias="pickupTime", description="取货时间")
  110. pickup_notes: Optional[str] = Field(default=None, max_length=500, alias="pickupNotes", description="取货注意事项")
  111. destination_address: Optional[str] = Field(default=None, max_length=500, alias="destinationAddress", description="目的地地址")
  112. destination_lng: Optional[Decimal] = Field(default=None, alias="destinationLng", description="送货经度")
  113. destination_lat: Optional[Decimal] = Field(default=None, alias="destinationLat", description="送货纬度")
  114. delivery_time: OptionalPlanDateTime = Field(default=None, alias="deliveryTime", description="送达时间")
  115. destination_notes: Optional[str] = Field(default=None, max_length=500, alias="destinationNotes", description="目的地注意事项")
  116. notify_message: Optional[str] = Field(default=None, alias="notifyMessage", description="通知信息")
  117. follow_user_id: Optional[str] = Field(default=None, alias="followUserId", description="跟进人ID")
  118. follow_user_name: Optional[str] = Field(default=None, max_length=100, alias="followUserName", description="跟进人姓名")
  119. remark: Optional[str] = None
  120. class PlanCreate(PlanBase):
  121. create_date: Optional[date] = Field(
  122. default=None, alias="createDate", description="创建日期;缺省则服务端用当天"
  123. )
  124. plan_date: Optional[date] = Field(default=None, alias="planDate", description="计划日期(非必填)")
  125. products: Optional[list[PlanProductCreate]] = Field(default=None, description="产品明细列表")
  126. model_config = {"populate_by_name": True}
  127. class PlanTransportCreateBlock(BaseModel):
  128. vehicle_id: str = Field(..., alias="vehicleId", description="车辆ID")
  129. driver_id: Optional[str] = Field(default=None, alias="driverId", description="司机/人员ID")
  130. carrier_id: Optional[str] = Field(default=None, alias="carrierId", description="承运商ID")
  131. distance_km: Optional[Decimal] = Field(default=None, alias="distanceKm", description="运输公里数")
  132. freight_type: str = Field(default="by_weight", alias="freightType", description="运费计算方式")
  133. transport_qty: Optional[Decimal] = Field(default=None, alias="transportQty", description="运输量")
  134. freight_price: Optional[Decimal] = Field(default=None, alias="freightPrice", description="运输单价")
  135. freight_amount: Optional[Decimal] = Field(default=None, alias="freightAmount", description="运费金额")
  136. has_invoice: Optional[int] = Field(default=None, alias="hasInvoice", description="是否开票(已废弃,由运费结算维护)")
  137. remark: Optional[str] = None
  138. model_config = {"populate_by_name": True}
  139. class PlanUpdate(BaseModel):
  140. create_date: Optional[date] = Field(default=None, alias="createDate")
  141. plan_date: Optional[date] = Field(default=None, alias="planDate")
  142. belongs_company: Optional[str] = Field(default=None, alias="belongsCompany")
  143. plan_type: Optional[str] = Field(default=None, alias="planType")
  144. follow_status: Optional[str] = Field(default=None, alias="followStatus")
  145. transport_status: Optional[str] = Field(default=None, alias="transportStatus")
  146. supplier_id: Optional[str] = Field(default=None, alias="supplierId")
  147. supplier_name: Optional[str] = Field(default=None, max_length=200, alias="supplierName")
  148. destination_id: Optional[str] = Field(default=None, alias="destinationId")
  149. destination_name: Optional[str] = Field(default=None, max_length=200, alias="destinationName")
  150. customer_id: Optional[str] = Field(default=None, alias="customerId")
  151. customer_name: Optional[str] = Field(default=None, max_length=200, alias="customerName")
  152. contact_name: Optional[str] = Field(default=None, max_length=100, alias="contactName")
  153. contact_phone: Optional[str] = Field(default=None, max_length=32, alias="contactPhone")
  154. party_a: Optional[str] = Field(default=None, max_length=200, alias="partyA")
  155. party_b: Optional[str] = Field(default=None, max_length=200, alias="partyB")
  156. contract_start_date: Optional[date] = Field(default=None, alias="contractStartDate")
  157. contract_end_date: Optional[date] = Field(default=None, alias="contractEndDate")
  158. delivery_method: Optional[str] = Field(default=None, alias="deliveryMethod")
  159. settlement_method: Optional[str] = Field(default=None, max_length=50, alias="settlementMethod")
  160. has_freight: Optional[int] = Field(default=None, alias="hasFreight")
  161. pickup_address: Optional[str] = Field(default=None, max_length=500, alias="pickupAddress")
  162. pickup_lng: Optional[Decimal] = Field(default=None, alias="pickupLng")
  163. pickup_lat: Optional[Decimal] = Field(default=None, alias="pickupLat")
  164. pickup_time: OptionalPlanDateTime = Field(default=None, alias="pickupTime")
  165. pickup_notes: Optional[str] = Field(default=None, max_length=500, alias="pickupNotes")
  166. destination_address: Optional[str] = Field(default=None, max_length=500, alias="destinationAddress")
  167. destination_lng: Optional[Decimal] = Field(default=None, alias="destinationLng")
  168. destination_lat: Optional[Decimal] = Field(default=None, alias="destinationLat")
  169. delivery_time: OptionalPlanDateTime = Field(default=None, alias="deliveryTime")
  170. destination_notes: Optional[str] = Field(default=None, max_length=500, alias="destinationNotes")
  171. notify_message: Optional[str] = Field(default=None, alias="notifyMessage")
  172. follow_user_id: Optional[str] = Field(default=None, alias="followUserId")
  173. follow_user_name: Optional[str] = Field(default=None, alias="followUserName")
  174. remark: Optional[str] = None
  175. products: Optional[list[PlanProductCreate]] = None
  176. transport: Optional[PlanTransportCreateBlock] = Field(default=None, description="运输预选/更新(有车牌时同步车辆与运费)")
  177. model_config = {"populate_by_name": True}
  178. class HazardCertReadyBody(BaseModel):
  179. ready: bool = Field(..., description="true=确认证书齐备 false=取消齐备")
  180. model_config = {"populate_by_name": True}
  181. class PlanRead(PlanBase, PlanResourceSnapshotMixin):
  182. id: str
  183. plan_no: str = Field(alias="planNo")
  184. create_date: date = Field(alias="createDate")
  185. plan_date: Optional[date] = Field(default=None, alias="planDate")
  186. hazard_cert_ready: int = Field(default=0, alias="hazardCertReady", description="危化品证书是否已人工确认齐备")
  187. hazard_cert_ready_by: Optional[str] = Field(default=None, alias="hazardCertReadyBy", description="危化品证书确认人")
  188. hazard_cert_ready_time: Optional[datetime] = Field(default=None, alias="hazardCertReadyTime", description="危化品证书确认时间")
  189. vehicle_id: Optional[str] = Field(default=None, alias="vehicleId")
  190. license_plate: Optional[str] = Field(default=None, alias="licensePlate")
  191. driver_id: Optional[str] = Field(default=None, alias="driverId")
  192. driver_name: Optional[str] = Field(default=None, alias="driverName")
  193. personnel_summary: Optional[str] = Field(default=None, alias="personnelSummary")
  194. transport_id: Optional[str] = Field(default=None, alias="transportId")
  195. transport_no: Optional[str] = Field(default=None, alias="transportNo")
  196. total_amount: Decimal = Field(alias="totalAmount")
  197. freight_amount: Decimal = Field(alias="freightAmount")
  198. tenant_no: str = Field(alias="tenantNo")
  199. order_num: Optional[int] = Field(default=None, alias="orderNum")
  200. create_by: Optional[str] = Field(default=None, alias="createBy")
  201. create_dept_id: Optional[str] = Field(default=None, alias="createDeptId")
  202. create_time: datetime = Field(alias="createTime")
  203. update_by: Optional[str] = Field(default=None, alias="updateBy")
  204. update_time: datetime = Field(alias="updateTime")
  205. del_flag: int = Field(default=0, alias="delFlag")
  206. model_config = {"from_attributes": True, "populate_by_name": True}
  207. class PlanWithProducts(PlanRead):
  208. products: list[PlanProductRead] = []
  209. class PlanInboundSlipTemplateSave(BaseModel):
  210. slip_template_content: str = Field(..., alias="slipTemplateContent", description="入库单打印模板HTML")
  211. model_config = {"populate_by_name": True}
  212. class PlanDeliverySlipTemplateSave(BaseModel):
  213. delivery_slip_template_content: str = Field(
  214. ..., alias="slipTemplateContent", description="送(提)货单打印模板HTML"
  215. )
  216. model_config = {"populate_by_name": True}
  217. class PlanSubpackSlipTemplateSave(BaseModel):
  218. subpack_slip_template_content: str = Field(
  219. ..., alias="slipTemplateContent", description="分装单打印模板HTML"
  220. )
  221. model_config = {"populate_by_name": True}
  222. # =============================================================================
  223. # 从合同创建计划
  224. # =============================================================================
  225. class CreateFromContractRequest(BaseModel):
  226. contract_id: str = Field(..., alias="contractId", description="CRM合同ID")
  227. create_date: Optional[date] = Field(
  228. default=None, alias="createDate", description="创建日期;缺省当天"
  229. )
  230. plan_date: Optional[date] = Field(default=None, alias="planDate", description="计划日期(非必填)")
  231. follow_user_id: Optional[str] = Field(default=None, alias="followUserId", description="跟进人ID")
  232. follow_user_name: Optional[str] = Field(default=None, max_length=100, alias="followUserName", description="跟进人姓名")
  233. model_config = {"populate_by_name": True}
  234. class PlanCreateFull(PlanCreate):
  235. contract_id: str = Field(..., alias="contractId", description="CRM合同ID")
  236. transport: Optional[PlanTransportCreateBlock] = Field(default=None, description="运输安排(有车牌时创建运输)")
  237. model_config = {"populate_by_name": True}
  238. # =============================================================================
  239. # 计划列表查询
  240. # =============================================================================
  241. class PlanListQuery(BaseModel):
  242. q: Optional[str] = Field(default=None, description="模糊搜索计划号/客户名称")
  243. plan_no: Optional[str] = Field(default=None, alias="planNo", description="计划编号")
  244. plan_type: Optional[str] = Field(default=None, alias="planType", description="单据类型")
  245. follow_status: Optional[str] = Field(default=None, alias="followStatus", description="跟进状态")
  246. transport_status: Optional[str] = Field(default=None, alias="transportStatus", description="运输状态")
  247. archive_status: Optional[str] = Field(default=None, alias="archiveStatus", description="归档状态")
  248. customer_name: Optional[str] = Field(default=None, alias="customerName", description="客户名称")
  249. plan_date_from: Optional[date] = Field(default=None, alias="planDateFrom", description="计划日期开始")
  250. plan_date_to: Optional[date] = Field(default=None, alias="planDateTo", description="计划日期结束")
  251. page: int = Field(default=1, ge=1, description="页码")
  252. page_size: int = Field(default=20, ge=1, le=100, alias="pageSize", description="每页数量")
  253. model_config = {"populate_by_name": True}
  254. class PlanProductCertItem(BaseModel):
  255. id: str = Field(..., description="计划产品明细ID")
  256. purchase_cert_url: Optional[str] = Field(default=None, alias="purchaseCertUrl", description="购买证URL")
  257. transport_cert_url: Optional[str] = Field(default=None, alias="transportCertUrl", description="运输证URL")
  258. legal_use_cert_url: Optional[str] = Field(default=None, alias="legalUseCertUrl", description="合法使用证明URL")
  259. model_config = {"populate_by_name": True}
  260. class PlanProductCertsUpdate(BaseModel):
  261. products: list[PlanProductCertItem] = Field(..., description="危化品证书更新列表")
  262. model_config = {"populate_by_name": True}
  263. class PlanArchiveBrandQtyItem(BaseModel):
  264. brand_id: str = Field(default="", alias="brandId")
  265. brand_name: Optional[str] = Field(default=None, alias="brandName")
  266. qty: Decimal = Field(..., ge=0)
  267. model_config = {"populate_by_name": True}
  268. class PlanArchiveProductItem(BaseModel):
  269. id: str = Field(..., description="计划产品明细ID pl_plan_product.id")
  270. actual_qty: Decimal = Field(..., alias="actualQty", ge=0, description="实际数量")
  271. brand_qtys: Optional[list[PlanArchiveBrandQtyItem]] = Field(default=None, alias="brandQtys")
  272. model_config = {"populate_by_name": True}
  273. class PlanArchiveFreightBlock(BaseModel):
  274. has_freight: int = Field(default=1, alias="hasFreight", description="是否有运输费用 0-否 1-是")
  275. distance_km: Optional[Decimal] = Field(default=None, alias="distanceKm")
  276. freight_type: str = Field(default="by_weight", alias="freightType")
  277. freight_price: Optional[Decimal] = Field(default=None, alias="freightPrice")
  278. freight_amount: Optional[Decimal] = Field(default=None, alias="freightAmount")
  279. transport_qty: Optional[Decimal] = Field(default=None, alias="transportQty")
  280. has_invoice: Optional[int] = Field(default=None, alias="hasInvoice")
  281. model_config = {"populate_by_name": True}
  282. class PlanArchiveBody(BaseModel):
  283. products: list[PlanArchiveProductItem] = Field(..., min_length=1)
  284. freight: Optional[PlanArchiveFreightBlock] = None
  285. model_config = {"populate_by_name": True}
  286. class PlanDriverFeedbackProductItem(BaseModel):
  287. plan_product_id: str = Field(alias="planProductId")
  288. actual_qty: Decimal = Field(alias="actualQty", ge=0)
  289. brand_qtys: Optional[list[PlanArchiveBrandQtyItem]] = Field(default=None, alias="brandQtys")
  290. model_config = {"populate_by_name": True}
  291. class PlanDriverFeedbackUpdateBody(BaseModel):
  292. feedback_remark: Optional[str] = Field(default=None, alias="feedbackRemark", max_length=500, description="回传备注")
  293. feedback_images: Optional[list[str]] = Field(default=None, alias="feedbackImages", description="回传附件URLs")
  294. feedback_time: OptionalPlanDateTime = Field(default=None, alias="feedbackTime", description="回传时间")
  295. products: Optional[list[PlanDriverFeedbackProductItem]] = None
  296. feedback_qty: Optional[Decimal] = Field(default=None, alias="feedbackQty", ge=0, description="回传数量")
  297. model_config = {"populate_by_name": True}
  298. class PlanSiteFeedbackUpdateBody(BaseModel):
  299. site_feedback_remark: Optional[str] = Field(
  300. default=None, alias="siteFeedbackRemark", max_length=500, description="现场回执备注"
  301. )
  302. site_feedback_images: Optional[list[str]] = Field(
  303. default=None, alias="siteFeedbackImages", description="现场回执附件URLs"
  304. )
  305. site_feedback_time: OptionalPlanDateTime = Field(
  306. default=None, alias="siteFeedbackTime", description="现场回执保存时间"
  307. )
  308. model_config = {"populate_by_name": True}
  309. class PlanListResponse(BaseModel):
  310. total: int
  311. list: list[PlanWithProducts]