VAT Trial Calc(增值税试算)
依据本仓库演示页 /demo/local-crm-tools/tax-calc 的自研逻辑(src/utils/localCrmTools/taxCalc.ts → calcVat),指导 Agent 实现本地、无接口的增值税价税试算。
本 Skill 经递归自我改造三轮加固(见文末),在保留原公式语义的前提下补齐校验、舍入一致性与回归样例。
源逻辑分析(必须先懂再写)
输入
| 参数 | 含义 | UI 默认 |
|---|---|---|
amount |
金额(≥0,两位小数) | 10000 |
ratePercent |
税率百分数,如 6 表示 6% |
可选 1 / 3 / 6 / 9 / 13 |
mode |
exclusive 不含税 · inclusive 含税 |
exclusive |
内部
rate = max(0, ratePercent) / 100
分支 A:不含税计价(exclusive)
已知:不含税金额 = amount
税额 = round2(amount × rate)
价税合计 = round2(amount + 税额)
不含税金额 = round2(amount)
公式文案:税额 = 不含税金额 × 税率;价税合计 = 不含税 + 税额
分支 B:含税计价(inclusive)
已知:价税合计 = amount
不含税金额 = round2(amount ÷ (1 + rate))
税额 = round2(amount − 不含税金额)
价税合计 = round2(amount)
公式文案:不含税 = 含税 ÷ (1+税率);税额 = 含税 − 不含税
关键不变量
- 恒等(在 round2 约定下)
- exclusive:amountIncl ≈ amountExcl + tax
- inclusive:tax ≈ amountIncl - amountExcl(税额用差式,避免excl×rate再 round 导致 0.01 偏差) - 税率 0:税额为 0;两种 mode 下 excl/incl 均等于输入金额(round2 后)。
- 本地试算:不申报、不抵扣进项;仅开票前自检 / 商务测算。
输出 TaxResult
{
amountExcl: number; // 不含税
tax: number; // 税额
amountIncl: number; // 价税合计
rate: number; // 回显百分数
formula: string; // 给人看的公式说明
}
硬约束
- 公式不得改语义:与上节分支 A/B 一致;不得擅自改成「税额 = 含税 × 税率」等错误式。
- 金额舍入统一
round2:+(n.toFixed(2))或等价银行家以外的常规四舍五入到分(与源码一致用toFixed(2))。 - inclusive 税额必须用差式
含税 - 不含税,与源码一致。 - 非法输入:
amount < 0、NaN、非有限数 → 拒绝计算并提示;ratePercent < 0→ 按 0(与Math.max(0, rate)一致)或显式校验(推荐显式校验并提示)。 - 展示公式:结果区必须展示
formula,便于业务核对。 - 变更输入即时重算(
@change/ watch),与演示页一致。
标准工作流
VAT Trial Calc
- [ ] 复述 mode 与公式(exclusive / inclusive)
- [ ] 实现 calcVat + round2 + 输入校验
- [ ] 常用税率档位 1/3/6/9/13(可扩展)
- [ ] UI:金额、税率、模式、四字段结果+公式
- [ ] 跑 fixtures(见 reference)
- [ ] 对照源文件 taxCalc.ts 做一次差分确认
推荐实现骨架
export type TaxMode = 'exclusive' | 'inclusive';
export type TaxResult = {
amountExcl: number;
tax: number;
amountIncl: number;
rate: number;
formula: string;
};
const round2 = (n: number) => +n.toFixed(2);
export function calcVat(amount: number, ratePercent: number, mode: TaxMode): TaxResult {
if (!Number.isFinite(amount) || amount < 0) {
throw new Error('金额必须为 ≥ 0 的有限数字');
}
if (!Number.isFinite(ratePercent) || ratePercent < 0) {
throw new Error('税率必须为 ≥ 0 的有限数字');
}
const rate = ratePercent / 100;
if (mode === 'exclusive') {
const tax = round2(amount * rate);
return {
amountExcl: round2(amount),
tax,
amountIncl: round2(amount + tax),
rate: ratePercent,
formula: '税额 = 不含税金额 × 税率;价税合计 = 不含税 + 税额',
};
}
const excl = round2(amount / (1 + rate));
const tax = round2(amount - excl);
return {
amountExcl: excl,
tax,
amountIncl: round2(amount),
rate: ratePercent,
formula: '不含税 = 含税 ÷ (1+税率);税额 = 含税 − 不含税',
};
}
本仓库已有实现时可直接复用:
import { calcVat } from '@/utils/localCrmTools/taxCalc'
新页面优先复用,避免分叉;若加固校验,在包装函数中 throw/Toast,勿改坏旧调用方默认行为(或同步改源并加注释)。
递归自我改造记录(本 Skill 内建)
R1 — 忠实抽取
从 taxCalc.ts + 演示 UI 还原双模式公式与 toFixed(2) 舍入。
R2 — 发现缺口并补强
- 源码未拦截
NaN/ 负数 → Skill 要求显式校验 - 仅差式算税可减少 inclusive 下 0.01 误差 → 写入硬约束
- 税率档位与 UI 默认写入文档,避免 Agent 臆造 16%/17% 旧档而不说明
R3 — 可回归
- 增加 fixtures(reference)
- 要求交付前用样例手算核对
- 明确「非申报级、无进项抵扣」边界,防止做成税务引擎误导
Agent 实现后若发现新边界,应追加到 reference.md fixtures,而不是悄悄改公式。
反模式
| 禁止 | 原因 |
|---|---|
| 含税金额 × 税率 = 税额 | 数学错误(多算) |
不含税用 amount × (1-rate) |
与增值税价税分离不符 |
| 混用 round 位数(有的 2 有的 4) | 对账失败 |
| 做成进项/销项/申报表 | 超出试算范围 |
| 抄财务软件闭源规则却声称同源 | 本 Skill 仅对齐仓库自研试算 |
Agent 交付检查
- [ ] exclusive / inclusive 两路公式正确
- [ ] 税额舍入与源语义一致(含税用差式)
- [ ] 非法输入有提示
- [ ] 结果含 formula
- [ ] fixtures 通过
- [ ] 优先复用
taxCalc.ts
附加资源
- 样例与对照表:reference.md
- Vue 模板:templates/vue-vat-trial.md
- 逻辑对照笔记:analysis.md