凭证规则引擎设计:报销转凭证不只是一张映射表
报销单记的是「谁、因何事、花了多少钱」,凭证要的是「借贷科目、金额、辅助核算维度」。中间那层翻译不是一张表能解决的——同一个费用类型,不同部门、不同项目、不同票种,出来的科目都不一样。真正需要的是一个可配置、可追溯、可试算的规则引擎。
引擎要从单据里推导四类信息
| 推导目标 | 决定因素 | 说明 |
|---|---|---|
| 借方科目 | 费用类型 + 部门 / 项目等维度 | 费用的归属,最核心也最复杂 |
| 贷方科目 | 支付方式 | 由「谁先垫付了钱」决定 |
| 税额拆分 | 票种 + 费用类型可抵扣性 + 税率 | 决定费用金额是否含税 |
| 辅助核算维度 | 单据字段直接透传 | 不需映射,但要校验完整性 |
四类里只有第一类需要复杂规则,但它恰恰是决定整个方案成败的部分。
最容易犯的设计错误:把条件写成固定列
很多实现会把匹配条件设计成规则表上的固定列——一列 dept_id、一列 project_id、一列 invoice_type。这样做的后果是:每增加一个匹配维度就要改表结构、改代码、重新发版。
正确的做法是规则与条件分两张表,条件行数不限,之间是 AND 关系:
这样设计之后,新增一个匹配维度只需在 field 的枚举里增加取值,无需变更表结构,也无需发版。
科目主数据不能省
科目不能只在规则表里存一个编码字符串,必须有独立的主数据表。关键是 required_dims 这个字段——它让系统能在凭证生成阶段就完成维度完整性校验,而不是等推送到总账后被拒绝。
优先级不要让财务手工填
让财务人员手工填写优先级数字,几乎必然出错——他们需要在脑子里比较所有规则的精确程度。更可靠的做法是由系统按条件数量自动计算:条件越多的规则越精确,优先级越高。
在自动计算的基础上保留人工微调入口,用于处理同条件数但需要区分先后的特殊情况。
必须有兜底规则
匹配流程是:筛选候选规则(类型匹配、已启用、单据日期落在生效期间)→ 逐条判断所有条件是否全部满足 → 命中多条按 priority 升序取第一条 → 无命中走兜底规则。
兜底规则不能省。如果没有它,客户新增一个费用类型就会导致凭证生成失败,而此时报销单已经审批通过、无法回退,业务直接卡死。兜底科目(如「待分类费用」)能让业务先流转,财务事后调整。
试算功能同样不能省
引擎必须提供试算能力:输入一张历史报销单,输出完整的推导链条——命中了哪条规则、为什么命中、最终生成什么科目。
| 试算输出项 | 内容示例 |
|---|---|
| 输入摘要 | 费用类型=差旅费-住宿,部门=销售部,票种=专票,税率 6% |
| 候选规则 | 命中 3 条:规则A(800) / 规则B(900) / 兜底(1000) |
| 最终采用 | 规则A —— 条件「费用类型=差旅费」「部门=销售部」均满足 |
| 推导结果 | 借方 660101 销售费用——差旅费 |
缺少这个功能,规则配错只能等到生成凭证报错时才发现,排查成本极高,财务人员也无法自助验证。
历史凭证不能被规则变更影响
半年后财务调整了映射规则,你仍需能解释「这张历史凭证当时为何是这个科目」。这依赖两项机制:规则表的 effective_from / effective_to 生效期间,以及凭证分录上记录 hit_rule_id 规则快照。
凭证生成时按「单据日期」所处期间选取规则版本,而不是按当前时间。
这个场景在开灵科技费控系统里怎么处理
规则与条件分离的数据模型,新增匹配维度不改表结构不发版;优先级按条件数量自动计算并保留人工微调;内置兜底规则与试算工具,财务可自助输入历史单据查看完整推导链条;凭证分录记录命中规则快照,规则变更不影响已生成的历史凭证。
常见问题
不行。同一费用类型在不同部门、不同项目、不同票种下科目都可能不同,一对一映射表无法表达多条件匹配和优先级。
财务需要在脑中比较所有规则的精确程度,规则一多必然出错。按条件数量自动计算更可靠,再保留人工微调处理特例。
会有少量单据进入待分类费用,但这比凭证生成失败、已审批的报销单卡死要好得多。财务可事后集中调整。
不会。规则表有生效期间,凭证分录上记录了当时命中的规则 ID,按单据日期选取规则版本,历史凭证可完整追溯。
