Beancount —— 命令行复式簿记

本文介绍复式簿记的基本概念以及如何使用 Beancount 记账。本文适合的读者:

  • 想要记账的;
  • 曾经或正在记账但是目前对记账方式/软件不满意的;
  • 控制欲强的。

一、为什么要记账

记账能让自己了解自己的财务状况,用大白话来说就是能回答以下问题:

  • 我的钱从哪来?
  • 我的钱在哪?
  • 我的钱去哪了?

一本维护良好的账本能生成很多有用的财务报表,其中最有用的是「损益表」和「资产负债表」,前者能回答第一个和第三个问题,后者能回答第二个问题。为了维护一本良好的账本,你需要科学的记账方法和科学的记账软件,本文将向你安利一种科学的记账方法(复式簿记)和一套科学的记账软件(Beancount)。阅读以下内容之前,你需要做好以下准备:

  • 有基础的会计知识,至少听说过「会计恒等式」;
  • 能熟练地在终端里编辑文本文件,无论 Vim 或是 Emacs;
  • 对自己的财务状况有基本了解,并愿意对此做出优化。

有以下技能会更方便:

  • 基础的 Python 知识,或是其他适合于文本处理的编程语言知识(用于导入银行账单);
  • 熟练使用 Git 等版本管理工具(用于跨设备同步)。

二、什么是复式簿记

复式簿记是一种把每笔交易都记录到复数个账户中的簿记方法。举个例子,想像你面前有两个桶,分别是「资产」(Assets)和「费用」(Expenses),左边的桶里装满了豆子,右边的桶是空的。你用 1000 元听了一场演唱会,为了记录这笔费用,你把 1000 粒豆子从 Assets 桶里转移到了 Expenses 桶里,代表你的资产减少了 1000 元,而你花在演唱会上的费用增加了 1000 元,在这个过程中,豆子的总量没有变化(资产减少的豆子与费用增加的豆子数量一致),这便是最简单的复式簿记。

实际上,复式簿记系统中,一般有五个大桶,每个桶里又可以放很多个小桶,这五个大桶分别是:

  • 资产 Assets —— 现金、银行存款、有价证券等;
  • 负债 Liabilities —— 信用卡、房贷、车贷等;
  • 收入 Income —— 工资、奖金等;
  • 费用 Expenses —— 外出就餐、购物、旅行等;
  • 权益 Equity —— 用于「存放」某个时间段开始前已有的豆子,下文详述。

豆子(或是货币)在这五个桶里倒来倒去,出入相抵,这便是会计恒等式。这些桶里剩余的货币数量,则是生成损益表和资产负债表的重要依据。

与传统的复式簿记不同,Beancount 及其前辈们用的复式簿记方法使用了正负号而不是拗口的「借」(debit)和「贷」(credit)来表示五个桶之间的豆子变动,更加容易理解和思考,也不容易出错。本文所介绍的复式簿记采用 Beancount 的方案。

虽然复式簿记可以用来记任何东西的变动,但主要还是用来记货币的变动,因此桶中的数字是可以为负数的。

具体怎么记呢?再举几个例子:

  • 收入→资产:小明是个无业游民,有天他在路上捡到 100 元,没有交给警察叔叔——收入桶倒出 100 元,资产桶增加 100 元;
  • 负债→资产:小明看中一件新衣服,但是买不起,于是问大明借了 200 元——负债桶倒出 200 元,资产桶增加 200 元;
  • 资产→费用:小明用 300 元买了一件衣服——资产桶倒出 300 元,费用桶增加 300 元;
  • 费用→资产:小明发现衣服不合适,要退货,老板说你穿了好几天了,只能退你 250 元——费用桶倒出 250 元,资产桶增加 250 元;

小明完成了这四笔交易之后,四个桶的状态:

  • 收入:-100 元
  • 负债:-200 元
  • 资产:250 元
  • 费用:50 元

