在 Beancount 中分摊费用

Martin Blais,2015 年 5 月

http://furius.ca/beancount/doc/shared

引言

本文介绍了一种在复杂群体情境中精确且简便地核算共享费用的方法。例如,与一群朋友旅行时,每个人支付不同的开销。我们展示如何处理需平均分摊的群体费用,以及需分配给特定人员的费用。

该方法基于复式记账系统。其核心在于将费用记录与其相关支付分离。这使得处理共享成本变得更为容易,因为采用此方法后,无需关心谁支付了什么:我们只需在最后核对每个人应承担的确切金额,并自动计算每个人的最终调整转账。

本文围绕一次由两人平均分摊费用的简单旅行展开。然而,该方法同样适用于任何涉及多方共享收入与支出的项目,无论费用是否平均分摊。

旅行示例

马丁(作者)和卡罗琳(他的女友)于 2015 年 3 月前往墨西哥旅行。我们前往科苏梅尔岛进行了为期三天的潜水旅行,随后前往图卢姆停留两天,放松并探索天然井

我们的假设是:

  • 混乱的支付。我们的生活非常忙碌……在旅行前和旅行期间,我们都会自行支付各种开销,而不会预先考虑谁该支付什么,但我们会仔细记录每一笔付款。我们各自仅根据需要支付与行程相关的筹备费用。例如,卡罗琳提前选定了航班并完成预订,而我在出发前一周支付了度假村费用,并预订了租车及当地潜水店的活动。

  • 共享资产与个人资产的使用。 我们双方都将携带现金,并在旅途中分别使用各自的钱包以及一个已兑换为当地货币(墨西哥比索,我使用“MXN”符号)的共享现金池进行支付,必要时也会在出行前后使用信用卡。

  • 多种货币。 我们的部分开销以美元计价,部分以墨西哥比索计价。例如,航班费用已用美元支付,当地餐饮和住宿用比索支付,但当地的潜水店却向我们收取美元。兑换后的金额将来自现金和信用卡两种来源。

  • 共享池中的个人开销。 尽管大多数开销将平均分摊,但有些开销仅涉及其中一人,我们需要明确记录这些支出。例如,卡罗琳参加了 SCUBA 认证课程(PADI 开放水域),相关费用将由她自行承担;同样,她不应为马丁昂贵的船潜费用买单。更复杂的是,潜水店在我们行程结束时为我们开具了一张合并的联合账单。

关于分摊的一点说明

我认为有必要就我们开销的“分摊”问题稍作说明,因为这一话题曾在以往的邮件列表讨论中多次出现,涉及各种分摊示例。

我们故意锱铢必较。 本练习的目的在于以极其细致的方式记录每一笔支出。本文件的宗旨正是展示:即使面对复杂的交易组合,只要记录详尽,也能高效、简洁地厘清每位参与者的实际支出,而不论谁实际支付了款项。我们并非吝啬鬼。

我们将假设双方同意平均分摊开销。 我们彼此之间的“慷慨”与此文件无关。我们都是收入可观的专业人士,可以假设我们已达成一致,对本次旅行的共同费用进行均摊(50/50)。

我们所展示方法的一个特点是,如何分摊开销的决定可以与实际支付行为独立进行。例如,我们最终可能决定由我承担三分之二的费用,但这一决定将被精确计算,而非依赖模糊的“我记得我付过这个”的临时判断。这一点在人数较多的团体中尤其有用,因为若开销未被准确追踪,通常每个人都会觉得自己的支出比别人多。群体中所有成员感知到的支出比例总和总是超过 100%……

方法概览

本节将简要图文介绍该方法。我们设立一组属于本项目的通用资产账户,并将所有个人开销及转账记录为来自外部收入账户:

旅行期间,我们使用这些通用资产进行支出。大部分支出将归于双方(最终平均分摊),但部分支出则明确归属于个人:

旅行结束后,剩余的资产(如我们回家时携带的现金)将返还给各自,以清零资产账户余额,这一过程通过反向记账至收入账户来记录:

最后,共享费用列表通过一个插件进行分摊——该插件会将每个标记为共享费用的条目拆分——然后根据最终金额在各方之间进行一次性转账,以确保我们各自支付了属于自己的费用,达到账目平衡:

请注意,每位参与者的最终费用余额可能不同,这是由于某些费用被单独归类,或我们决定不平均分摊总费用所致。

