Files
shanxia2.0/docs/superpowers/plans/2026-10-15-pl-segment-export-extend.md
T
2026-10-19 18:28:02 +08:00

12 KiB
Raw Blame History

运输计划相关列表导出扩展 · 段次×产品行 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 危化品证、运输归档、合同完善导出对齐列表段次×产品行(方案 A);出库单已完成仅验收。

Architecture: 复用基线 _iter_segment_product_slots / _format_stop_segment_label;归档改造 build_plan_archive_export_rows 为段次多行 + planQty/actualQty/unitPrice/amount;合同完善 export_contract_gaps 对缺口计划做段次展开;证件仅前端注入 stopSegment。

Tech Stack: Python/FastAPI、pytest、Vue 3

Spec: docs/superpowers/specs/2026-10-15-pl-segment-export-extend-design.md

Do NOT git commit unless user explicitly asks.


File map

文件 职责
web-admin/src/views/pl/plan-cert/index.vue 导出列注入 stopSegment
backend/app/api/pl/plan.py PLAN_ARCHIVE_EXPORT_COLUMNS、ARCHIVE_PRICE_EXPORT_FIELDS、CONTRACT_GAP_EXPORT_COLUMNS、export_contract_gaps
backend/app/services/pl_plan_archive_export.py 段次展开替换配对宽表默认路径
backend/tests/test_pl_plan_archive_export.py 归档段次导出用例
backend/tests/test_pl_plan_contract_gap_export.py(新建或扩现有) 合同完善段次导出用例
web-admin/src/views/pl/plan-archive/index.vue 导出可见列映射对齐新 field(stopSegment、去掉旧宽列 prop)
web-admin/src/views/pl/plan-contract/index.vue contractGapExportColumns 对齐新列

Task 1: 危化品证上传 — 注入 stopSegment

Files:

  • Modify: web-admin/src/views/pl/plan-cert/index.vue

  • Step 1: 改 planCertExportColumns

对照运输计划列表写法,在 planNo 后插入 stopSegment:

const planCertExportColumns = computed(() => {
  const skip = new Set([
    'loadCustomer',
    'deliveryCustomer',
    'loadAddressTime',
    'deliveryAddressTime'
  ])
  const cols = visibleColumns.value
    .filter((col) => !skip.has(col))
    .map((col) => PLAN_CERT_EXPORT_COLUMN_FIELD_MAP[col] || col)
  if (!cols.includes('planNo')) cols.unshift('planNo')
  const planNoIdx = cols.indexOf('planNo')
  if (!cols.includes('stopSegment')) {
    cols.splice(planNoIdx + 1, 0, 'stopSegment')
  }
  return cols
})
  • Step 2: 自检 — 读文件确认 splice 存在;无需后端改动。

Task 2: 归档导出 — 失败测试(段次×产品)

Files:

  • Modify: backend/tests/test_pl_plan_archive_export.py

  • Step 1: 新增/改写主用例为段次多行

将 test_build_plan_archive_export_rows_pair_mode_and_single_side 的「默认 stop_kind=None → 1 行配对」改为期望:

# stop_kind=None → 段次展开:装货 1 行 + 送货 1 行 = 2 行
rows = build_plan_archive_export_rows(..., stop_kind=None)
assert len(rows) == 2
assert [r["stopSegment"] for r in rows] == ["装货", "送货 #2"]
assert rows[0]["loadCustomer"] == internal
assert rows[0]["deliveryCustomer"] == ""
assert rows[0]["planQty"] == ...  # load product plan_qty
assert rows[0]["actualQty"] == Decimal("10")  # inbound
assert rows[0]["unitPrice"] == Decimal("100")
assert rows[1]["deliveryCustomer"] == "外部客户"
assert rows[1]["loadCustomer"] == ""
assert rows[1]["actualQty"] == Decimal("9")
# 不得再有 loadPlanQty + deliveryPlanQty 同排有值的配对语义
assert "loadPlanQty" not in rows[0] or rows[0].get("loadPlanQty") is None  # 更好:列定义已无此 field,行 dict 可不含

stop_kind="load" / "delivery" 仍只输出对应侧行(可用 stopSegment 断言,兼容旧 stopKind 可删)。

  • Step 2: 改写依赖配对的测试

  • test_append_paired_product_rows_*:若配对函数保留仅供内部/删除,则删除或改为测新 append helper。

  • 品牌/别名用例改为走 build_plan_archive_export_rows 段次路径断言。

  • Step 3: 跑测确认 RED