这四个桶里的数字加起来是——0 元。因为这四个桶里的数字之和一开始就是 0,而每笔交易都是在桶之间加减,负数和正数的绝对值相等(和为 0),因此总量并没有变化。每笔交易在不同账户的数字加起来和为 0 是复式簿记的重要特性和原则,也是用来检验账目正确性的重要依据。复式簿记这一特性在企业账目管理中有着重要的意义——不同账户的交易内容记在不同的账本上,由不同的财务人员管理,使账目之间互相制约、不容易出错(无论是有意的还是无意的)。

在上面的例子中,「负债 -100 元」和「资产 250 元」挺容易理解的,但是「收入 -100 元」和「费用 50 元」可能不是那么容易一下子想通。如果上面倒豆子的想像没能让你信服的话,以下两个方案有助于理解(但可能并没有倒豆子那么欢乐):

  • 把收入(Income)想像成一个装着你一生(过去和未来)所有劳动成果的桶,每次你的收入都是从桶里取出东西(通常以货币的形式),一直取啊取啊,直到某一天……所以收入桶的数字通常是负数
  • 把费用(Expenses)想像成一个装着你一生(过去和未来)所有消费的桶,每次你的支出都是往桶里放东西(以货币的形式表现),和朋友出去唱歌转换成快乐存进去,看过的电子书转换成精神食粮存进去,吃过的饭转换成……所以费用桶的数字通常是正数

一旦接受了「收入和负债通常为负数」、「资产和费用通常为正数」这两个设定,那你便很容易理解这条等式了:

(Income + Liabilities) + (Assets + Expenses) + Equity = 0

用大白话来说就是:你赚的钱(Income),加上你借来的钱(Liabilities),最终要么变成你自己的钱(Assets),要么就是花掉了(Expenses),最终得到的是个零。这就是人的一辈子……

等下,Equity 是怎么来的?仔细想想小明的例子,他的四个桶要满足这个等式,前提是桶里都是空的。但是小明不是一个刚出生的婴儿,他已经活了二十多年了,之前的 Income、Liabilities、Assets、Expenses 怎么算呢?答案就是放到 Equity 里。当小明决定开始用复式簿记的时候,他从 Equity 里倒一些豆子其他桶里(或从其他桶倒一些豆子到 Equity 里),将其他桶的数字调节成符合当前实际情况即可。实际操作中,人们一般只关心 Income 和 Expenses 桶的数字在某段时间内的变化,并不关心它的总数(除非你想统计你出生到现在一共收入多少、支出多少),只要把 Assets 和 Liabilities 调节准就行了。这便是 Equity 的作用——存放已有的「权益」。

更一般地,Equity 可以用来存放所选取的时间范围之前的「汇总」。比如小明从 2012 年开始用 Beancount,一直用到 2016 年,他想只看 2016 年的财务状况,那 Beancount 便会把他 2016 之前四年的数据「调节」到 Equity 里,来维持 2016 年会计恒等式的平衡。

三、Ledger-like 和 Beancount

Beancount 是一个 Ledger-like 软件。Ledger 是这一类复式簿记软件的开创者。他们共有的特点是:

  • 采用改进的复式簿记方案(使用正负号而不是「借」和「贷」来表示账户之间的变化);
  • 使用纯文本文件作为账本,用户用文本编辑器即可记账;
  • 账本既是用户输入的文件,同时也是软件的「数据库」;
  • 软件读取账本并生成报表,账本本身也可供人类直接阅读。

市面上的复式簿记软件不少(如 GnuCash),但是大部分都是提供一个 GUI,用户在一堆文本框里输入各种数字和文字,软件接受输入然后存储到自己的数据库里(SQLite、MySQL 等)。用户无法直接看到或操作他们的数据,必须通过软件来操作;一旦软件停止更新,用户的数据就危在旦夕:难以导出,难以复用,很难跨平台或跨设备同步。

而 Ledger-like 软件则直接使用文本文件作为账本,用户直接用最喜爱的编辑器打开账本即可记账。软件只是读取你的账本并生成报表,即使软件停止更新,用户依然可以直接阅读账本。你可以方便地在各在平台上记账,甚至跨设备问题也可以用 Dropbox 等同步工具,或是 Git 等版本管理工具轻松解决。