如何记录费用

为了记录此次旅行的所有费用,我们将创建一个新的 Beancount 输入文件。尽管这些费用来源于每个人的个人账户,但将此次旅行视为一个特殊项目——就像一个仅在旅行期间存在的独立实体(例如一家公司)——会更有帮助。我们为此次旅行编写的示例文件可在此处查看,有助于您跟随本文操作。

账户

输入文件中的账户集合无需与您个人 Beancount 文件中的账户名称完全一致。我们使用的账户包括对应马丁和卡罗琳个人账户的通用名称(例如,使用 Income:Martin:CreditCard 而非 Liabilities:US:Chase),而支出账户也可能与我个人 Beancount 文件中的常规支出账户不匹配——这并不重要。

作为约定,任何专属于某位旅行者的账户,其名称中将包含该人的姓名。例如,卡罗琳的信用卡账户将命名为“Income:Caroline:CreditCard”。这一点很重要,因为后续我们将利用此命名规则来分摊贡献和费用。

让我们逐一查看为实现此目的所需的不同类型账户。

外部收入账户

该“项目”将通过两位旅行者个人账户的转账获得收入。我们将这些账户视为项目外部的账户,因此将其定义为收入账户:

;; External accounts for Martin.
2015-02-01 open Income:Martin:Cash
2015-02-01 open Income:Martin:Cash:Foreign
2015-02-01 open Income:Martin:Wallet
2015-02-01 open Income:Martin:CreditCard

;; External accounts for Caroline.
2015-02-01 open Income:Caroline:Cash
2015-02-01 open Income:Caroline:Wallet
2015-02-01 open Income:Caroline:MetroCard
2015-02-01 open Income:Caroline:CreditCard

从这些账户发生的交易,必须从您个人的 Beancount 文件中复制过来。显然,您必须仔细确保包含所有与旅行相关的交易。我在个人文件中使用了标签来完成这一操作。

资产与负债账户

在旅行期间,将存在若干临时资产账户,这些账户将在旅行结束时清零。一个例子是本地货币的零用现金池:

2015-02-01 open Assets:Cash:Pesos
  description: "A shared account to contain our pocket of pesos"

旅行期间,我们各自随身携带了现金,因此我为此创建了两个独立的账户:

2015-02-01 open Assets:Cash:Martin
  description: "Cash for the trip held by Martin"

2015-02-01 open Assets:Cash:Caroline
  description: "Cash for the trip held by Caroline"

需要注意的是,尽管这些账户有各自的名称,但它们仍被视为项目的一部分。仅为了方便分别追踪我们各自在旅途中持有的现金余额,才做了这样的区分。

支出账户

我们将定义多种账户来记录我们的支出。例如,“Expenses:Flights”账户将包含与航班旅行相关的所有费用。为方便起见,并考虑到本文件中支出类型繁多,我们选择使用“auto-accounts”插件,让 Beancount 自动创建这些账户:

plugin "beancount.ops.auto_accounts"

这些账户中的绝大多数用于分摊我们之间的共同开支。例如,共享的潜水费用将记入“Expenses:Scuba”账户。

然而,对于仅由其中一人承担的开支,我们直接在账户名称中加入旅行者的姓名。例如,马丁的额外船潜费用将记入“Expenses:Scuba:Martin”账户。

示例交易

让我们关注文件中出现的不同类型的交易。本节中,我将带您了解一些代表性交易(我为这些交易赋予的名称是任意的)。

贡献交易

项目开支的贡献通常通过从外部账户支付费用来实现。例如,卡罗琳用她的信用卡支付机票费用如下所示:

2015-02-01 * "Flights to Cancun"
   Income:Caroline:CreditCard        -976.00 USD
   Expenses:Flights

马丁支付前往机场的出租车费用如下所示:

2015-02-25 * "Taxi to airport" ^433f66ea0e4e
  Expenses:Transport:Taxi                   62.80 USD
  Income:Martin:CreditCard

携带现金

我们两人均携带了一些现金,因为有人提醒我们在墨西哥可能难以使用信用卡。在我的个人 Beancount 文件中,现金账户为“Assets:Cash”,但在这里必须将其记为以我名字命名的外部贡献:

