From b7236329c2423fe20c9d4c0a783188a2fe552eaa Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Fri, 31 Jul 2026 05:31:22 +0800 Subject: [PATCH] feat(web-admin): driver feedback dialog brand qty entry Co-authored-by: Cursor --- backend/app/schemas/pl/plan.py | 28 +- backend/app/services/pl_plan_service.py | 25 +- web-admin/src/api/pl/index.ts | 20 +- .../pl-plan-driver-feedback-dialog/index.vue | 452 +++++++++++++++--- .../business/pl-plan-view-tabs/index.vue | 2 +- 5 files changed, 458 insertions(+), 69 deletions(-) diff --git a/backend/app/schemas/pl/plan.py b/backend/app/schemas/pl/plan.py index f690a0c40..2c00c79d7 100644 --- a/backend/app/schemas/pl/plan.py +++ b/backend/app/schemas/pl/plan.py @@ -88,8 +88,16 @@ class PlanProductRead(PlanProductBase): class PlanBase(BaseModel): belongs_company: str = Field(..., alias="belongsCompany", description="归属公司 shanxia-安庆山峡 yundong-安徽云栋") - plan_type: str = Field(..., alias="planType", description="单据类型 direct-直销单 inbound-入库单 outbound-出库单") - follow_status: str = Field(default="draft", alias="followStatus", description="跟进状态") + 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="完成状态") @@ -130,7 +138,7 @@ class PlanCreate(PlanBase): create_date: Optional[date] = Field( default=None, alias="createDate", description="创建日期;缺省则服务端用当天" ) - plan_date: date = Field(..., alias="planDate", 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} @@ -199,7 +207,7 @@ class PlanRead(PlanBase, PlanResourceSnapshotMixin): id: str plan_no: str = Field(alias="planNo") create_date: date = Field(alias="createDate") - plan_date: date = Field(alias="planDate") + 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="危化品证书确认时间") @@ -256,7 +264,7 @@ class CreateFromContractRequest(BaseModel): create_date: Optional[date] = Field( default=None, alias="createDate", description="创建日期;缺省当天" ) - plan_date: date = Field(..., alias="planDate", 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} @@ -332,11 +340,19 @@ class PlanArchiveBody(BaseModel): 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_qty: Optional[Decimal] = Field(default=None, alias="feedbackQty", ge=0, description="回传数量") 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} diff --git a/backend/app/services/pl_plan_service.py b/backend/app/services/pl_plan_service.py index 96469d8ed..32de5c053 100644 --- a/backend/app/services/pl_plan_service.py +++ b/backend/app/services/pl_plan_service.py @@ -32,6 +32,7 @@ from app.schemas.pl.plan import ( PlanTransportCreateBlock, ) from app.services.pl_brand_archive import persist_plan_product_brand_qtys, validate_brand_qty_rows +from app.services.pl_feedback_brand_qty import persist_transport_product_feedback from app.services.pl_freight_settlement_service import sync_draft_settlement_freight_for_transport from app.services.pl_transport_resource_snapshot_service import ( apply_resource_snapshot_json, @@ -966,6 +967,7 @@ def create_transport_for_plan( driver_id=driver_id, ) plan.transport_status = "arranged" + plan.follow_status = "arranged" plan.freight_amount = freight_amount or 0 return transport @@ -1316,7 +1318,28 @@ def apply_plan_driver_feedback( if not transport: raise HTTPException(status_code=404, detail="运输单不存在") - if payload.feedback_qty is not None: + if payload.products is not None: + products_payload = [ + { + "planProductId": item.plan_product_id, + "actualQty": item.actual_qty, + "brandQtys": [b.model_dump(by_alias=True) for b in (item.brand_qtys or [])], + } + for item in payload.products + ] + try: + persist_transport_product_feedback( + db, + tenant_no=tenant_no, + transport=transport, + plan_id=plan.id, + products=products_payload, + user_id=user_id, + enforce_brand=True, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + elif payload.feedback_qty is not None: transport.feedback_qty = payload.feedback_qty if payload.feedback_remark is not None: transport.feedback_remark = payload.feedback_remark.strip() if payload.feedback_remark else None diff --git a/web-admin/src/api/pl/index.ts b/web-admin/src/api/pl/index.ts index 903d16d49..df1373101 100644 --- a/web-admin/src/api/pl/index.ts +++ b/web-admin/src/api/pl/index.ts @@ -416,7 +416,7 @@ export interface Plan { id: string planNo: string createDate: string - planDate: string + planDate?: string | null belongsCompany: string planType: string followStatus: string @@ -509,9 +509,9 @@ export interface PlanTransportCreateBlock { export interface PlanCreateFullPayload { contractId: string createDate: string - planDate: string + planDate?: string | null belongsCompany: string - planType: string + planType?: string contractNo?: string supplierId?: string supplierName?: string @@ -591,11 +591,18 @@ export interface PlanArchiveBody { freight?: PlanArchiveFreightBlock } +export interface PlanDriverFeedbackProductItem { + planProductId: string + actualQty: number + brandQtys?: PlanArchiveBrandQtyItem[] +} + export interface PlanDriverFeedbackUpdateBody { feedbackQty?: number | null feedbackRemark?: string | null feedbackImages?: string[] feedbackTime?: string | null + products?: PlanDriverFeedbackProductItem[] } export interface PlanSiteFeedbackUpdateBody { @@ -626,6 +633,9 @@ export const planApi = { setHazardCertReady: (id: string, ready: boolean) => request.patch(`/transport/plan/${id}/hazard-cert-ready`, { ready }), cancel: (id: string) => request.post(`/transport/plan/${id}/cancel`), + waitNotice: (id: string) => request.post(`/transport/plan/${id}/wait-notice`), + cancelWaitNotice: (id: string) => + request.post(`/transport/plan/${id}/cancel-wait-notice`), getArrangeInfo: (id: string) => request.get(`/transport/plan/${id}/arrange-info`), archive: (id: string, data?: PlanArchiveBody) => request.post(`/transport/plan/${id}/archive`, data), @@ -636,7 +646,7 @@ export const planApi = { delete: (id: string) => request.delete(`/transport/plan/${id}`), createFromContract: (data: { contractId: string - planDate: string + planDate?: string | null createDate?: string followUserId?: string followUserName?: string @@ -776,6 +786,7 @@ export interface PlIoOrderItem { productNo?: string productName?: string brandSummary?: string + brandName?: string specification?: string unit?: string unitLabel?: string @@ -783,6 +794,7 @@ export interface PlIoOrderItem { actualQty?: number qtySource?: string packageSpec?: string + deliveryMethod?: string remark?: string } diff --git a/web-admin/src/components/business/pl-plan-driver-feedback-dialog/index.vue b/web-admin/src/components/business/pl-plan-driver-feedback-dialog/index.vue index 25d99d99d..0e840da1a 100644 --- a/web-admin/src/components/business/pl-plan-driver-feedback-dialog/index.vue +++ b/web-admin/src/components/business/pl-plan-driver-feedback-dialog/index.vue @@ -3,7 +3,7 @@ - - - - - - - - - - - - - - +
+ + + + + + + + + + + + +
+

产品回传数量

+

+ 暂无产品明细 +

+
+
+ {{ row.productName || '产品' }} + + 产品 {{ idx + 1 }}/{{ productRows.length }} + +
+
+ 实际数量 + + {{ row.unitLabel || row.unit || '吨' }} +
+ +
+
+ 合计回传 + {{ totalFeedbackQty }} +
+
+