Beancount 是 Ledger-like 软件中优秀的一员,相比用 C++ 写成的 Ledger,用 Python 写成的 Beancount 更轻便,更方便增加插件和二次开发,也增加了很多功能,如灵活强大的多「货币」支持。这里为加上引号是因为,Beancount 其实并不知道什么是「货币」,它记录的只是「通货」(commodity)的变化,所有的 commodity 皆由用户自己定义,因此 Beancount 可以用来记录包括货币在内任何东西的变化,比如年假天数、股票、航空里程、信用卡积分,当然了,还可以用来数豆子。这也是 Beancount 名字的来源。

四、Beancount 基础

Beancount 是个 Python 软件,可以从 PyPI 安装。建议同时安装另一个相关软件 Fava,是 Beancount 的一个漂亮的 Web UI:

virtualenv BEANCOUNT
source BEANCOUNT/bin/active
pip install beancount
pip install beancount-fava

装好之后便可以开始写你的第一个账本了。怎么写?Beancount 作者写了非常非常详细的文档:

比如小明如果用 Beancount 的话,他的账本将是这样的:

1970-01-01 open Income:Windfall
1970-01-01 open Assets:Cash
1970-01-01 open Liabilities:Da-Ming
1970-01-01 open Expenses:Clothing

2016-01-01 * "捡到钱了"
  Income:Windfall                            -100.00 CNY
  Assets:Cash                                +100.00 CNY

2016-01-01 * "向大明借钱"
  Liabilities:Da-Ming                        -200.00 CNY
  Assets:Cash                                +200.00 CNY

2016-01-01 * "XX 百货商店" "买衣服"
  Assets:Cash                                -300.00 CNY
  Expenses:Clothing                          +300.00 CNY

2016-01-02 * "XX 百货商店" "退衣服"
  Expenses:Clothing                          -250.00 CNY
  Assets:Cash                                +250.00 CNY

首先小明需要设立账户。开户日期可随自己喜好定,只需比最早一笔涉及到该账户的交易更早即可。这里小明都使用了 1970 年 1 月 1 日作为开户日期,保证今后记录的各种交易都会发生在这个日期之后。

然后便可以真正开始记了,交易的格式如上所示。其中日期后面的星号(*)代表这是一笔已确认的交易,如果换成感叹号(!)的话,则代表这笔交易有疑惑,后期对账时应注意。对账标志后面则是跟着收款人(Payee)和备注(Narration),需要用引号包起来。Payee 是可选的,只有一个字符串的话,这串字符就是 Narration 了。

小明的账本已经写完了,手工书写,也能肉眼阅读。那么 Beancount 有什么用?当然是生成报表:

(BEANCOUNT) bean-report xiaoming.bean balances
Assets:Cash               250.00 CNY
Equity              
Expenses:Clothing          50.00 CNY
Income:Windfall          -100.00 CNY
Liabilities:Da-Ming      -200.00 CNY

于是小明对自己的财务状况一目了然:有 250 元现金,在衣服上花了 50 元,一共收入了 100 元,欠着大明 200 元。bean-report 没有报错,说明账是平的(总和为 0)。bean-report 还能生成很多报表,使用 bean-report -h 查看帮助。

Beancount 自带了一个朴素的 Web UI,能以交互式的方式查看各种财务报表,执行 bean-web xiaoming.bean 命令,然后在浏览器中打开 http://localhost:8080/ 即可:

小明的资产负债表

小明的资产负债表

小明的损益表

小明的损益表

如果你之前安装了 Fava,还可以用 Fava 看到一个更华丽的 Web UI,执行 fava xiaoming.bean 命令,然后在浏览器中打开 http://localhost:5000/ 即可。由于小明的数据还比较单薄,这里贴两张 Fava 作者的示例图:

Fava 展现的资产负债表

Fava 展现的资产负债表

Fava 展现的损益表

Fava 展现的损益表

