日期: 2026-08-30
状态: 已确认设计(方案 2:后端安排接口强校验 + 响应带回提醒)
Demo: docs/fresh-mint-pl-plan-contract-qty-check-demo.html
范围: backend 运输安排接口 + web-admin 安排成功/失败弹窗;复用合同详情产品安排数量口径
运输计划装货/送货段可绑定 CRM 合同,并从合同产品填写本次 plan_qty。客户需要:
| 项 | 决策 |
|---|---|
| 超量 | 硬拦截;弹窗说明差额;必须改数量后才能安排 |
| 未超限提醒 | 安排成功后弹窗提醒剩余量(知悉「知道了」,非二次确认) |
| 框架无数量 | 不拦;成功后提醒累计已安排(含本次) |
| 校验范围 | 有绑合同的段都校验 |
| 已安排合计口径 | 仅该合同挂接过的停靠段汇总(与合同详情三列一致) |
| 触发操作 | 仅「安排运输 / 保存并安排 / 调整运输」 |
| 草稿仅保存 | 不校验、不提醒 |
| 调整运输计算 | 已安排合计 − 本计划旧 planQty + 本计划新 planQty ≤ 合同数量 |
| 实现路径 | 方案 2:arrange 接口强校验 + 成功响应带回 contractQtyReminder |
复用 backend/app/services/pl_contract_plan_qty.py:
pl_plan_contract.contract_id ∪ 遗留 pl_plan.contract_idfollow_status=cancelled、del_flag=1;待安排 / 等通知不计入archived / arranged_unarchived;arrangedTotalQty = 二者之和product_id 汇总:
inbound_qty、送货用 outbound_qty;为 null 时回退 plan_qty;为 0 按 0(不回退)plan_qty2026-08-30-pl-contract-qty-archived-actual-design.mdplan_id 各 product_id 的 plan_qty(调整前快照;首次安排且尚未计入「已安排」桶时旧量通常为 0 或虽有草稿量但不在合计中)plan_qty(「保存并安排」须先落库计划再 arrange)arrangedTotalQty - oldPlanQty + newPlanQtyarrangedTotalQty 已含旧量,必须先扣再加,避免自我双计。framework / framework_sale / framework_purchase)且产品 quantity_mode = customer_demand(或等价「按客户需求」展示)→ 无固定数量 → 不硬拦,走框架累计提醒quantity 为有效正数 → 有固定数量 → 走硬拦 / 剩余提醒quantity 为空、0 或无法解析 → 视为无固定数量(按提醒累计处理,不硬拦);实现时与框架判定一并单测钉死remainingQty = contractQty - (arrangedTotalQty - oldPlanQty + newPlanQty)
成功后提醒展示该值(及合同数量、已安排合计含本次)。
安排入口(列表安排/调整、表单保存并安排)
→ 计划已落库
→ POST /transport/order/arrange/{plan_id}
1) assert_contract_qty_for_arrange(...) // 超限 → 422,事务未写运输
2) 既有安排逻辑(运输单、地磅、自动开始执行合同等)
3) build_contract_qty_reminder(...) // 写入 TransportArrangeResult
→ 前端:失败弹超额;成功后若有 reminder.items 再弹知悉框
权威计算在后端;前端只解析错误与提醒,不单独实现汇总口径。
422detail:结构化列表(优先)或可解析文案,每项至少含:
contractId / contractNostopKind:load | delivery(及送货序号若易得)productId / productNamecontractQty / arrangedTotalQty(扣旧前或展示用含口径说明)oldPlanQty / newPlanQty / effectiveTotal / overflowQty前端用 ElMessageBox.alert 标题「无法安排运输」,列出超限产品与差额。
TransportArrangeResult 增加可选:
contractQtyReminder?: {
items: Array<{
contractId: string
contractNo?: string
contractType?: string
stopKind: 'load' | 'delivery'
productId: string
productName?: string
mode: 'remaining' | 'framework_cumulative'
contractQty?: number // remaining
arrangedTotalQty: number // 含本次有效合计
remainingQty?: number // remaining
thisPlanQty: number // 本次新量
}>
}
items: [],前端不弹pl_contract_plan_qty.py 旁或同文件扩展):
collect_plan_contract_qty_checks(db, tenant_no, plan_id) -> (violations, reminder_items)assert_contract_qty_for_arrange(...):violations 非空则抛 HTTPException 422arrange_transport 中:危化证等现有校验之后、写运输单之前调用 assert;commit 成功组装 result 时附带 reminder(reminder 按安排成功后状态计算,含本计划新量;本计划此时应已进入可计入桶或按「有效合计」公式给出与校验一致的含本次值)TransportArrangeResult 增加 contractQtyReminderusePlPlanArrangeSubmit / usePlPlanArrangeActions(及表单内安排成功)在现有成功 Toast 之后,若 items.length 则 ElMessageBox.alert(无 Cancel,主按钮「知道了」);文案区分剩余 vs 框架累计;可对齐 Demo 场景 B/C422,若识别为合同数量超限则专用弹窗(Demo 场景 A),否则保留原错误提示权威示意:docs/fresh-mint-pl-plan-contract-qty-check-demo.html
arrangedTotalQty 口径一致(同夹具)TransportArrangeResult 为扩展字段,旧客户端可忽略