Beancount 中的余额断言

Martin Blais,2014 年 7 月

http://furius.ca/beancount/doc/proposal-balance

本文总结了所有命令行记账系统中余额断言的不同语义,并为 Beancount 提出了新的总余额和文件顺序运行断言语法。

动机

部分断言与完整断言

文件断言与日期断言

顺序与歧义

日内断言

日初与日末

状态

提案

文件断言

完整断言

动机

Beancount 和 Ledger 都实现了余额断言。这些断言为系统提供了检查点,可用于验证数据录入的完整性1

传统会计原则指出,用户永远不能更改过去——修正过去需要插入新条目以撤销之前的错误2——但我们在命令行记账社区对此持不同意见:我们希望保留重建过去的能力,更重要的是,能够在原始数据录入处修正过去的错误。是的,我们是业余爱好者,但我们致力于通过挑战现有概念来简化记账流程,只要不破坏已知余额,我们认为这是完全合理的。这些已知余额正是我们通过余额断言提供的。

另一种看待这些余额断言的方式是,它们只是各种账户对账单上报告的最终金额,如下图所示。

根据此讨论帖,我们发现当前版本的 Ledger(3.0)和 Beancount(2.0b)对余额断言存在不同的解释。

balance-statement-1.png

部分断言与完整断言

Beancount 中的断言目前如下所示:

2012-02-04 balance Assets:CA:Bank:Checking    417.61 CAD

该指令的含义是:"请断言在 2012 年 2 月 3 日结束时,账户 'Assets:CA:Bank:Checking' 的库存中恰好包含 417.61 加元单位"(由于在 Beancount 中余额断言指定在日期的起始时刻,因此日期显示为 4 日)。该断言未涉及账户库存中的其他商品。例如,如果账户中包含美元单位,则不会对其进行检查。我们将这种解释称为部分余额断言单商品断言

另一种断言是详尽的:"请断言在 2012 年 2 月 3 日结束时,账户 'Assets:CA:Bank:Checking' 的库存中仅包含 417.61 加元单位。" 我们称之为完整余额断言。要实现这种断言,必须支持指定库存的完整内容。目前尚不支持此功能。

还需注意,我们断言的是库存的数量单位,而非其成本基础。

文件断言与日期断言

对于断言的运行余额,存在两种不同的解释和实现方式:

  • Beancount 首先对所有指令进行排序,并在指令日期的开始时验证余额。在前面的例子中,这意味着“在 2012-02-04 任何交易发生之前的余额”。我们将这种机制称为日期断言基于日期的断言

  • Ledger 在解析阶段会持续计算每个账户的库存余额,并在文件中断言出现的位置执行检查。我们将这种机制称为文件断言文件顺序断言基于文件的断言。这类断言不与任何特定日期关联(这在 Ledger 中略具误导性,因为断言是附加在交易的分录上的,似乎暗示它们发生在交易日期,但实际上并非如此,它们严格依据文件中的位置生效)。

顺序与歧义

这两种断言的一个重要区别是,文件断言不是顺序无关的。例如,考虑以下输入文件:

;; Credit card account
2014/05/01 opening
  Liabilities:CreditCard    $-1000.00
  Expenses:Opening-Balances

2014/05/12 dinner
  Liabilities:CreditCard    $-74.20
  Expenses:Restaurant

;; Checking account
2014/06/05 salary
  Assets:Checking            $4082.87
  Income:Salary

2014/06/05 cc payment
  Assets:Checking            $-1074.20 = $3008.67
  Liabilities:CreditCard     = $0

如果你将信用卡还款移动到信用卡账户部分,相同的交易集将失败:

;; Credit card account
2014/05/01 opening
  Liabilities:CreditCard    $-1000.00
  Expenses:Opening-Balances

2014/05/12 dinner
  Liabilities:CreditCard    $-74.20
  Expenses:Restaurant

2014/06/05 cc payment
  Assets:Checking            $-1074.20 = $3008.67
  Liabilities:CreditCard     = $0

;; Checking account
2014/06/05 salary
  Assets:Checking            $4082.87
  Income:Salary

这种细微的问题对初学者来说可能难以理解。将文件断言移至其独立的、无日期的指令中,可能更能体现其语义,例如如下形式:

balance Assets:Checking = $3008.67

缺少日期表明该检查并非应用于某个特定时间点。

日内断言

