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.
| 文件 | 职责 |
|---|---|
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 对齐新列 |
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
})
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
Files:
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 等(过滤仍安全)。
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 循环导入):
backend/app/services/pl/plan_list_export_segments.py
iter_segment_product_slotsformat_stop_segment_labelplan.py 改为从该模块 import(保持 _iter_segment_product_slots 别名或直接替换调用)对每个 plan:
plan_common = _plan_common_export_fields(...)_append_legacy_plan_product_rows 但补 stopSegment=""、planQty/actualQtyslots = iter_segment_product_slots(load, deliveries);若 stop_kind=="load" 只保留 load;=="delivery" 只保留 deliverystopSegment = format_stop_segment_label(kind, seq)contractNo = 本段合同 label(format_stop_contract_export_label)planQty/actualQty/unitPrice/amount 用现有 _get_stop_archive_*stop_customer_keywords:保留——装或送客户匹配即保留该行(按行侧客户过滤)可删除或停用 _append_paired_product_rows(若测试不再引用)。
cd backend && . venv/bin/activate && PYTHONPATH=. pytest tests/test_pl_plan_archive_export.py tests/test_pl_plan_list_export.py -v --tb=line
plan-archive/index.vue:导出 columns 映射增加 stopSegment;若前端曾请求 loadPlanQty 等,改为 planQty/actualQty/unitPrice/amount(与列表 prop 一致)。找到构建 export columns 的 computed,对齐新 PLAN_ARCHIVE_EXPORT_COLUMNS。
Files:
backend/tests/test_pl_plan_contract_gap_export.py(或扩现有相关测)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)。
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"),
]
export_contract_gapslist_plan_contract_gaps(...) 得 items(含 plan id)load_plan_stops_grouped for those idsgapTypecontractGapExportColumnsconst contractGapExportColumns = [
'planNo',
'stopSegment',
'planDate',
'deliveryTimes',
'planType',
'followStatus',
'loadCustomer',
'deliveryCustomer',
'productName',
'supplierName',
'customerName',
'gapType'
]
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
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: 验收清单(人工)
证件 / 归档 / 合同完善 / 出库单各导出一次一装多送计划
plan_list_export_segments.py」处理。