Files
shanxia2.0/docs/superpowers/specs/2026-10-15-plan-list-segment-export-design.md
T
2026-10-19 18:28:02 +08:00

7.9 KiB
Raw Blame History

运输计划 / 库存列表导出 · 段次×产品行 — 设计文档

日期: 2026-10-15
状态: 待实现
方案: A — 完全对齐列表展平行(flattenPlanArchivePairRows)
范围: 运输计划列表 + 库存管理入库单/出库单(共用同一套导出逻辑)

1. 目标

列表导出 Excel 的行模型与列表 UI 一致:

  • 一装多送时,能按段次拆成多行(先装货产品行,再各送货段产品行)
  • 每行的「计划量 / 实际量」为本行所属段上的数量,不再把多段产品/客户压成一行
  • 库存管理(stockRole=inbound|outbound)与运输计划列表走同一后端导出构建函数

2. 已确认决策

# 项 决策
D1 行结构 方案 A:段次×产品多行,对齐 flattenPlanArchivePairRows
D2 作用面 运输计划列表 + 入库单 + 出库单,共用逻辑
D3 段次列 新增「段次」列:装货 / 送货 #1 / 送货 #2…
D4 客户/地址列 装货行只填装货侧;送货行只填对应送货段;不再用 / 拼接多送客户
D5 计划级字段 计划编号等每行重复(Excel 不做合并单元格)
D6 归档导出 不改归档配对宽表(build_plan_archive_export_rows)
D7 文件名 / Sheet 保持现有:入库单 / 出库单 / 计划列表

3. 非目标

  • 不改列表 UI、合并单元格、斑马纹等展示逻辑
  • 不改计划归档导出的配对宽表与「装货量+送货量同列」结构
  • 不新增独立导出 API;仍走现有 plan 列表导出入口
  • 不做前端本地拼 Excel;行展开在后端完成

4. 现状问题

层 现状 问题
列表 flattenPlanArchivePairRows:装货产品行 → 各送货段产品行;planQty/actualQty 取本段产品 正确
列表导出 _build_plan_list_export_rows:按计划级 PlPlanProduct 展开;装/送客户、地址用 / 汇总整单 一装多送时一行;数量无法按段对照列表

5. 行模型

对每个计划,按 stops_by_plan(load_plan_stops_grouped)展开:

装货段.products[]  →  行:段次=装货,产品/量=装货段产品
送货段1.products[] →  行:段次=送货 #1,产品/量=该送货段产品
送货段2.products[] →  行:段次=送货 #2,…

5.1 顺序与占位

  1. 先全部装货段产品行,再按送货 seq 依次展开各送货段产品行(与列表一致)。
  2. 某侧无产品:仍输出该侧占位行(产品列、计划量、实际量为空),与列表「空产品仍占一行」行为一致。
  3. 无段结构(legacy):无 load/delivery 段时,回退为计划级 PlPlanProduct 一行(段次为空),兼容旧数据。
  4. 筛选条件(含 stockRole、列头筛选等)仍由现有列表查询决定「导出哪些计划」;本设计只改单计划内行展开。

5.2 示例

计划 PO20261009016:装货 1 个产品、送货 2 段各 1 个产品 → 导出 3 行:

段次 装货客户 送货客户 产品 计划量
装货 A (空) 尿素 30
送货 #1 (空) B1 尿素 20
送货 #2 (空) B2 尿素 10

6. 列语义

6.1 新增列

field 表头 取值
stopSegment 段次 装货;送货 #{seq}(seq 缺省时用展开序号)

默认导出列顺序:建议紧挨「计划编号」之后。前端可见列导出映射:若列表无独立段次 prop,仍由后端默认列包含 stopSegment;前端 planExportColumns 增加 { prop: 'stopSegment', label: '段次' }(或等价别名),以便「按可见列导出」时可选。

6.2 按行填写规则