;; Initial cash on us.
2015-02-24 * "Getting cash for travel"
  Income:Martin:Cash                        -1200 USD
  Assets:Cash:Martin                         1200 USD

卡罗琳的现金情况类似:

2015-02-24 * "Getting cash for travel"
  Income:Caroline:Cash                       -300 USD
  Assets:Cash:Caroline                        300 USD

再次说明,Assets:Cash:MartinAssets:Cash:Caroline账户被视为项目的一部分,此处仅表示现金由谁携带。(这些账户最终会被清零,因此账户中包含我们的名字并无影响。)

转账

旅行前,我非常忙碌,看起来卡罗琳将负责大部分安排和预付款项,因此我向她的 Google 钱包转账,以帮助她提前垫付部分开支:

2015-02-01 * "Transfer Martin -> Caroline on Google Wallet"
  Income:Martin:Wallet                      -1000 USD
  Income:Caroline:Wallet                     1000 USD

现金兑换

获取当地货币是通过在机场兑换少量现金完成的(汇率非常差):

2015-02-25 * "Exchanged cash at XIC at CUN airport"
  Assets:Cash:Caroline                    -100.00 USD @ 12.00 MXN
  Assets:Cash:Pesos                          1200 MXN

Assets:Cash:Pesos”账户用于追踪我们共同持有的比索现金池,用于各种小额开支。

以美元支付的现金开支

一些当地开支需要使用美元,本例中我从自己的现金口袋中支付:

2015-03-01 * "Motmot Diving" | "Deposit for cenote diving"
  Expenses:Scuba                            50.00 USD
  Assets:Cash:Martin

以当地货币支付的现金开支

使用我们共同的比索现金支付现金开支如下所示:

2015-02-25 * "UltraMar Ferry across to Cozumel"
  Expenses:Transport:Bus                326 MXN
  Assets:Cash:Pesos

有时我们甚至不得不混合使用美元和比索支付。本例中,我们的比索用完了,因此必须同时支付美元和比索(图卢姆海滩区域的所有餐厅和酒店均接受美元):

2015-03-01 * "Hartwood" | "Dinner - ran out of pesos"
  Expenses:Restaurant                  1880 MXN
  Assets:Cash:Pesos                   -1400 MXN
  Assets:Cash:Martin                 -40.00 USD @ 12.00 MXN

我采用了餐厅提供的不利汇率来接受美元(当时市场汇率为 1 比 14.5)。

个人开支

以下是使用共同资金记录个人开支的示例。为了能够潜入珊瑚礁,我们每天需向岛屿支付 2.50 美元的“海洋公园”费用。这是一次短途旅行,我仅潜水了三天,而卡罗琳的费用已包含在其课程中,仅有一天除外:

  2015-02-25 * "Marine Park (3 days Martin, 1 day Caroline)"
    Expenses:Scuba:ParkFees:Martin             7.50 USD
    Expenses:Scuba:ParkFees:Caroline           2.50 USD
    Assets:Cash:Martin

唯一需要做的就是将这些费用记入我们名字对应的费用账户,最终会自动分开。

这里有一个更复杂的例子:科苏梅尔潜水俱乐部的潜水店在我们行程结束时给我们开了一张总账单,涵盖了所有租赁设备和额外潜水项目。我所做的只是将这份明细账单转换为一笔交易,并分别记入各自的账户:

2015-03-01 * "Scuba Club Cozumel" | "Dive shop bill" ^69b409189b37
  Income:Martin:CreditCard           -381.64 USD
  Expenses:Scuba:Martin                        27 USD ;; Regulator w/ Gauge
  Expenses:Scuba:Caroline                       9 USD ;; Regulator w/ Gauge
  Expenses:Scuba:Martin                        27 USD ;; BCD
  Expenses:Scuba:Caroline                       9 USD ;; BCD
  Expenses:Scuba:Martin                         6 USD ;; Fins
  Expenses:Scuba:Martin                        24 USD ;; Wetsuit
  Expenses:Scuba:Caroline                       8 USD ;; Wetsuit
  Expenses:Scuba:Caroline                       9 USD ;; Dive computer
  Expenses:Scuba:Martin                         5 USD ;; U/W Light
  Expenses:Scuba:Caroline                      70 USD ;; Dive trip (2 tank)
  Expenses:Scuba:Martin                        45 USD ;; Wreck Dive w/ Lite
  Expenses:Scuba:Martin                        45 USD ;; Afternoon dive
  Expenses:Scuba:Caroline                      45 USD ;; Afternoon dive
  Expenses:Scuba:Martin                     28.64 USD ;; Taxes
  Expenses:Scuba:Caroline                   24.00 USD ;; Taxes

