总账与业务单据一体化设计

ERP 系统最有价值的属性只有一条:每一张业务单据——销售发货、采购入库、收付款——不经过任何人二次录入就能落进总账。这条管线一旦打通,账永远是最新的,月末结账不再是救火。本文介绍我们在 Web 版 ERP 项目中使用的单据到凭证设计方案。

核心思想:业务单据不认识会计

把会计逻辑挡在业务模块之外。销售发货模块应该只输出事实——"100 件 A 物料以单价 P 从 W 仓发出给客户 C"——由专门的过账引擎决定这对总账意味着什么。这个分离在模块化单体中是自然结果:会计规则可以随税率、科目映射的变化而演进,业务操作代码一行不动。

过账管线

  1. 业务事件——单据在自己的模块中被确认(过账)。确认是一次状态迁移而不是一次保存:已确认单据即被冻结。
  2. 过账请求——模块发布过账请求,包含单据类型、主键、金额和维度(组织、部门、项目、币种)。
  3. 模板解析——过账引擎按单据类型解析过账模板。模板把单据字段映射为借贷规则:"发货 → 借主营业务成本,贷库存商品;应收 → 借应收账款,贷主营业务收入"。
  4. 凭证生成——引擎生成一张平衡的凭证(借贷相等作为硬不变量),折算为记账本位币,每一行都挂上来源单据引用。
  5. 写入总账——数据库允许时凭证与单据确认同事务写入;模块解耦时通过 outbox 模式异步投递。

过账模板即配置

绝不把科目编码写死。过账模板是一张小型规则表,通常由实施顾问维护:每种单据类型、每种业务场景(仓间调拨、委托代销、退货)声明借什么、贷什么、金额从哪个单据字段取。模板带版本和生效日期——下个季度起的税率变化是新模板版本,不是一次代码上线。

保持模板简单:一行行地列科目、方向、金额表达式、维度映射。模板里一旦出现循环和条件分支,顾问就维护不了了——那是你用一种很烂的方式发明了第二门编程语言。

幂等:过账必须可以安全重试

任何 Web 系统里重试都是必然发生的。过账引擎必须幂等:同一单据过账两次只产生一张凭证,而不是两张。两个机制让这件事落地:

  • 凭证自然键——在 (来源单据类型, 来源单据ID) 上加唯一约束,重复插入直接报错。
  • Outbox 模式——过账请求和单据确认写在同一个事务里落 outbox 表,再由 relay 异步投递。relay 崩溃就重读 outbox;唯一键负责去重。

冲销遵循同一规则:取消单据生成冲销凭证,绝不在原地修改。总账只追加,更正走新分录。

期间控制与结账

两道护栏保证总账不疯:

  • 期间状态——每个会计期间分为打开、软关闭(禁止人工录入,允许引擎补记)、关闭(什么都不行)。过账引擎写入前检查期间状态。
  • 子模块对账——应收应付子模块余额、库存估值与总账统驭科目持续对账,任何漂移都上监控大屏,而不是等到审计才被发现。

用户看到什么

用户体验很简单:确认单据,点"查看凭证"看生成的分录;出错了点"过账日志"。绝不隐藏失败——单据已确认但过账失败是最糟糕的状态,UI 必须立即暴露它,并在问题解决前阻止期间关闭。这套管线在前端的呈现方式见企业级 Web 界面模式;生产侧同一管线的物料成本去向,见BOM 展开与 MRP 运算实现。

结语

把总账当作业务事件的消费者,用声明式模板生成凭证,让过账幂等且只追加,用对账大屏代替 Excel 结账。做到这些,财务不再是 ERP 的瓶颈——它变成永远正确的那部分。