列 装货行 送货行
计划级(编号、归属公司、跟进状态、计划日期、单据类型、车牌、人员、跟进人、供方、需方、司机、运输/归档状态、ERP、备注等) 重复填计划值 同左
送货时间(deliveryTimes) 空 本送货段日期(单值,不再整单 / 拼接)
装货客户 / 装货地址时间 / 装货地磅 本装货段 空
送货客户 / 送货地址时间 / 送货地磅 空 本送货段
合同(productContract) 本装货段合同号 本送货段合同号
产品名称 / 品牌 / 别名 / 型号规格 / 包装规格 / 单位 本行段产品(CRM 标准名/别名解析保持现有) 同左
计划量 / 实际量 本行段产品 planQty;实际量语义对齐列表 getRowActualQty(装货读 inbound 侧实际量,送货读 outbound 侧实际量) 同左

枚举中文标签(跟进状态、单据类型、运输/归档状态、归属公司等)与文件名/Sheet 标题逻辑保持现有实现。

6.3 字典与别名

  • 单位等 dict_type 列仍走现有 export_rows_to_xlsx_response 字典翻译。
  • _PLAN_EXPORT_COLUMN_ALIASES:如前端 prop 与 field 不一致则补映射;stopSegment 本身可直接作为 prop。

7. 架构与落点

ListExportDropdown(运输计划 / 入库 / 出库)
  → POST 现有 plan 导出 API(带 filters + stockRole + columns)
       → 查询 plans(不变)
       → load_plan_stops_grouped
       → _build_plan_list_export_rows  【改为段次×产品展开】
       → PLAN_EXPORT_COLUMNS(+ stopSegment)
       → xlsx(filename/sheet 按 stockRole)
单元 职责 依赖
_build_plan_list_export_rows 单计划 → 多行 dict;段次与侧栏字段按行写入 stops_by_plan、CRM 别名 map、段产品
PLAN_EXPORT_COLUMNS 列定义含 stopSegment —
_apply_plan_list_export_stop_fields 改为按单侧 stop填写,或拆成 load/delivery 专用 helper,避免整单拼接 单条 load 或单条 delivery
web-admin plan/index.vue(及库存页复用) 导出列清单增加段次;文件名已按 stockRole 共用 PlanListPage
tests/test_pl_plan_list_export.py 一装多送行数、段次文案、侧栏字段空/非空、qty —

建议:段次展开逻辑可抽到 backend/app/services/pl/ 下小函数(如 iter_plan_list_export_segments),便于单测且与 plan.py 路由层解耦;若改动面小,也可先内联在 _build_plan_list_export_rows,以最小必要为准。

8. 实际量语义(与列表对齐)

列表:

  • getRowPlanQty ← 本行 PlanStopProduct.planQty
  • getRowActualQty ← getStopArchiveActualQty:装货用 inboundQty,送货用 outboundQty

导出 MUST 使用同一语义读取段产品字段(后端 stop product 模型上的对应属性),不得再写计划级 PlPlanProduct.actual_qty 作为有段计划的实际量来源。

9. 测试要求

至少覆盖:

  1. 一装多送:1 装货产品 + 2 送货段各 1 产品 → 3 行;段次为 装货/送货 #1/送货 #2;各行 planQty 为段量;装货行 deliveryCustomer 为空、送货行 loadCustomer 为空。
  2. 单装单送:行为与列值正确;中文标签回归(跟进状态、单据类型等)。
  3. 空产品占位:送货段无产品仍有一行,产品/量为空。
  4. legacy:无 stops 时仍按计划级产品展开,段次为空。
  5. 文件名:stockRole → 入库单_ / 出库单_ / 计划列表_(现有用例保留)。
  6. 列过滤:请求 stopSegment 时出现在导出列;别名 normalize 不破坏既有 prop。

10. 验收标准

  • 对 PO20261009016 类一装多送计划:导出 Excel 行数与列表展平行数一致(同筛选、同计划集合前提下)。
  • 打开「入库单.xlsx / 出库单.xlsx / 计划列表.xlsx」均可看到「段次」及按段拆分的计划量/实际量。
  • 计划归档导出行为不变(回归抽测或现有 test_pl_plan_archive_export 全绿)。