在资产负债表(balance sheet)里,你可以一目了然地看到自己有多少资产、资产分别在哪些账户里、有多少负债、是对哪些银行的负债。

在损益表(income statement)里,你可以一目了然地看到自己的每月有哪些收入、收入来自于哪些地方、有多少支出、支出花在了什么地方。

在这些页面里还有更多报表等待着你去探索。

五、Beancount 进阶

以下举几个例子,展现一下 Beancount 和复式簿记能处理多么复杂的交易。这些复杂的交易用单式簿记来记录是困难而极易出错的,但是在复式簿记里却是自然而流畅的。之前说过,复式簿记的「复」是指一笔交易会涉及到复数个账户。小明的例子都是两个账户间「一对一」交易,如一个 Income 账户一个 Assets 账户,或一个 Assets 账户一个 Expenses 账户等。但实际上,生活中会遇到各种「一对多」或「多对一」或「多对多」的交易:购买大件物品时因银行支付限额而使用多张银行卡合并付款;朋友出去唱歌、聚餐每人付的钱不同,事后 AA 平摊等。以下几个例子是有意构造的涉及到两个以上账户的交易,让我们一起来看看小红的账本。

第一个例子

2016-01-31 * "工资 2016-01"
  Income:SomeCompany:Salary                -20000.00 CNY ; 应发工资
  Income:SomeCompany:Reimbursement          -1000.00 CNY ; 餐补
  Income:SomeCompany:Reimbursement           +100.00 CNY ; 餐补扣除
  Expenses:Government:Pension               +1500.00 CNY ; 养老保险
  Expenses:Government:Unemployment           +100.00 CNY ; 失业保险
  Expenses:Government:MedicalCare            +500.00 CNY ; 医疗保险
  Expenses:Government:HousingFund           +3000.00 CNY ; 住房公积金
  Expenses:Government:IncomeTax             +3000.00 CNY ; 个人所得税
  Assets:CMB:C1234                         +12800.00 CNY ; 实发工资

这个例子展现了如何在 Beancount 里体现工资条上的内容。每个月的工资条上总会有各种各样的名目。小红在使用了 Beancount 之后,可以方便地把工资、餐补、三险一金、个税等信息都记录进去,以后能很方便地统计每个月有多少工资是喂狗的。

本例子中有 Income:* 账户有三条记录,Expenses:* 账户有五条记录,Assets:* 账户有一条记录,共八条记录,总和为 0。

第二个例子

2016-02-01 * "XX 购物中心" "购物"
  Liabilities:CMB:CreditCards               -1000.00 CNY ; 信用卡刷卡
  Expenses:Clothing:Pants                    +200.00 CNY ; 长裤一条
  Expenses:Clothing:Shirts                   +200.00 CNY ; 衬衫一条
  Assets:Receivables:Xiao-Mei                +600.00 CNY ; 帮室友小美付钱

小红拿到工资第二天就和小美去购物中心逛街,买了一件衣服一条裤子,花了 400 元,小美没带卡,身上现金不够,于是让小红帮她付钱,以后再还她,于是小红把 1000 元的东西一起刷了信用卡。

本例子中小红的 Liabilities 桶里倒出了 1000 元,往两个 Expenses 桶里各倒了 200 元进去,又往 Assets 桶里倒了 600 元。帮小美付了钱,算是小美欠小红的钱,所以算作资产。所有数字加起来和为 0。

第三个例子

2016-02-05 * "XX 黑心饭店" "和小美吃饭"
  Assets:Cash:Wallet                         -300.00 CNY ; 钱包现金
  Assets:Receivables:Xiao-Mei                -200.00 CNY ; 小美帮我付的现金
  Expenses:Food:DiningOut                    +250.00 CNY ; AA 我的一半
  Assets:Receivables:Xiao-Mei                +250.00 CNY ; AA 她的一半

过了几天小红和小美去一家饭店吃饭。本以为人均消费 100 元左右就可以搞定,没想到了这是家黑心饭店,老板说两人共消费了 500 元,还只能付现金,不能刷卡。小红和小美掏空了钱包,总算凑齐了 500 元现金,其中小红付了 300 元,小美付了 200 元。这顿饭两人还是打算 AA 平分掉。

