从分类账里找出借贷不平
目标
从复式记账总账入手,将一张借贷不平的凭证定位到具体一行分录;在不删除原始记录的前提下更正它,再使余额表与总账保持一致。
为什么重要
在银行系统中,余额不是存储起来的值,而是从总账计算出的值。出于性能考虑,每个科目会同时保存余额列,但它只是总账的副本,而不是原始依据。
这一区分在调查中至关重要,原因如下。单个余额只是一个数字,仅凭这个数字无法判断其对错。复式记账总账则会自我校验——每笔交易都同时记录在借方和贷方,因此整个总账的借方合计与贷方合计必须相等;一旦这一恒等式被破坏,本身就是事故信号。随后还可以按凭证进一步缩小范围。
本次事件如下。客户的运维负责人联系称“结算余额不一致”。三天的总账中有两处偏差,但性质截然相反:一处是总账错误而余额表正确,另一处则是总账正确而余额表错误。如果不区分二者就直接处理,修正方向会恰好相反。
更正方式也与一般服务不同。不能通过 UPDATE 修改错误凭证。要先录入一张像镜像一样反转原凭证的冲销凭证来抵消它,再重新录入正确凭证。总账中必须并列保留这三张凭证,才能在两个月后的审计中解释当时发生了什么。
步骤
- 创建并运行
/root/bank/gen_ledger.py,生成/root/bank/ledger.db。其中包含 10 个科目、201 张凭证和 430 行分录。 - 将按科目汇总的试算平衡表输出到
/root/bank/trial_balance.csv。表头为code,name,debit,credit,balance,其中balance是借方合计减去贷方合计。一次也没有发生变动的科目也要保留。 - 在
/root/bank/tb_totals.txt中写四行。debit_total=、credit_total=、gap=来自总账,cached_sum=来自accounts.cached_balance的合计。 - 按凭证比较借方合计和贷方合计,找出不平的凭证,并在
/root/bank/broken_entry.txt中写入entry_id=、debit=、credit=、gap=。将该凭证的分录原样输出到/root/bank/broken_lines.csv,表头为line_id,entry_id,account_code,dc,amount。 - 在
/root/bank/balance_diff.csv中只保留余额表与总账汇总值不同的科目,表头为code,cached,ledger,gap。gap是cached减去ledger。 - 不要改动原凭证,录入两张更正凭证。冲销凭证登记为
JE-20260826-0134-R,重新记账凭证登记为JE-20260826-0134-C,并同时写入journal和journal_line。 - 以总账汇总值重新计算并覆盖
accounts.cached_balance,然后在/root/bank/recheck.txt中写七行:debit_total=、credit_total=、gap=、unbalanced_entries=、original_credit=、cached_sum=、balance_diff_rows=。 - 在
/root/bank/ledger_report.md中按## 무엇이 틀렸나、## 어떻게 찾았나、## 어떻게 고쳤나、## 잔액 표는 왜 못 잡았나、## 재발 방지五个章节编写报告。
参考
- 使用
sqlite3 -readonly <파일> "<쿼리>"读取,可以防止调查过程中误写数据。 - 输出 CSV 时,先通过标准输入传入
.headers on和.mode csv:sqlite3 -readonly ledger.db > out.csv <<'SQL' ... SQL。 - 带符号余额可以通过
SUM(CASE dc WHEN 'D' THEN amount ELSE -amount END)一次计算完成。 - 常见错误 1:在第 2 步从表中排除没有发生变动的科目。这样就无法区分“没有变动”和“科目不存在”。
- 常见错误 2:在第 6 步将冲销凭证金额写成正确金额。必须按原记录金额反转,才能准确抵消错误。
- 常见错误 3:在第 7 步期望按凭证统计的不平数量变为 0。原凭证与冲销凭证成对保留下来才是正常状态。
创建总账快照
创建并运行 /root/bank/gen_ledger.py,生成 /root/bank/ledger.db。其中包含 10 个科目、201 张凭证和 430 行分录。
先创建 /root/bank,然后在其中使用 python3 创建 sqlite 数据库。数据库包含 accounts、journal、journal_line 三张表,分录共有 430 行。
编制试算平衡表
将按科目汇总的试算平衡表输出到 /root/bank/trial_balance.csv。表头为 code,name,debit,credit,balance,其中 balance 是借方合计减去贷方合计。一次也没有发生变动的科目也要保留。
分别计算每个科目的借方金额合计和贷方金额合计,balance 是借方合计减去贷方合计。这三天中一次也没有发生变动的科目也必须留在表中,因此请使用 LEFT JOIN。
分别统计总账与余额表
在 /root/bank/tb_totals.txt 中写四行。debit_total=、credit_total=、gap= 来自总账,cached_sum= 来自 accounts.cached_balance 的合计。
共有四个数字。前三个来自 journal_line,最后一个来自 accounts.cached_balance。本步骤要发现的是两张表的偏差大小不同。
定位到一张凭证和一行分录
按凭证比较借方合计和贷方合计,找出不平的凭证,并在 /root/bank/broken_entry.txt 中写入 entry_id=、debit=、credit=、gap=。将该凭证的分录原样输出到 /root/bank/broken_lines.csv,表头为 line_id,entry_id,account_code,dc,amount。
按凭证分组,比较借方合计和贷方合计。在 HAVING 子句中加入差值不为 0 的条件后,只会剩下一张凭证。不要修改分录值,按现状原样输出。
核对余额表与总账汇总
在 /root/bank/balance_diff.csv 中只保留余额表与总账汇总值不同的科目,表头为 code,cached,ledger,gap。gap 是 cached 减去 ledger。
按科目并列放置 accounts.cached_balance 和总账汇总值,只保留不同的记录。gap 是 cached 减去 ledger。会得到两行,但产生偏差的原因截然相反。
通过冲销和重新记账进行更正
不要改动原凭证,录入两张更正凭证。冲销凭证登记为 JE-20260826-0134-R,重新记账凭证登记为 JE-20260826-0134-C,并同时写入 journal 和 journal_line。
原凭证一行也不能改动。冲销凭证只交换原凭证的借方和贷方,金额则使用记录中的原值。随后再以正确金额录入一张凭证。
根据总账重建余额表
以总账汇总值重新计算并覆盖 accounts.cached_balance,然后在 /root/bank/recheck.txt 中写七行:debit_total=、credit_total=、gap=、unbalanced_entries=、original_credit=、cached_sum=、balance_diff_rows=。
出现偏差时,始终修正副本。用总账汇总值覆盖 accounts.cached_balance。不要期望按凭证统计的不平数量变为 0——冲销凭证会与原凭证成对保留。
编写总账一致性报告
在 /root/bank/ledger_report.md 中按 ## 무엇이 틀렸나、## 어떻게 찾았나、## 어떻게 고쳤나、## 잔액 표는 왜 못 잡았나、## 재발 방지 五个章节编写报告。
需要五个章节。在“哪里出错了”一节写明凭证编号和差额;在“如何修正”一节写明冲销凭证编号;在“余额表为什么没发现”一节写明只有余额不一致的科目编号和金额。