2026-08-30-pl-plan-contract-qty-check-design.md 8.0 KB

运输计划安排 · 合同数量硬拦与成功后提醒

日期: 2026-08-30
状态: 已确认设计(方案 2:后端安排接口强校验 + 响应带回提醒)
Demo: docs/fresh-mint-pl-plan-contract-qty-check-demo.html
范围: backend 运输安排接口 + web-admin 安排成功/失败弹窗;复用合同详情产品安排数量口径

1. 背景与目标

运输计划装货/送货段可绑定 CRM 合同,并从合同产品填写本次 plan_qty。客户需要:

  1. 有固定数量的合同产品:安排时校验「已安排 + 本次」是否超过合同总量;超限硬拦截。
  2. 未超限:安排成功之后弹窗提醒剩余可安排量。
  3. 框架采购/销售合同(产品无固定数量):不拦截;安排成功之后提醒累计已安排数量。

2. 已确认决策

项 决策
超量 硬拦截;弹窗说明差额;必须改数量后才能安排
未超限提醒 安排成功后弹窗提醒剩余量(知悉「知道了」,非二次确认)
框架无数量 不拦;成功后提醒累计已安排(含本次)
校验范围 有绑合同的段都校验
已安排合计口径 仅该合同挂接过的停靠段汇总(与合同详情三列一致)
触发操作 仅「安排运输 / 保存并安排 / 调整运输」
草稿仅保存 不校验、不提醒
调整运输计算 已安排合计 − 本计划旧 planQty + 本计划新 planQty ≤ 合同数量
实现路径 方案 2:arrange 接口强校验 + 成功响应带回 contractQtyReminder

3. 非目标

  • 草稿「仅保存」不做数量校验/提醒
  • 超量软放行(确认后仍安排)
  • 独立预检 API
  • 改动合同详情三列字段定义或汇总算法语义
  • 合同完善页单独做数量校验(本页不安排运输)

4. 数量与分类规则

4.1 「已安排合计」

复用 backend/app/services/pl_contract_plan_qty.py:

  • 关联:pl_plan_contract.contract_id ∪ 遗留 pl_plan.contract_id
  • 排除:follow_status=cancelled、del_flag=1;待安排 / 等通知不计入
  • 分桶:archived / arranged_unarchived;arrangedTotalQty = 二者之和
  • 按合同挂接的停靠段产品、按 product_id 汇总:
    • 已归档:装货用 inbound_qty、送货用 outbound_qty;为 null 时回退 plan_qty;为 0 按 0(不回退)
    • 已安排未归档:用 plan_qty
    • 详见 2026-08-30-pl-contract-qty-archived-actual-design.md

4.2 本计划旧量 / 新量

  • 旧量:当前库内、该合同挂接段上本 plan_id 各 product_id 的 plan_qty(调整前快照;首次安排且尚未计入「已安排」桶时旧量通常为 0 或虽有草稿量但不在合计中)
  • 新量:调用 arrange 时库内已保存的本计划对应段产品 plan_qty(「保存并安排」须先落库计划再 arrange)
  • 有效合计(校验用):arrangedTotalQty - oldPlanQty + newPlanQty
    说明:若本计划当前已在「已安排」桶中,arrangedTotalQty 已含旧量,必须先扣再加,避免自我双计。

4.3 合同产品是否「有固定数量」

  • 合同类型为框架类(framework / framework_sale / framework_purchase)且产品 quantity_mode = customer_demand(或等价「按客户需求」展示)→ 无固定数量 → 不硬拦,走框架累计提醒
  • 否则若产品 quantity 为有效正数 → 有固定数量 → 走硬拦 / 剩余提醒
  • quantity 为空、0 或无法解析 → 视为无固定数量(按提醒累计处理,不硬拦);实现时与框架判定一并单测钉死

4.4 剩余量

remainingQty = contractQty - (arrangedTotalQty - oldPlanQty + newPlanQty)
成功后提醒展示该值(及合同数量、已安排合计含本次)。

5. 架构

安排入口(列表安排/调整、表单保存并安排)
  → 计划已落库
  → POST /transport/order/arrange/{plan_id}
       1) assert_contract_qty_for_arrange(...)  // 超限 → 422,事务未写运输
       2) 既有安排逻辑(运输单、地磅、自动开始执行合同等)
       3) build_contract_qty_reminder(...)      // 写入 TransportArrangeResult
  → 前端:失败弹超额;成功后若有 reminder.items 再弹知悉框

权威计算在后端;前端只解析错误与提醒,不单独实现汇总口径。

6. API

6.1 超量失败

  • HTTP 422
  • detail:结构化列表(优先)或可解析文案,每项至少含:
    • contractId / contractNo
    • stopKind:load | delivery(及送货序号若易得)
    • productId / productName
    • contractQty / arrangedTotalQty(扣旧前或展示用含口径说明)
    • oldPlanQty / newPlanQty / effectiveTotal / overflowQty

前端用 ElMessageBox.alert 标题「无法安排运输」,列出超限产品与差额。

6.2 成功响应扩展

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: [],前端不弹
  • 同一安排可同时含多合同、多产品、两种 mode 的混合 items(一个弹窗汇总展示)

7. 后端实现要点

  • 新建纯函数/服务函数(建议 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 422
  • 在 arrange_transport 中:危化证等现有校验之后、写运输单之前调用 assert;commit 成功组装 result 时附带 reminder(reminder 按安排成功后状态计算,含本计划新量;本计划此时应已进入可计入桶或按「有效合计」公式给出与校验一致的含本次值)
  • 多段多合同:逐合同 × 产品检查;任一超限即失败(可一次返回全部超限项)
  • 无合同段 / 内置免合同:跳过
  • 单测覆盖:超限、未超限剩余、框架累计、调整扣旧加新、多合同、本计划不在已安排桶时的首次安排

8. 前端实现要点

  • 类型:TransportArrangeResult 增加 contractQtyReminder
  • 成功路径:usePlPlanArrangeSubmit / usePlPlanArrangeActions(及表单内安排成功)在现有成功 Toast 之后,若 items.length 则 ElMessageBox.alert(无 Cancel,主按钮「知道了」);文案区分剩余 vs 框架累计;可对齐 Demo 场景 B/C
  • 失败路径:捕获 arrange 422,若识别为合同数量超限则专用弹窗(Demo 场景 A),否则保留原错误提示
  • 不在点击安排前做二次确认弹窗
  • 草稿仅保存路径不调用上述逻辑

9. UI 参考

权威示意:docs/fresh-mint-pl-plan-contract-qty-check-demo.html

  • 场景 A:超量硬拦
  • 场景 B:框架成功后累计提醒
  • 场景 C:未超限成功后剩余提醒

10. 测试清单

  • 有数量超限 → 422,不产生/不更新运输安排副作用
  • 有数量未超 → 安排成功 + reminder.mode=remaining
  • 框架无数量 → 安排成功 + reminder.mode=framework_cumulative
  • 调整运输:扣旧加新后恰不超 / 恰超
  • 装货+送货各绑不同合同:分别校验与提醒
  • 仅保存计划:不校验
  • 与合同详情同合同同产品 arrangedTotalQty 口径一致(同夹具)

11. 风险与注意

  • 「保存并安排」顺序必须先 persist 计划产品数量再 arrange,否则新量读库错误
  • 提醒与校验须使用同一套有效合计公式,避免成功提示的剩余与拦截阈值不一致
  • TransportArrangeResult 为扩展字段,旧客户端可忽略