本例中,倒豆子的桶有两个,分别是代表「小红的钱包」的桶,和代表「小美欠小红的钱」的桶,豆子倒去哪儿了?一半进了「小红的消费」桶,另一半回到了「小美欠小红的钱」桶。整个交易中,数字的总和依然为 0。

第四个例子

2016-02-10 * "在免税店买东西"
 Assets:Cash                                 -200.00 USD
 Liabilities:CMB:CreditCards                 -650.00 CNY @@ 100.00 USD
 Expenses:Clothing:Pants                     +150.00 USD
 Expenses:Clothing:Shoes                     +150.00 USD

小红去国外出差了,回国前为了把兑换的美元现金花掉,忍不住又在免税店大肆购物,结果现金不够,于是 300 美元的商品用现金支付了 200 美元,用信用卡支付了 100 美元。小红的信用卡开通了外币消费人民币入账功能,刷美元也出人民币账单。

本例中,涉及到了合并付款和货币转换。小红的信用卡被扣掉了 650 人民币,这其实是由 100 美元转换而来。在 Beancount 中使用 @@ 即可连接两种互相转换的 commodity。在本次交易中,负数共 -200.00 USD + (-100.00 USD) = -300.00 USD,正数共 +150.00 USD + (+150.00 USD) = +300.00 USD,正负相加依然得到的是 0。

使用 bean-query 进行复杂查询

bean-web 的朴素 Web UI 和 fava 的华丽 Web UI 已经能展现很多有用的财务报表,满足大部分用户的需求,如果用户需要进行一些更复杂的数据统计,比如「我 2015 年吃过的饭店按次数排列」,则可以使用 bean-query 工具用 SQL 语句进行查询,详见 Beancount 作者的文档:Beancount – Query Language。这是一个用来统计光顾麦当劳次数的例子:

bean-query-mcdonalds-blurred

六、Beancount 最佳实践

目前我的 Beancount 账本中已经导入了好多个月的数据,在使用过程中也总结了一些最佳实践。以下内容说说我个人是怎么用 Beancount 的,它们中的一部分或全部或许可以为你所用。

编辑器支持

Beancount 的作者是 Emacs 用户,因此自己写了 Emacs 插件。Vim 用户可以使用第三方的插件:nathangrigg/vim-beancount。安装插件之后会为 *.bean*.beancount 文件加上语法高亮和账户名字补全(比如输入 I:S:S 即可补全出 Income:SomeCompany:Salary),还可以将货币那一列的小数点自动对齐。以下是我的 .vimrc 中相关的配置:

let b:beancount_root = '/path/to/your.beancount'
autocmd FileType beancount inoremap . .<C-O>:AlignCommodity<CR>
autocmd FileType beancount inoremap <Tab> <c-x><c-o>

其他编辑器如 Sublime 等也有各自的插件,请自行 Google。

开户日期的选择

账户的开户日期需要在该账户第一笔交易之前。小明为了省事将所有的账户全部开在了 1970-01-01 这个日期。其实可以有一些更有创意的选择:

  • Expenses 账户可以使用自己的生日作为开户日期;
  • Income 账户下可以按来源分类,如 Income:SomeCompany:Salary, Income:AnotherCompany:Salary 等,然后以公司入职时间作为开户日期;
  • Assets 和 Liabilities 账户中的借记卡和信用卡,可以以在银行的开户日期作为 Beancount 中的开户日期,如果记不得具体日子,写成那个月的 1 号也行。

不要惧怕开账户。即使是一些短时间用的小账户(比如只用两个月的储值卡),也可以开账户,因为账户是可以关闭的。关闭后的账户不会出现在关闭后的报表里,不会触发你的强迫症……

多货币账户