cd backend && . venv/bin/activate && PYTHONPATH=. pytest tests/test_pl_plan_archive_export.py -v --tb=line

Task 3: 归档导出 — 实现段次列与行构建

Files:

  • Modify: backend/app/api/pl/plan.py(PLAN_ARCHIVE_EXPORT_COLUMNS、ARCHIVE_PRICE_EXPORT_FIELDS)

  • Modify: backend/app/services/pl_plan_archive_export.py

  • Step 1: 替换 PLAN_ARCHIVE_EXPORT_COLUMNS

PLAN_ARCHIVE_EXPORT_COLUMNS = [
    ExportColumn(title="序号", field="rowNo"),
    ExportColumn(title="计划编号", field="planNo"),
    ExportColumn(title="段次", field="stopSegment"),
    ExportColumn(title="合同编号", field="contractNo"),
    ExportColumn(title="计划日期", field="planDate"),
    ExportColumn(title="归档状态", field="archiveStatus"),
    ExportColumn(title="跟进状态", field="followStatus"),
    ExportColumn(title="单据类型", field="planType"),
    ExportColumn(title="装货客户", field="loadCustomer"),
    ExportColumn(title="装货地址", field="loadAddress"),
    ExportColumn(title="装货时间", field="loadTime"),
    ExportColumn(title="送货客户", field="deliveryCustomer"),
    ExportColumn(title="送货地址", field="deliveryAddress"),
    ExportColumn(title="送货时间", field="deliveryTime"),
    ExportColumn(title="产品名称", field="productName"),
    ExportColumn(title="品牌", field="brandName"),
    ExportColumn(title="别名", field="aliasName"),
    ExportColumn(title="型号/规格", field="model"),
    ExportColumn(title="包装", field="packageSpec"),
    ExportColumn(title="单位", field="unit", dict_type="crm_product_unit"),
    ExportColumn(title="计划量", field="planQty"),
    ExportColumn(title="实际量", field="actualQty"),
    ExportColumn(title="单价(元)", field="unitPrice"),
    ExportColumn(title="金额(元)", field="amount"),
    ExportColumn(title="车牌号", field="licensePlate"),
    ExportColumn(title="运费单位", field="freightType"),
    ExportColumn(title="运费单价", field="freightPrice"),
    ExportColumn(title="运费金额", field="freightAmount"),
    ExportColumn(title="ERP", field="erpEntered"),
    ExportColumn(title="备注", field="remark"),
]

ARCHIVE_PRICE_EXPORT_FIELDS 保留 unitPrice/amount/freightPrice/freightAmount;可移除已删除的 loadUnitPrice 等(过滤仍安全)。

  • Step 2: 重写 build_plan_archive_export_rows 默认路径

伪逻辑:

from app.api.pl.plan import _iter_segment_product_slots, _format_stop_segment_label
# 避免循环 import:把 _iter_segment_product_slots / _format_stop_segment_label
# 抽到 app/services/pl/plan_list_export_segments.py 更干净(推荐本 Task 内抽取)

推荐抽取(若 plan.py ↔ pl_plan_archive_export.py 循环导入):

  • Create: backend/app/services/pl/plan_list_export_segments.py
    • iter_segment_product_slots
    • format_stop_segment_label
  • Update plan.py 改为从该模块 import(保持 _iter_segment_product_slots 别名或直接替换调用)
  • Archive/contract 从该模块 import

对每个 plan:

  1. plan_common = _plan_common_export_fields(...)
  2. 取 stops;legacy 无段 → 旧 _append_legacy_plan_product_rows 但补 stopSegment=""、planQty/actualQty
  3. 有段:slots = iter_segment_product_slots(load, deliveries);若 stop_kind=="load" 只保留 load;=="delivery" 只保留 delivery
  4. 每槽位一行:
    • stopSegment = format_stop_segment_label(kind, seq)
    • 装货行:填 loadCustomer/Address/Time,delivery 侧空;送货行相反
    • contractNo = 本段合同 label(format_stop_contract_export_label)
    • 产品 identity + planQty/actualQty/unitPrice/amount 用现有 _get_stop_archive_*
  5. stop_customer_keywords:保留——装或送客户匹配即保留该行(按行侧客户过滤)

