【注意】最后更新于 September 26, 2021,文中内容可能已过时,请谨慎使用。
上文中我将本地环境重新配置,又加上了远程记帐的功能。此文主要讨论我会如何优化帐目账本,并批量从银行帐单导入帐务。
帐目优化
之前使用 Beancount 的时候,各种帐目设置多由随手记转化而来,缺乏股票与税务的考量。所以这次我也花了一些时间来优化帐目配置。
首先是收入,我的收入由几部分组成:salary, sign-on, RSU 以及 401k contribution 这类 benefits,另外工资单上的税和实际交的税会有一个差别。另外我会有一些利息盈余收入。
1
2
3
4
5
6
7
8
9
10
11
12
|
;; taxable
2014-01-01 open Income:Hooli:Salary
2014-01-01 open Income:Hooli:Bonus
2014-01-01 open Income:Hooli:RSU
2014-01-01 open Income:Interest:Checking
2014-01-01 open Income:Interest:Savings
2017-01-01 open Income:Trade:PnL
;; pre-tax
2020-01-01 open Income:Hooli:Benefits
;; non-taxable
2014-01-01 open Income:Cashback
2014-01-01 open Income:Rebate
|
然后是支出,支出是因人而异的,不过基本上可以分为衣食住行娱乐几个大类与一些被动支出的项目。这次结构优化,修改了一些有问题的逻辑,比如将退税从收入挪去了支出。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
1990-01-01 open Expenses:Clothing
1990-01-01 open Expenses:Grocery
1990-01-01 open Expenses:Food
1990-01-01 open Expenses:Living
1990-01-01 open Expenses:House
1990-01-01 open Expenses:Transit
1990-01-01 open Expenses:Travel
1990-01-01 open Expenses:Fun
1990-01-01 open Expenses:Hobby
1990-01-01 open Expenses:Tax
1990-01-01 open Expenses:Health
1990-01-01 open Expenses:Commission
|
之后则是资产,这个比较简单,一般就是现金、银行账户、投资与固定资产。另外,车这类物品未来是有可能作为二手卖出的,所以也算成固定资产。衣物家具和玩具一类其实也可以资产这一类,不过对于我来说卖衣物玩具的可能性较低,暂且还是将其算成支出了。
1
2
3
4
5
6
7
|
2014-01-01 open Assets:US:Cash
2014-01-01 open Assets:US:Chase:Checking
2014-01-01 open Assets:US:Chase:Savings
2014-01-01 open Assets:Trade:Fidelity "FIFO"
2014-01-01 open Assets:Retirement:Fidelity
2014-01-01 open Assets:Health:HSA
2014-01-01 open Assets:Fixed:Car
|
另一项负债也比较简单,以信用卡为主。方便起见,我将礼卡,朋友之间的 AA,与旅行或医疗产生的待结算花销也放在这里。
1
2
3
4
5
|
2014-01-01 open Liabilities:US:Chase
2014-01-01 open Liabilities:US:Giftcard
2014-01-01 open Liabilities:Payables
2014-01-01 open Liabilities:Receivables
2014-01-01 open Liabilities:Settlements
|
帐本优化
一般来说如果帐目不太多的话,完全可以将所有帐目置于一个文档之中。然而我有从 2012 年开始八年多的帐目,一个文档有些臃肿。所以我大概按照如下逻辑将帐本分为一些较小的文件
- 帐户与货币的开销户 (open and close) 使用单独的文件记录;
- 使用一个单独的文件进行信用卡帐单结算余额 (statement balance) 的对帐与现金花销的补足 (pad)。
- 对于支出 (expense),将周期性开销与医疗费用(美国的医保报销相对来说比较复杂)分列单独文件外,其余按年记录。
- 对于收入 (income) 与资产 (assets),则将工资单,股票账户等单列。
- 另外,对于出游,由于一般来说都是拉帮结伙,所以单独放在 event 文件夹中。
- 还有一些预付型费用,如果是 ezpass 这类会给出帐目明细的,则也单独放在一个文件中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
option "title" "Accounting"
option "operating_currency" "USD"
;;; Setup
include "accounts.bean"
include "commodities.bean"
;;; Balance & Pad
include "balances.bean"
;;; Expenses & Liabilities
include "2020.bean"
include "living.bean"
include "health.bean"
;;; Income & Assets
include "paycheck.bean"
include "trade.bean"
include "crypto.bean"
;;; Events
include "event/2020-EventA.bean"
;;; Points
include "points/ezpass.bean"
|
另外,有时自己可能会写一些简单的插件,如果不想费事安装,也可以将其放入beancount 文件夹中用以下声明来使用
1
2
|
option "insert_pythonpath" "True"
plugin "plugins.beancount_share" "{}"
|
帐务导入
帐务导入是可以极大地缩短记帐耗时的一步,但是由于作者这方面的 documentation 写得不太好,我一直没搞明白,也就一直没用到 Beancount 的 Importer。这次由于我拖了半年没有记与女票共用信用卡的花费,只能苦心研究这个功能。对于帐务导入而言,我需要准备两个东西,一是 csv 格式的银行帐单,二是 自己写的 import 配置。
Chase, Amex 和 Citi 都是可以按帐单或自选日期导出近两年的帐目为 csv,而 BoA 只有 Checking 可以自选日期,credit card 只能按月导出。其中 Chase 信息最为充分,Amex 则只有选择导出为 xlsx 格式时才会附带分类信息。看起来 Chase 似乎很棒,但是由于 Beancount.core.data.Transaction
并没有 category 这个 attribute,所以除非重写 Importer function 或者直接从 row 中得到分类,否则也无法利用这个分类信息。
导出为 csv 后,我会做一些处理。首先删除不必要的 control-M
符号,然后将大写单词换成首字母大写。Citi 比较坑,分了借贷的同时还使用了正负号,需要移除。另外,我们有几张卡是共享帐户,所以需要添加一个共享的标记。此外,我还将文件名放在第一行,有助于 importer 对不同的卡进行识别。我本来想简单点直接用 sed
做这些处理,结果发现 MacOS 下的 sed
和 UNIX 是有区别的,不能使用 \u
等标识符。折腾了一下午后,在女票的指导下发现使用 perl
就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#!/bin/bash
for filename in temp/*.csv
do
echo $filename
basename=$(basename $filename)
sed -i '' $'s/\r//' $filename
perl -i -pe 's/.*/\L$&/g;s/(^| |,)./uc($&)/ge;s/,./\U$&/g' $filename
if [[ $filename = "temp/Citi"* ]]
then
sed -i '' 's/-\([0-9.]*\)/\1/g' $filename
fi
if [[ $filename = "temp/Chase0000"* ]] || [[ $filename = "temp/Citi0000"* ]]
then
sed -i '' '1s/$/,Tag/; 2,$s/$/,share-GF/' $filename
fi
sed -i '' "1i\\
$basename
" $filename
done;
|
针对不同银行帐目,import 配置会不太一样。比如 Chase 的帐单只有一个 amount,而 Citi 的帐单分为借贷两列,而且正负号是反的。其次,如果使用 beancount.ingest.importers.csv.Importer
的默认配置,是只会添加帐目发生的帐户,但不会有这项帐目的去向信息。我写了一个简单的 categorizer function 来实现这部分。另外,配置文件可以是 .import
也可以加上 beancount.ingest.scripts_utils
和相关 function 写成 .py
文件,但是后者在我这儿有些问题,我便采用了前者。以下是我简化的配置示例 config.import
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
#!/usr/bin/env
import os
import sys
from beancount.core import data
from beancount.ingest import extract
from beancount.ingest.importers import csv
def categorizer(txn):
account = ''
if txn.narration.startswith('Safeway'):
account = 'Expenses:Grocery:Safeway'
if account != '':
txn.postings.append(
data.Posting(account,
-txn.postings[0].units,
None, None, None, None))
return txn
CONFIG = [
# Chase
csv.Importer(
{
csv.Col.DATE: 'Transaction Date',
csv.Col.NARRATION: 'Description',
csv.Col.AMOUNT: 'Amount',
csv.Col.CATEGORY: 'Category',
csv.Col.TAG: 'Tag'
},
account='Liabilities:US:Chase',
currency='USD',
regexps='Chase0000',
skip_lines=1,
last4_map={
'0000': 'Chase',
},
categorizer=categorizer,
),
# Citi
csv.Importer(
{
csv.Col.DATE: 'Date',
csv.Col.NARRATION: 'Description',
csv.Col.AMOUNT_DEBIT: 'Debit',
csv.Col.AMOUNT_CREDIT: 'Credit',
csv.Col.TAG: 'Tag'
},
account='Liabilities:US:Citi',
currency='USD',
regexps='Citi0000',
skip_lines=1,
last4_map={
'0000': 'Citi',
},
categorizer=categorizer,
)
]
|
接下来,执行下面的语句即可将帐单导入并存为 temp.bean
文件。
1
|
bean-extract config.import temp > temp.bean
|
有些人可能希望每张卡的记录单独列出,那这个 temp.bean
文件就够用了 。我还是希望在最终的帐单中所有帐目按时间顺序排列,所以可以使用 bean-report
来达到效果。
1
|
bean-report temp.bean print > output.bean
|
系列链接