最终余额

当然,你可以在旅行中的任何时候使用余额核对。例如,在离开坎昆机场前,我们知道短时间内不会再花任何墨西哥比索,于是我在卡罗琳决定把剩下的钱全花在机场商店高价巧克力上后,清点了我们剩余的金额:

2015-03-04 balance Assets:Cash:Pesos               65 MXN

理想情况下,记账人应在每天或每两天的安静时刻完成这项工作,这样更容易核对可能遗漏的支出(毕竟我们在度假,放松状态下难免会忘记记录一些事情)。

结清资产账户

旅行结束时,应通过将剩余资金转出给参与者(即转入收入账户)来结清所有资产和负债账户的最终余额。这将使旅行的所有账户余额归零,并确保所有预付的旅行费用均已分配给旅行者。

顺便说一句,谁保管这笔钱并不重要,因为最终我们会做一笔调整转账,以实现平均分摊。你可以将钱转给任何人;最终结果都是一样的。

我们的结清交易如下所示:

2015-03-06 * "Final transfer to clear internal balances to external ones"
  Assets:Cash:Pesos                               -65 MXN
  Income:Martin:Cash:Foreign                       60 MXN
  Income:Caroline:Cash                              5 MXN
  Assets:Cash:Martin                             -330 USD
  Income:Martin:Cash                              330 USD
  Assets:Cash:Caroline                           -140 USD
  Income:Caroline:Cash                            140 USD

2015-03-07 balance Assets:Cash:Pesos                0 MXN
2015-03-07 balance Assets:Cash:Pesos                0 USD
2015-03-07 balance Assets:Cash:Martin               0 USD
2015-03-07 balance Assets:Cash:Caroline             0 USD

我们有三张 20 比索纸币,我保留了这些纸币以备将来旅行使用。卡罗琳保留了 5 比索硬币(忘了把它当作小费交出去)。我们转出了旅行期间各自携带的现金金额。

如何做笔记

这次旅行中我没有带笔记本电脑——毕竟这是度假,我喜欢断开连接。我也没带笔记本。相反,我每天晚上在酒店的几张纸上做笔记。这个过程每晚大约花费 5 到 10 分钟,仅凭记忆回忆并记录下来。

这些笔记看起来像这样:

sharing-example-note-taking.png

我制作了一张纸质表格,每行包含:

  • 交易的说明(一段描述,便于日后选择对应的费用账户)

  • 谁支付的(资金来自哪个资产或收入账户)

  • 金额(以美元或墨西哥比索计)

旅行结束后,我坐在电脑前输入了对应的 Beancount 文件。如果我在度假期间有电脑,我可能会边走边录入。当然,由于一些错误,我不得不做了一些调整。

总之,只要你组织得当,这项工作的额外负担是极小的。

核对支出

在旅行文件上运行 bean-web:

bean-web beancount/examples/sharing/cozumel2015.beancount

你可以在“所有交易”视图中查看余额(点击“所有交易”)。

资产负债表应显示资产账户余额为空:

balsheet.png

权益账户的余额应反映旅行期间所有货币兑换的总额。你可以通过计算加权平均汇率来验证:7539.00 / 559.88 ~= 13.465 美元/墨西哥比索(这个数值是合理的)。

审查贡献

损益表应汇总所有支出和项目贡献:

income.png

收入账户余额显示了每个人贡献的总额。需要注意的是,在创建收入账户时,我额外创建了一些特定账户来区分每种付款来源,例如“卡罗琳的信用卡”等。

从这个视图中,我们可以看到我们为此次旅行总共贡献了 4254.28 美元(并剩余 65 墨西哥比索现金)。考虑到货币兑换,支出部分应与此匹配:3694.40 + 7474 / 13.465 ~= 4249 美元,结果基本吻合(微小差异可归因于汇率波动)。

如果你想查看贡献支付列表及最终余额,请点击某位旅行者的根账户,例如“收入:卡罗琳”(点击“卡罗琳”),这将带你进入该根账户的日记账:

caroline.png

该日记账包含其子账户的所有交易。底部的最终余额应显示这些账户的总余额,即卡罗琳为此次旅行的贡献金额:415 美元,并保留了 5 墨西哥比索(硬币)。我们同样可以查看马丁的账户,得出其最终余额为 3839.28 美元,并保留了 60 墨西哥比索(纸币)。

你也可以使用 bean-query 来获得相同的结果:

~/p/.../examples/sharing$ bean-query cozumel2015.beancount

Input file: "Mexico Trip: Cozumel & Tulum SCUBA Diving"
Ready with 105 directives (160 postings in 45 transactions).

beancount> SELECT sum(position) WHERE account ~ '^Income:.*Caroline'

sum_positio
-----------
-415.00 USD
   5    MXN

分摊支出

损益表的支出部分列出了各项支出明细。注意,某些支出账户已明确按成员分别记账,例如“支出:潜水:马丁”。而其他账户,例如“支出:杂货”,则计划进行分摊。

我们实现这一目标的方法是添加一个插件,该插件会将所有意图共享的交易(即不含任何成员名称的条目)实际拆分。例如,以下输入交易:

2015-02-28 * "Scuba Club Cozumel" | "Room bill"
  Income:Martin:CreditCard         -1380.40 USD
  Expenses:Scuba:Martin              178.50 USD
  Expenses:Accommodation            1201.90 USD

将被插件转换为如下形式:

2015-02-28 * "Scuba Club Cozumel" | "Room bill"
  Income:Martin:CreditCard         -1380.40 USD
  Expenses:Scuba:Martin              178.50 USD
  Expenses:Accommodation:Martin      600.95 USD
  Expenses:Accommodation:Caroline    600.95 USD

请注意:

  • 只有支出账户会被拆分为多个条目。

  • 根据惯例,账户名称中包含成员姓名的账户已被分配给该成员。为此,插件需要提供成员的姓名列表。

  • 所有生成的收入和支出账户均包含成员姓名。

启用该插件的方式如下:

plugin "beancount.plugins.split_expenses" "Martin Caroline"

取消输入文件中此行的注释后,重新加载网页并访问损益表页面,应会显示按个人分类的大量支出账户。现在,为了计算每位成员产生的总支出,我们需要汇总所有包含成员姓名的支出账户,例如“支出:.*马丁”。

目前网页工具尚不支持此类筛选功能1,但我们可以使用 bean-query 工具生成每位成员的支出总额:

beancount> SELECT sum(position) WHERE account ~ '^Expenses:.*Martin'

sum_positio
-----------
2007.43 USD
3837.0  MXN

beancount> SELECT sum(position) WHERE account '^Expenses:.*Caroline'

sum_positio
-----------
1686.97 USD
3637.0  MXN

这表示“马丁累计支出为 2007.43 美元和 3837.0 墨西哥比索。”

你可以手动将其转换为美元金额:

yuzu:~$ dc -e '2007.43 3837.0 13.465 / +p'
2291.43

或者,你可以使用 bean-query 最近引入的 “CONVERT” 函数来实现这一点:

beancount> SELECT convert(sum(position), 'USD') WHERE account ~ '^Expenses:.*Martin'

     convert_sum_position_c_     
---------------------------------
2288.528901098901098901098901 USD

beancount> SELECT convert(sum(position), 'USD') WHERE account ~ '^Expenses:.*Caroline'

     convert_sum_position_c_     
---------------------------------
1953.416886446886446886446886 USD

(2291.43 和 2288.53 之间的差异可归因于兑换交易中使用的汇率略有不同。)

类似地,你可以生成每个人支付的费用列表,例如:

beancount> SELECT sum(position) WHERE account ~ '^Income:.*Caroline'

最终转账

为了计算每位成员应支付的总金额,过程类似:只需将该成员名下所有账户的余额相加即可:

beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Caroline'

     convert_sum_position_c_     
---------------------------------
1538.783186813186813186813187 USD

beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Martin'

     convert_sum_position_c_      
----------------------------------
-1546.355494505494505494505494 USD

请注意,这包括了该成员的收入和支出账户。这就像将两个独立的账本合并为一个。(同样,微小的差异可归因于汇率随时间的变化。)

我们现在可以确定一个最终转账金额,以结算我们的各项开支;我们已同意将此金额四舍五入为 1500 美元:

2015-03-06 * "Final transfer from Caroline to Martin to pay for difference"
  Income:Caroline:Wallet                    -1500 USD
  Income:Martin:Wallet                       1500 USD

如果你取消注释输入文件中的这笔交易(靠近文件末尾),你将看到修正后的余额:

beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Martin'

     convert_sum_position_c_     
---------------------------------
-46.3554945054945054945054945 USD

beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Caroline'

    convert_sum_position_c_     
--------------------------------
38.7831868131868131868131868 USD

更新你的个人账本

太好了!现在我们已经对旅行中的开支进行了对账。但你仍需在个人账本中(如果你有的话)反映这些开支。本节将说明如何在该文件中记录这些交易。

账户名称

首先,请注意,你的个人账本中的账户名称无需与旅行账本文件中使用的账户名称完全一致。我从不将这些文件合并处理。(不建议合并的一个充分理由是,每次旅行都会引入其他人的账户名称,而我更希望避免这些名称污染我的主账本文件。)

使用标签

我喜欢使用标签来汇总与此次旅行相关的所有交易。在此示例中,我使用了标签 #trip-cozumel2015

记录贡献

你对项目的贡献应记入一个临时暂存账户。我将其命名为 “Assets:Travel:Pending”。例如,旅行账本中的这笔交易:

2015-02-25 * "Yellow Transfers" | "SuperShuttle to Playa del Carmen"
  Expenses:Transport:Bus                 656 MXN
  Income:Martin:CreditCard            -44.12 USD @ 14.86854 MXN

最终将被导入我的个人账本,并分类如下:

2015-02-25 * "YELLOW TRANSFER MX  CO" #trip-cozumel2015
  Liabilities:US:BofA:CreditCard      -44.12 USD
  Assets:Travel:Pending                44.12 USD

你可以将此理解为将资金投入一个待分配的现金池,之后你将从中支取相关开支。

我为旅行携带的现金也适用同样的处理方式:

2015-02-24 * "Taking cash with me on the trip"
  Assets:Cash                             -1200.00 USD
  Assets:Travel:Pending                    1200.00 USD

请注意,与旅行相关的所有交易都应记入该账户,包括账户间的转账:

2015-03-06 * "Final transfer from Mexico trip" #trip-cozumel2015
  Assets:Google:Wallet                       1500 USD
  Assets:Travel:Pending                      -1500 USD

记录支出

旅行结束后,我们需要将暂存账户的余额转换为一系列支出记录。要查询你自己的支出清单,可以执行如下查询:

beancount> SELECT account, sum(position) WHERE account ~ '^Expenses:.*:Martin' 
           GROUP BY 1 ORDER BY 1

            account             sum_positio
------------------------------- -----------
Expenses:Accommodation:Martin    735.45 USD
Expenses:Alcohol:Martin          483    MXN
Expenses:Bicycles:Martin          69.5  MXN
Expenses:Flights:Martin          488.00 USD
Expenses:Groceries:Martin        197.0  MXN
Expenses:Museum:Martin            64    MXN
Expenses:Restaurant:Martin        22.28 USD
                                1795.5  MXN
Expenses:Scuba:Martin            506.14 USD
Expenses:Scuba:ParkFees:Martin     7.50 USD
Expenses:Tips:Martin             225    MXN
                                 189.16 USD
Expenses:Transport:Bus:Martin    709    MXN
Expenses:Transport:Taxi:Martin    53.90 USD
                                 294    MXN
Expenses:Transport:Train:Martin    5.00 USD

理论上,你可以编写脚本来自动移除账户名称中的成员姓名,并直接输出格式化好的交易记录。但由于账户名称无法与你的个人账本一一对应,你仍需手动调整,因此我并未自动化这一过程。此外,旅行结束后你只需处理一次这类记录,不值得为此花费太多时间优化2

以下是最终交易的完整样子;我的输入文件中有一个这样的部分:

2015-02-25 event "location" "Cozumel, Mexico"    ;; (1)
pushtag #trip-mexico-cozumel-2015

… other transactions related to the trip…

2015-03-01 event "location" "Tulum, Mexico"      ;; (1)

