Files
shanxia2.0/docs/superpowers/plans/2026-08-16-contract-settlement-method-options.md
T
2026-08-17 09:22:31 +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: 为合同结算方式增加按量结算/月结/季结/其他四项,并让第五条条款按「条款驱动两项 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: 手工验证(需在浏览器确认)
  1. 选月结 → 打开打印 → 第五条含「结算方式:月结。」
  2. 选手改第五条并保存 → 再打开仍为改后文案
  3. 选款到发货 → 仍为款到发货默认句
  4. 生成 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