在 Beancount 中,一个账户中可以有多种 commodity,比如现在小红的 Expenses:Clothing:Pants 账户就存放了 200.00 CNY 和 100.00 USD。她在出差前想必 Assets:Cash 里也同时存在着 CNY 和 USD 两种 commodity。

如果有多货币的使用,建议将自己主要使用的货币定义到账本中,在账本中添加 option "operating_currency" "CNY" 这一行即可将 CNY 定义为主要货币,在 bean-web 和 fava 中会单独列出来,而其他的 USD、CAD、JPY 等则会列到 Other 里。主要货币可以定义多个。

另外,在账户开户的时候,可以在账户名后面跟上这个账户里允许出现的货币的名字。如人民币-美元的双币信用卡,消费 JPY、CAD 等其他货币的时候,也是以 USD 入账的,为了防止自己在记录一些外币交易时忘记转换货币或是搞错账户,可以在开户时写成 2012-01-01 open Liabilities:CMB:CreditCards CNY,USD 这样,限定这个账户里只能出现 CNY 和 USD 两种货币,如果不慎记入了其他货币,Beancount 会报错。单币信用卡同理,如果你的信用卡不管刷什么外币都是以 CNY 记账,可以在开设账户的时候加上 CNY 这个限制,防止出错(不小心把外币消费没加 @@ 直接记进来)。

账本文件的分割

随着时间的积累,账本文件会越来越大,编辑起来不太方便。Beancount 有 include 语句,可以在一个账本文件里包含另一个账本文件。我的主账本文件里只有一些 option 条目,其他都是 include,各种打开/关闭账户的的条目放单独的文件里,然后每个月的账本是一个单独的文件,也 include 进来。

Beancount 会把所有交易都读到内存里后按日期重新排序,所以每条交易在文件里出现的顺序并不重要。

导入银行账单

不同人的记账习惯不同,有的人喜欢消费完一笔立刻就记账,有的人喜欢定期(每天、每周、每月)把之前的收支汇总到账本上。在我看来,所有「有据可查」的交易,如走银行卡的交易,是可以定期汇总的,但是那些无账单无票据的交易,如现金交易,要么就是干脆不记,要么就应该想办法立刻记下来,否则当你定期回忆的时候一定会因为各种原因出错,从而打击记账的信心。

更新:如果你喜欢发生一笔交易立刻就记下来,而不是定期导入账单,并且你是一位 OS X 用户,可以试试 @blaulan 制作的 Alfred Workflow:blaulan/alfred-beancount

就我自己而言,因为不喜欢现金找零,我会尽量避免现金消费,我的绝大部分交易全部都是刷卡消费,全部都能从银行账单里查到,现金类交易每月通常不超过 10 笔,因此我的记账主要是靠导入银行账单。现金部分则是在手机上安装一个简单的记账软件,里面只有一个账户,就是我的现金账户,每月那少量的现金交易就立刻用它记下来。由于对功能性要求非常少,几乎任何一个手机记账软件都可胜任,随便挑一个就行。

那么怎么导入银行账单呢?一些银行提供了 OFX(Open Financial Exchange)格式的账单,Beancount 可以直接导入,但是据我观察,中国大陆的银行没有一家是支持 OFX 的,都是自己搞一套自己的账单,能有个 CSV 导出已经不错了。所以只能自己写脚本解析了。这是我写的导入招行信用卡账单的脚本:cmb_credit_cards.py

由于 Beancount 的账本是文本文件,将银行账单转换成 Beancount 账本只是对字符串的操作,这方面各种脚本语言都可以大显神通。我自己的做法将银行账单中的每笔交易的日期、内容和金额提取出来,全部拼成从该账户往 Equity:Uncategorized 里倒豆子的交易,然后再用 Vim 配合插件手工将账户名根据实际情况改好。这样的操作我每月末做一次。处理完银行账单之后,我在后面再追加现金交易,将手机中的记录中的那几笔现金交易录入到账本里。由于 Beancount 会对交易按日期重新排列,所以直接追加到后面即可,不用管文件中的顺序。