另一方面,基于日期的断言由于其顺序无关性,排除了日内断言的可能性,即在同一账户、同一天内,两个交易记录之间发生的余额断言。

目前 Beancount 不支持日内断言。

需要注意的是,尽管存在这一局限,这并未造成太大问题,因为通常可以通过适当增加或减少一天,或直接跳过无法实现的断言来规避(跳过断言并无大碍,因为它们是可选的,仅用于提供确定性;只要在跳过断言之后的某个日期仍有断言存在,就不会构成严重问题)。

不过,为基于日期的断言找到解决日内断言的方法会更好。一个有趣的想法是扩展语义,使余额检查在与该断言同一天的所有交易中,按文件顺序应用,即在断言前后直接发生的交易中进行平衡,例如以下情况将能平衡:

2013-05-05 balance Assets:Checking    100 USD

2013-05-20 * "Interest payment"
  Assets:Checking           12.01 USD
  Income:Interest

2013-05-20 balance Assets:Checking    112.01 USD

2013-05-20 * "Check deposit"
  Assets:Checking           731.73 USD
  Assets:Receivable

一旦出现其他日期的指令,这种顺序就会被打破。

另一个想法是始终将余额断言以文件顺序作为第二排序键(在日期之后),并按此顺序应用。不过我不确定这种做法是否容易理解。

日初与日末

最后,为完整起见,值得指出的是,日期断言必须明确定义其在一天中的生效时间。在 Beancount 中,目前它们在日初生效。

提供一个在日末生效的日期断言替代版本(例如“balance_end”)或许值得考虑。Beancount v1 曾支持此功能(“check_end”),但在 v2 重写时被移除,因为当时尚不明确其必要性。单一语义的简洁性本身也有其优势。

状态

Ledger 3.0 目前仅支持对交易的部分文件顺序断言。

Beancount 2.0 目前仅支持在每天开始时的部分基于日期的断言。

提案

我建议对 Beancount 的余额断言进行以下改进。

文件断言

文件断言应作为插件提供。它们的格式如下:

2012-02-03 file_balance Assets:CA:Bank:Checking    417.61 CAD

理想情况下,为了明确它们严格按文件顺序生效,它们不应包含日期,类似这样:

file_balance Assets:CA:Bank:Checking    417.61 CAD

但这样会破坏其他指令语法的一致性,而且仅仅为了这一点就让原本非常规整简单的解析器变得更加复杂……所有其他指令都以日期和单词开头,其他行则基本被忽略。这将与这一惯例有所偏离。最后,保留一个日期仍然很有用,以便在渲染的日记中插入这些指令。因此,我正在考虑保留日期。如果你决定使用这些特殊的断言,你应该清楚它们的含义。

我也不喜欢将断言附加到交易上的做法;交易语法已经足够复杂,应保持简洁。这应该是一个独立的指令,只有少数人会使用。

为实现此功能,插件只需根据文件位置(使用条目的 fileloc 属性)对所有指令重新排序,忽略日期,并从上到下重新计算累计余额并应用检查。这完全可以像基于日期的断言一样通过后处理完成,而不会干扰任何其他处理过程。

此外,将此功能作为插件实现的另一个优势是,不使用该指令的用户无需承担计算这些库存的开销。

完整断言

Beancount 应通过当前的余额断言指令支持完整断言。它们并非非常重要,但可能很有用。

可能的语法建议:

2012-02-03 balance* Assets:CA:Bank:Checking        417.61 CAD, 162 USD

2012-02-03 balance Assets:CA:Bank:Checking       = 417.61 CAD, 162 USD

2012-02-03 balance full Assets:CA:Bank:Checking    417.61 CAD, 162 USD

2012-02-03 balance 
  Assets:CA:Bank:Checking    417.61 CAD
  Assets:CA:Bank:Checking       162 USD

我仍不确定哪种最佳,目前看来这主要取决于个人偏好。


  1. 据我们所知,显式输入预期金额的概念是命令行记账系统独有的。其他系统通过冻结历史变更来“对账”。

  2. 这有多个原因。首先,在计算机出现之前,会计工作是通过手工账本完成的,手动重新计算累计余额需要对账本进行多次繁琐的修正,这必然极为不便,而在当前时间插入更正条目则容易得多。其次,如果你的会计余额用于报税,事后修改某些余额会使审计时追溯已申报金额的明细变得困难。这个问题在我们的场景中同样存在,但是否允许追溯修正取决于具体情境和账户类型,我们留给用户自行决定是否允许。