本文介绍复式簿记的基本概念以及如何使用 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 是复式簿记的重要特性和原则,也是用来检验账目正确性的重要依据。复式簿记这一特性在企业账目管理中有着重要的意义——不同账户的交易内容记在不同的账本上,由不同的财务人员管理,使账目之间互相制约、不容易出错(无论是有意的还是无意的)。

在上面的例子中,「负债 -200 元」和「资产 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:

python -m venv BEANCOUNT
source BEANCOUNT/bin/active
pip install beancount
pip install 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 会对交易按日期重新排列,所以直接追加到后面即可,不用管文件中的顺序。

来自 1.5 年后的更新:直到我有了导入 Chase 账单的需求,才发现 Beancount 有一整套的导入工作流:bean-identify, bean-extract 和 bean-file 分别可用来识别、解析和存档银行账单文件。在 beancount.ingest.importers 里已经有一些现成的导入器了,包括一个可配置的 CSV 导入器。上文提到的那个山寨导入器是用纯字符串拼接的方式的实现的,不推荐使用。我把 Beancount 自带的那个 CSV 导入器稍微修改了下,加了一些功能。但由于代码写得渣,估计不会被合并进主线了,有需要的可以自行下载这个 Pull Request 里的 patch 使用。(更新:已被合并进主线)目前我使用 CSV 导入器导入几乎一切账单,包括各种银行账单和 Splitwise 的 CSV 导出,并且在导入的时候根据预定义的好的规则自动填写 Expenses: 和 Income: 账户。我需要做的只是在导入完成之后快速校验一遍即可。

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

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

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

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

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

2019-05-09 更新:若是不想手动去重,可以考虑建立一个 Equity:Transfers 账户。比如「借记卡还信用卡」这一交易,借记卡账单里那一笔计为从「从借记卡转入 Equity:Transfers」,而信用卡账单里那一笔记为「从 Equity:Transfers 转入信用卡」。这样,只要 Equity:Transfers 的余额为零,你的账便是没有问题的。更好的方法是配置 ZeroSum 插件,自动识别哪些交易未被匹配。

定期断言

一本维护良好的账本应当定期做断言(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 时间点(也就是前一天深夜 23:59:59 的下一秒),余额为这个数字。小红账本里以上断言告诉 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 之后,我对它十分满意,因此写了这么一篇博客,希望能将它推广给更多的用户。


2019-03-01 三年后的更新:有关 Beancount 的新博客《使用 Beancount 记录证券投资》。


读者来信

2018-10-25 读者鱼翅 <e...@gmail.com> 来信交流了 Beancount 的用法,并写了一篇非常实用的 Beancount 心得:《beancount 简易入门指南dead link。他的文章重点写了自动化获取和导入银行账单的过程,这是本文写作时没有覆盖到的,推荐阅读。

2019-04-17 读者 Conver <c...@gmail.com> 来信询问:

今天看到你博客中介绍的复式记账法,有一点疑惑。比如资产中的银行存款有大量的钱,我全额买车了,如果将车作为自己的资产,这样的账应该怎么考虑呢?

我的回答是:

把车作为资产的话,可以使用一个独立的 commodity,比如:
2019-01-01 * "DMC DeLorean"
  Assets:Bank
  Equity:Cars      1 DELOREAN {30000.00 USD}
除了放在 Equity 里,你也可以选择放在 Assets 里。 此外,如果需要折旧的话,可以使用 amortize_over 插件。

2023-08-03 读者 Fang-Pen Lin <f...@launchplatform.com> 来信推荐了其创办 Beancount 托管平台 BeanHub。该平台提供了灵活的自定义表单功能,可以让用户方便地在浏览器中录入数据。对于有随手记账习惯的用户来说,可能无法随时随地直接使用文本编辑器进行录入。BeanHub 作为托管平台,免去了用户自己安装维护 Beancount 的麻烦,更可提供网页界面,方便在手机等移动设备上进行记录。


欢迎留下评论。评论前,请先阅读《隐私声明》。