可删除或停用 _append_paired_product_rows(若测试不再引用)。

  • Step 3: 测试全绿
cd backend && . venv/bin/activate && PYTHONPATH=. pytest tests/test_pl_plan_archive_export.py tests/test_pl_plan_list_export.py -v --tb=line
  • Step 4: 前端归档导出列

plan-archive/index.vue:导出 columns 映射增加 stopSegment;若前端曾请求 loadPlanQty 等,改为 planQty/actualQty/unitPrice/amount(与列表 prop 一致)。找到构建 export columns 的 computed,对齐新 PLAN_ARCHIVE_EXPORT_COLUMNS。


Task 4: 合同完善导出 — 测试 + 实现

Files:

  • Create/Modify: backend/tests/test_pl_plan_contract_gap_export.py(或扩现有相关测)

  • Modify: backend/app/api/pl/plan.py(CONTRACT_GAP_EXPORT_COLUMNS、export_contract_gaps)

  • Modify: web-admin/src/views/pl/plan-contract/index.vue

  • Step 1: 写失败测试

对 export_contract_gaps 的行构建逻辑抽函数更易测,例如:

def build_contract_gap_export_rows(db, tenant_no, gap_items, stops_by_plan) -> list[dict]:
    ...

测试:一计划,装货缺合同 + 两送货段有产品 → 多行;装货行 gapType=="缺供方合同"(或与现文案一致的短标签);送货行若无缺口则 gapType=="";stopSegment 正确。

缺口判定复用 stop_contract_gap(customer_name, len(contracts))(pl_plan_contract_gap.py)。

  • Step 2: 更新列定义
CONTRACT_GAP_EXPORT_COLUMNS = [
    ExportColumn(title="计划编号", field="planNo"),
    ExportColumn(title="段次", field="stopSegment"),
    ExportColumn(title="计划日期", field="planDate"),
    ExportColumn(title="送货时间", field="deliveryTimes"),
    ExportColumn(title="单据类型", field="planType"),
    ExportColumn(title="跟进状态", field="followStatus"),
    ExportColumn(title="装货客户", field="loadCustomer"),
    ExportColumn(title="送货客户", field="deliveryCustomer"),
    ExportColumn(title="产品名称", field="productName"),
    ExportColumn(title="供方", field="supplierName"),
    ExportColumn(title="需方", field="customerName"),
    ExportColumn(title="缺口类型", field="gapType"),
]
  • Step 3: 实现 export_contract_gaps
  1. 仍 list_plan_contract_gaps(...) 得 items(含 plan id)
  2. load_plan_stops_grouped for those ids
  3. 对每个 gap item / plan:展开 slots;填计划级字段 + 段字段 + 产品名 + gapType
  4. followStatus/planType 中文标签保持现有
  • Step 4: 前端 contractGapExportColumns
const contractGapExportColumns = [
  'planNo',
  'stopSegment',
  'planDate',
  'deliveryTimes',
  'planType',
  'followStatus',
  'loadCustomer',
  'deliveryCustomer',
  'productName',
  'supplierName',
  'customerName',
  'gapType'
]
  • Step 5: 跑测
cd backend && . venv/bin/activate && PYTHONPATH=. pytest tests/test_pl_plan_contract_gap_export.py tests/test_pl_plan_list_export.py tests/test_pl_plan_archive_export.py -v --tb=line

Task 5: 收尾

  • Step 1: 全量相关测
cd backend && . venv/bin/activate && PYTHONPATH=. pytest tests/test_pl_plan_list_export.py tests/test_pl_plan_archive_export.py tests/test_pl_plan_contract_gap_export.py -v
  • Step 2: Spec 对照 E1–E6 均有落点;更新 graphify(改过的 backend/web-admin)

  • Step 3: 验收清单(人工)

  • 证件 / 归档 / 合同完善 / 出库单各导出一次一装多送计划


Self-review

  1. Spec E1–E6 均有 Task。
  2. 循环 import 风险已用「抽取 plan_list_export_segments.py」处理。
  3. 价格字段过滤与 zip 同源行模型已写明。
  4. Commit 步骤省略(用户未要求不提交)。