值得一说的是,现在一些第三方支付服务很流行,比如杭州某公司推出的带聊天功能的支付服务、深圳某公司推出的带支付功能的聊天服务等。这些支付服务也会发所谓的「对账单」给用户,我对它们是一向无视的。我在第三方支付服务里是不留余额的,所有的「经过」第三方支付服务的交易都是从银行扣款的,因此我导入银行账单就够了,不用再导入第三方支付服务的账单。

导入银行账单时,需要注意的一个地方是去重。如果两个银行账户间有转账操作的话,会出现重复的账目,比如用借记卡对信用卡进行还款,在导入的借记卡账单和信用卡账单中都会有体现,然而这两笔交易其实是同一笔,这时候就需要去重。

我现在每月账本由三部分构成:

  • 信用卡账单(从银行账单导入后配合 Vim 插件半自动填写 Expenses 账户)
  • 借记卡账单(从银行账单导入后配合 Vim 插件半自动填写 Income 账户)
  • 现金交易记录(平时用手机记录,月末手工录入)

我的每次导入只需要去重两次,一次是每月借记卡自动还款信用卡,一次是每月 ATM 取款。如果你的账单构成比较复杂,是时候考虑优化一下了,比如第三方支付服务里不留余额,省得还要导入它们的账单并去重……

更新:我开设了一个 GitHub 项目 awesome-beancount,用于收集私有格式账单的导入脚本,如何下载各银行账单,以及其他的一些 Beancount 的最佳实践。目前里面已经有了一些用户分享的中国大陆银行及第三方支付服务的导入脚本。如果你使用的银行已经在这个项目里了,你可以直接使用;如果不在,你可以写出你自己的导入脚本并提交一个 pull request。

定期断言

一本维护良好的账本应当定期做断言(assertion),标记在某个日期某个账户(通常是 Assets 或 Liabilities 账户)里有多少豆子。断言的例子如下:

2016-02-01 balance Assets:Cash 500.00 CNY
2016-02-01 balance Assets:Cash 100.00 USD
2016-02-01 balance Assets:CMB:C1234 1000.00 CNY

断言语句告诉 Beancount,这个账户在这个日期凌晨 00:00:00 时间点(也就是前一天深夜 24:00:00),余额为这个数字。小红账本里以上断言告诉 Beancount,截止一月底,小红钱有 500 人民币、100 美元的现金,同时招行尾号 1234 的借记卡里有 1000 人民币的存款。

Beancount 的时间精度是「日」,所以这里必须强调,诸如 open, close, balance 等带日期的语句,均发生在当日的第一笔交易之前,你可以想像它们都是在凌晨发生的,而普通的交易都是发生在白天。因此,要断言一月份的余额,日期应写作 02-01 而不是 01-31。同样地,信用卡等通常为负数的账户也能进行断言,比如小红的信用卡账单日为 20 日,2 月份账单应还款 5000 元,那她的断言应该这样写(注意日期是第二天,也就是 21 日):

2016-02-21 balance Liabilities:CMB:CreditCards -5000.00 CNY

添加了断言之后,Beancount 便会检查那个账户的数字是否与断言的数字相等,如果不相等就会报错。人总是会犯错的,当你因为各种原因在账目上出现了错误,断言能帮助你缩小查错范围——你只需要检查最后一次成功的断言之后的发生的交易即可。

合理填充

Beancount 另一个有趣的功能是填充(padding),填充是配合断言一起用的,当 Beancount 解析到填充语句时,会自动在这条语句和下一条断言语句之间插入一条填充交易,使得断言成功。在填充语句所在日期和断言语句所在日期之间不能再有其他交易。例子如下:

2015-11-30 pad Assets:Cash Expenses:Food:Drinks
2015-12-01 balance Assets:Cash 200.00 CNY

