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: 为合同结算方式增加按量结算/月结/季结/其他四项,并让第五条条款按「条款驱动两项 vs 新四项约定句」规则解析,前后端一致。
Architecture: 字典扩 4 条;抽出前后端 defaultClause(settlementMethod);保留 {{SETTLEMENT_METHOD_CONTENT}} + settlement_clause_drafts[method];新四项默认文案为「请根据双方约定填写结算方式:{label}。供方开具13%增值税专用发票,无异议后,以现汇方式月结。」
Tech Stack: MySQL 字典、Python 3.9 / pytest、Vue 3 / TypeScript
Spec: docs/superpowers/specs/2026-08-16-contract-settlement-method-options-design.md
File map
| 文件 | 职责 |
|---|---|
backend/scripts/migrate_20260816_crm_settlement_method_options.sql |
幂等插入 4 条字典 |
backend/app/services/crm_settlement_clause.py |
后端 defaultClause 纯函数 |
backend/app/crud/crm/contract.py |
save_template 调用纯函数 |
backend/tests/test_crm_settlement_clause.py |
默认文案单测 |
web-admin/src/views/crm/contract/settlementClause.ts |
前端解析/默认文案 |
web-admin/src/views/crm/contract/components/ContractDetailDialog.vue |
改用共享函数 |
web-admin/src/views/crm/contract/detail.vue |
改用共享函数(若仍保留同逻辑) |
Task 1: 字典 SQL 迁移
Files:
-
Create:
backend/scripts/migrate_20260816_crm_settlement_method_options.sql -
Step 1: 写入幂等 INSERT
-- backend/scripts/migrate_20260816_crm_settlement_method_options.sql
-- 合同结算方式:按量结算 / 月结 / 季结 / 其他
-- 可重复执行(幂等)
INSERT INTO `sys_dict_data` (
`id`, `dict_label`, `dict_value`, `tag_type`, `dict_type`, `tenant_no`,
`order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`,
`update_by`, `update_time`, `del_flag`
) VALUES
(
'SXDC20260816SETTLE00000000000001', '按量结算', 'volume_settlement', '',
'crm_settlement_method', 'shanxia2.0', 2, '', '1', '', NOW(), NULL, NOW(), 0
),
(
'SXDC20260816SETTLE00000000000002', '月结', 'monthly_settlement', '',
'crm_settlement_method', 'shanxia2.0', 3, '', '1', '', NOW(), NULL, NOW(), 0
),
(
'SXDC20260816SETTLE00000000000003', '季结', 'quarterly_settlement', '',
'crm_settlement_method', 'shanxia2.0', 4, '', '1', '', NOW(), NULL, NOW(), 0
),
(
'SXDC20260816SETTLE00000000000004', '其他', 'other_settlement', '',
'crm_settlement_method', 'shanxia2.0', 5, '', '1', '', NOW(), NULL, NOW(), 0
)
ON DUPLICATE KEY UPDATE
`dict_label` = VALUES(`dict_label`),
`dict_value` = VALUES(`dict_value`),
`dict_type` = VALUES(`dict_type`),
`tenant_no` = VALUES(`tenant_no`),
`order_num` = VALUES(`order_num`),
`del_flag` = 0,
`update_time` = NOW();
- Step 2: 在目标库执行该 SQL,刷新管理端字典缓存后确认下拉出现 6 项
Task 2: 后端 defaultClause 纯函数 + 单测(TDD)
Files:
-
Create:
backend/app/services/crm_settlement_clause.py -
Create:
backend/tests/test_crm_settlement_clause.py -
Step 1: 先写失败单测
# backend/tests/test_crm_settlement_clause.py
from app.services.crm_settlement_clause import default_settlement_clause
def test_advance_payment_clause():
assert "款到发货" in default_settlement_clause("advance_payment")
def test_arrive_payment_clause():
assert "货到付款" in default_settlement_clause("arrive_payment")
def test_monthly_settlement_clause_uses_label():
text = default_settlement_clause("monthly_settlement")
assert text.startswith("请根据双方约定填写结算方式:月结。")
assert "13%增值税专用发票" in text
def test_volume_quarterly_other_labels():
assert "按量结算" in default_settlement_clause("volume_settlement")
assert "季结" in default_settlement_clause("quarterly_settlement")
assert "其他" in default_settlement_clause("other_settlement")
def test_unknown_falls_back_to_arrive():
assert default_settlement_clause("weird") == default_settlement_clause("arrive_payment")
def test_empty_falls_back_to_arrive():
assert default_settlement_clause("") == default_settlement_clause("arrive_payment")
assert default_settlement_clause(None) == default_settlement_clause("arrive_payment")
- Step 2: 运行确认失败
cd backend && source venv/bin/activate && pytest tests/test_crm_settlement_clause.py -v
Expected: FAIL(模块不存在)
- Step 3: 实现纯函数
# backend/app/services/crm_settlement_clause.py
from __future__ import annotations
from typing import Optional
ADVANCE_PAYMENT = "advance_payment"
ARRIVE_PAYMENT = "arrive_payment"
CLAUSE_LABELS = {
"volume_settlement": "按量结算",
"monthly_settlement": "月结",
"quarterly_settlement": "季结",
"other_settlement": "其他",
}
ADVANCE_CLAUSE = (
"款到发货。需方付款后安排发货,需方收到货物后,供方开具13%增值税专用发票。"
)
ARRIVE_CLAUSE = (
"货到付款,需方收到供方货物后,供方开具13%增值税专用发票,无异议后,以现汇方式月结。"
)
def default_settlement_clause(settlement_method: Optional[str]) -> str:
method = (settlement_method or "").strip() or ARRIVE_PAYMENT
if method == ADVANCE_PAYMENT:
return ADVANCE_CLAUSE
if method in CLAUSE_LABELS:
label = CLAUSE_LABELS[method]
return (
f"请根据双方约定填写结算方式:{label}。"
"供方开具13%增值税专用发票,无异议后,以现汇方式月结。"
)
# arrive_payment 与未知值
return ARRIVE_CLAUSE
- Step 4: 再跑单测,期望全部 PASS
cd backend && source venv/bin/activate && pytest tests/test_crm_settlement_clause.py -v
Task 3: 后端 save_template 改用纯函数
Files:
-
Modify:
backend/app/crud/crm/contract.py(save_template内空草稿兜底,约 1661–1669 行) -
Step 1: 增加 import
from app.services.crm_settlement_clause import default_settlement_clause
- Step 2: 替换空草稿兜底
将:
method = (entity.settlement_method or "").strip() or "arrive_payment"
default_clause = (
"款到发货。需方付款后安排发货,需方收到货物后,供方开具13%增值税专用发票。"
if method == "advance_payment"
else "货到付款,需方收到供方货物后,供方开具13%增值税专用发票,无异议后,以现汇方式月结。"
)
drafts = json.dumps({method: default_clause}, ensure_ascii=False)
改为:
method = (entity.settlement_method or "").strip() or "arrive_payment"
drafts = json.dumps(
{method: default_settlement_clause(method)},
ensure_ascii=False,
)
Task 4: 前端 settlementClause.ts
Files:
-
Create:
web-admin/src/views/crm/contract/settlementClause.ts -
Step 1: 新增共享模块
/** 与后端 app.services.crm_settlement_clause 保持一致 */
export const SETTLEMENT_ADVANCE = 'advance_payment'
export const SETTLEMENT_ARRIVE = 'arrive_payment'
export const SETTLEMENT_CLAUSE_LABELS: Record<string, string> = {
volume_settlement: '按量结算',
monthly_settlement: '月结',
quarterly_settlement: '季结',
other_settlement: '其他'
}
const ADVANCE_CLAUSE =
'款到发货。需方付款后安排发货,需方收到货物后,供方开具13%增值税专用发票。'
const ARRIVE_CLAUSE =
'货到付款,需方收到供方货物后,供方开具13%增值税专用发票,无异议后,以现汇方式月结。'
export function defaultSettlementClause(
settlementMethod?: string | null,
labelResolver?: (method: string) => string | undefined
): string {
const method = (settlementMethod || '').trim() || SETTLEMENT_ARRIVE
if (method === SETTLEMENT_ADVANCE) return ADVANCE_CLAUSE
if (method in SETTLEMENT_CLAUSE_LABELS) {
const label =
(labelResolver && labelResolver(method)) || SETTLEMENT_CLAUSE_LABELS[method]
return `请根据双方约定填写结算方式:${label}。供方开具13%增值税专用发票,无异议后,以现汇方式月结。`
}
return ARRIVE_CLAUSE
}
export function parseSettlementClauseDrafts(raw?: string | null): Record<string, string> {
if (!raw) return {}
try {
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
const drafts: Record<string, string> = {}
Object.entries(parsed).forEach(([key, value]) => {
if (typeof value === 'string' && value.trim()) {
drafts[key] = value.trim()
}
})
return drafts
} catch {
return {}
}
}
export function resolveSettlementClauseContent(options: {
settlementMethod?: string | null
settlementClauseDrafts?: string | null
labelResolver?: (method: string) => string | undefined
}): string {
const method = (options.settlementMethod || '').trim() || SETTLEMENT_ARRIVE
const drafts = parseSettlementClauseDrafts(options.settlementClauseDrafts)
return drafts[method] || defaultSettlementClause(method, options.labelResolver)
}
Task 5: ContractDetailDialog.vue 改用共享函数
Files:
-
Modify:
web-admin/src/views/crm/contract/components/ContractDetailDialog.vue -
Step 1: import
import {
parseSettlementClauseDrafts,
resolveSettlementClauseContent
} from '../settlementClause'
- Step 2: 删除组件内重复的
getDefaultSettlementClauseContent/ 内层parseSettlementClauseDrafts(replacePlaceholders内与文件级各一处),改为调用共享函数
在 replacePlaceholders 内,第五条取值改为:
const getSettlementClauseContent = (): string =>
resolveSettlementClauseContent({
settlementMethod: contractData.settlementMethod,
settlementClauseDrafts: contractData.settlementClauseDrafts,
labelResolver: (method) => dictStore.getLabel('crm_settlement_method', method) || undefined
})
文件级若仍有 parseSettlementClauseDrafts 供保存草稿使用,改为直接使用 import 的同名函数,删除本地实现。
- Step 3: 手工验证(需在浏览器确认)
- 选月结 → 打开打印 → 第五条含「结算方式:月结。」
- 选手改第五条并保存 → 再打开仍为改后文案
- 选款到发货 → 仍为款到发货默认句
- 生成 PDF 与弹窗一致
Task 6: detail.vue 对齐(若仍含同逻辑)
Files:
-
Modify:
web-admin/src/views/crm/contract/detail.vue -
Step 1: 同样 import
resolveSettlementClauseContent/parseSettlementClauseDrafts,删除本地重复实现,第五条解析走共享函数(与 Task 5 相同规则)
若该页已废弃不再进入打印路径,仍建议对齐,避免双份逻辑分叉。
Task 7: 验收与图谱
- Step 1: 再跑
cd backend && source venv/bin/activate && pytest tests/test_crm_settlement_clause.py -v
Expected: PASS
-
Step 2: 对照 spec「验收要点」手工走一遍 6 项结算方式(需在浏览器确认)
-
Step 3: 更新图谱
./scripts/update-graph.sh backend
./scripts/update-graph.sh frontend
(或按仓库惯例 graphify update .)
Spec coverage
| Spec 要求 | Task |
|---|---|
| 字典 4 项 | Task 1 |
| defaultClause 六种 + 未知回退 | Task 2–3 |
| 前端打印/PDF 解析 | Task 4–5 |
| detail 双份逻辑对齐 | Task 6 |
| 草稿按 method 保存不覆盖 | 现有 save 路径 + Task 5 手工验 |
| 验收与图谱 | Task 7 |