12 KiB
运输计划相关列表导出扩展 · 段次×产品行 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.pyiter_segment_product_slotsformat_stop_segment_label
- Update
plan.py改为从该模块 import(保持_iter_segment_product_slots别名或直接替换调用) - Archive/contract 从该模块 import
对每个 plan:
plan_common = _plan_common_export_fields(...)- 取 stops;legacy 无段 → 旧
_append_legacy_plan_product_rows但补stopSegment=""、planQty/actualQty - 有段:
slots = iter_segment_product_slots(load, deliveries);若stop_kind=="load"只保留 load;=="delivery"只保留 delivery - 每槽位一行:
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_*
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
- 仍
list_plan_contract_gaps(...)得 items(含 plan id) load_plan_stops_groupedfor those ids- 对每个 gap item / plan:展开 slots;填计划级字段 + 段字段 + 产品名 +
gapType - 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
- Spec E1–E6 均有 Task。
- 循环 import 风险已用「抽取
plan_list_export_segments.py」处理。 - 价格字段过滤与 zip 同源行模型已写明。
- Commit 步骤省略(用户未要求不提交)。