小红 11 月底做账目核对的时候,发现钱包里的现金是 200 元,但是根据 10 月底的余额,以及 11 月的交易记录,钱包里应该剩 200 多元才对,她想了下,可能是有几次在路边买了饮料喝忘记记录了,因此她使用填充功能来解决这个问题,在 11 月最后一笔交易和 12-01 的断言之间插入一条 pad 语句,这样 Beancount 便会自动插入一条交易,使 Assets:Cash 里的余额调整为 200.00 CNY,而因此产生的货币变化,则记录到 Expenses:Food:Drinks 账户里。在本例中,自动插入的交易内容即是从 Assets 账户倒出了一些货币到 Expenses 账户里。

Beancount 作者便是这样来使用的填充功能的。他的现金账户几乎只用来购买烟酒和饮料,但是他又懒得记录现金支出,于是他就在月底的时候将现金账户 pad 到 Expenses:Food 一次,然后用断言语句记下月底现金账户的实际余额,中间的差值会由 Beancount 自动算出来并插入。

填充功能另一个用途是开户时设定初始余额。比如小红的借记卡是 2010 年开户的,她从 2015-06-01 开始用 Beancount,她就可以这么写:

2010-01-01 open Assets:CMB:C1234
2015-05-31 pad Assets:CMB:C1234 Equity:Opening-Balances
2015-06-01 balance Assets:CMB:C1234 1000.00 CNY

这样 Beancount 会自动插入一条交易,把 Assets:CMB:C1234 在 2015-06-01 (凌晨)的余额的调整为 1000.00 CNY,因此产生的货币变化(新开的账户余额默认是 0),记录到 Equity:Opening-Balances 账户里。在本例中,自动插入的交易内容即是从 Equity 账户倒出了一些货币到 Assets 账户里。

填充功能比手工写一笔交易有什么好呢?你不需要去计算两个数字之间的差额了——Beancount 会自动算出差额并帮你插入交易。此外,这个差额是动态计算的,在上面两个例子中,如果小红想起了在哪天买了什么饮料,重新记上去,那么这个差额会自动变小;如果小红后来又导入了 2010-01-01 到 2015-05-31 之间的账单,那这个开户余额也会自动根据实际情况调整大小。

七、尾声

我从 2011 年大学一年级开始用手机记账,至今也快五年了。其间换过几次记账软件,但其实一直在凑合着用,因为这些记账软件总有一些功能没有办法覆盖到,因此我总是想着各种方法去曲线救国。比如最常见的与室友出去 AA 聚餐,一个人付钱,其他人把钱给付款人——这种交易在付款人的银行卡里是体现为一笔交易,但是实际上在软件里却要为每个单独记一笔,否则到月底一看,「哇,我这个月在吃饭上花了这么多钱」,其实只是把帮别人付的钱都加入了「聚餐」这个类别而已。但如果真的按照实际情况付一次钱记多笔的话,拿银行卡账单对账的时候又会让人很焦躁。

大约在 2015 年,我开始逐渐意识这些困难不是记账软件本身的问题,而是记账方法的问题。我用过的那些手机记账软件,不管 UI 多么好看,它们本质都是个单式簿记系统,因此只能处理「一对一」的简单交易,像 AA 聚餐、合并付款这种「一对多」和「多对一」的交易,就没法合理优雅地记录了,更别说上文小红和小美在黑心饭店遇到的「多对多」交易了,根本应付不来。

于是我开始接触复式簿记。发现手机上唯一堪用的复式簿记软件是 GnuCash 的 Android 版。然而它虽然堪用,却不堪重用——Bug 实在太多了。Beancount 作者曾吐槽过 GnuCash 的电脑版,他说他每隔几个月就会去尝试一下它,但总能在一小时内发现新的 bug。电脑版尚且如此,手机版的 bug 数量更难想像——这些有着复杂 GUI 的传统复式簿记软件太难用了。

在 2015 年底的时候,我看到 @yegle 提到了 Beancount 这个软件,便去了解了一下,这才打开了新世界的大门——这才是「double-entry accounting done right」啊!没有复杂的 GUI,只有亲切的 CLI、强大的功能、简明的语法。这才意识到原来复式簿记可以如此简单好用。

用了几个月 Beancount 之后,我对它十分满意,因此写了这么一篇博客,希望能将它推广给更多的用户。

评论页