# 运输计划 / 库存列表导出 · 段次×产品行 — 设计文档 > **日期**: 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`)展开: ```text 装货段.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. 架构与落点 ```text 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` 全绿)。