上文中我将本地环境重新配置,又加上了远程记帐的功能。此文主要讨论我会如何优化帐目账本,并批量从银行帐单导入帐务。

帐目优化

之前使用 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 年开始八年多的帐目,一个文档有些臃肿。所以我大概按照如下逻辑将帐本分为一些较小的文件

  1. 帐户与货币的开销户 (open and close) 使用单独的文件记录;
  2. 使用一个单独的文件进行信用卡帐单结算余额 (statement balance) 的对帐与现金花销的补足 (pad)。
  3. 对于支出 (expense),将周期性开销与医疗费用(美国的医保报销相对来说比较复杂)分列单独文件外,其余按年记录。
  4. 对于收入 (income) 与资产 (assets),则将工资单,股票账户等单列。
  5. 另外,对于出游,由于一般来说都是拉帮结伙,所以单独放在 event 文件夹中。
  6. 还有一些预付型费用,如果是 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

系列链接