2015-03-07 * "Final reconciliation - Booking pending expenses for trip"
  Expenses:Travel:Accommodation         735.45 USD
  Expenses:Food:Alcohol                  483.0 MXN
  Expenses:Sports:Velo                    69.5 MXN
  Expenses:Transportation:Flights       488.00 USD
  Expenses:Food:Grocery                  197.0 MXN
  Expenses:Fun:Museum                     64.0 MXN
  Expenses:Food:Restaurant               22.28 USD
  Expenses:Food:Restaurant              1795.5 MXN
  Expenses:Scuba:Dives                  506.14 USD
  Expenses:Scuba:Fees                     7.50 USD
  Expenses:Scuba:Tips                    225.0 MXN
  Expenses:Scuba:Tips                   189.16 USD
  Expenses:Transportation:Bus            709.0 MXN
  Expenses:Transportation:Taxi           53.90 USD
  Expenses:Transportation:Taxi           294.0 MXN
  Expenses:Transportation:Train           5.00 USD
  Assets:Cash:Foreign                     60.0 MXN                ;; (2)
  Assets:Cash                           330.00 USD                ;; (3)
  Assets:Travel:Pending               -2337.43 USD                ;; (4)
  Assets:Travel:Pending                -288.67 USD @@ 3897.0 MXN  ;; (5)

2015-03-07 * "Writing off difference as gift to Caroline"     ;; (6)
  Assets:Travel:Pending                     -43.18 USD
  Expenses:Gifts

2015-03-14 balance Assets:Travel:Pending     0 USD   ;; (7)
2015-03-14 balance Assets:Travel:Pending     0 MXN

poptag #trip-mexico-cozumel-2015

观察结果:

  1. 我使用了“event”指令来追踪我的位置。这样做是因为我将来需要这些信息用于移民目的(同时也为了好玩,记录我在各地停留的天数)。

  2. 我把随身携带的额外现金转移到了我的“外币现金”账户:Assets:Cash:Foreign

  3. 旅行归来后,我将随身携带的现金“转回”到原账户。

  4. 这一笔记录将 Pending 账户中的美元支出清零。

  5. 这一笔记录将 Pending 账户中的墨西哥比索支出清零。我手动计算了其金额(3897 / 13.5 ≈ 288.67 美元),并使用该完整金额作为汇率。

  6. 我希望在旅行结束后完全清空 Pending 账户,因此我核销了我们约定不支付的超额部分(不平衡额与 1500.00 美元之间的差额)。

  7. 最后,我断言 Pending 账户中已无美元和墨西哥比索余额。

我通过运行 bean-check 或在 Emacs 中对不完整的交易使用 bean-doctor context 找到了缺失的金额。

生成报告

如果你想为旅行中每位参与者自动生成报告,有一个脚本可以为前文提到的查询生成文本(最终为 CSV)格式的报告。你可以使用此脚本在旅行或项目进行中或结束后,为每位参与者提供支出状态。

该脚本位于 split_expenses 插件内部,调用方式如下:

python3 -m beancount.plugins.split_expenses <beancount-filename> --text=<dir>

对于每位参与者,它将生成以下报告:

  • 详细的支出日记

  • 详细的贡献日记

  • 按类别(账户类型)划分的支出明细

最后,它还会为每位参与者生成最终余额,你可以据此向彼此发送最终的结算转账。现在就尝试在 Beancount 提供的其中一个示例文件上运行吧。

其他示例

另一个示例文件展示了如何在三名参与者之间分摊费用,参见 duxbury2015.beancount。未来还将引入更多示例文件。

结论

我们这里描述的任务有不止一种实现方式。然而,我们提出的方法可以很好地扩展到更大的参与群体,能够处理某些支出虽由团体共同承担但实际只为某一个人产生的场景,并最终支持参与者之间非均等分摊。这种方法非常通用。

此外,将这种方法延伸至长期持续的项目也只需一步之遥。本文所呈现的思想为在简单场景中使用复式记账法提供了一个很好的实例。我希望通过这个案例,能帮助大家建立起使用复式记账法所需的直觉。


  1. 随着我们类似 SQL 的查询语言逐步成熟,bean-web 最终将允许用户根据从表达式筛选出的交易集合创建视图。这一功能将最终实现。

  2. 请注意,我们可以在 bean-query 工具中实现一种特殊的“Beancount”支持格式,以事务形式输出余额。我不确定这有多大用处,但这是一个值得在未来考虑的思路。