AI+Beancount 复式记账个人务实

2025-06-01 blog life finance AI beancount LLM

AI+Beancount 复式记账个人务实

其实大概从本科以来,我一直在使用Beancount这个开源的复式记账工具来进行个人财务管理。但是之前记账实在是太繁琐和麻烦了,经常拖拖拉拉,记个大概。

改变发生在去年年底,从那时候开始,我开始引入大语言模型(LLM)来参与个人记账务实。从此,每个月的支付宝、微信和信用卡账单大概只需要十多分钟的就可以快速地导入进对账单。因此,2025年起我们的记账开始变得详细和完整,账本也更准确地反映出了个人财务状况。如今,我也有一些经验总结,希望做一个回顾和整理。

记账方案的顶层设计——权责发生制还是收付实现制

以往,大多数讲Beancount记账软件的博客通常会从单式记账(Single-entry Bookkeeping)和复式记账(Double-entry Bookkeeping)的区别谈起。但是今天,我想从一个更加深入也更本质的问题来谈起,因为这决定了我们账本的整体设计框架,也就是权责发生制和收付实现制。

简单回顾一下Beancount引入的复式记账,这引入了一个规则就是每一个交易(Transactions)均是由多个条目(Postings)来组成的,代表了资金的来龙去脉。在Beancount中,并非使用会计学中“借贷记账法”,而是简化为使用负数代表资金的来源(即贷方,Credit),正数代表资金的去向(即借方,Debit)。举一个简单的例子,假设你借给小明2000块钱,传统单式记账可能会记录一笔2000元的支出,而复式记账则同时记录这笔资金的从哪里来、到哪里去。:

2025-05-01 * "小明" "借款2000元"
  Assets:Bank:Cash                 -2000 CNY
  Assets:Receivables:XiaoMing       2000 CNY

这样,这一笔支出并不会记录为一笔支出(Expenses),而是记录为小明的应收账款中的金额。这样,你就知道这2000元并不是你这个月切实的消费,因此并不需要从个人的本月预算中省吃俭用出这2000元。

同样,当下个月小明连本带利归还2200元时,你的记账如下:

2025-06-01 * "小明" "还款2000元"
  Assets:Bank:Cash                   2200 CNY
  Income:PnL                         -200 CNY
  Assets:Receivables:XiaoMing       -2000 CNY

这代表着小明还清了钱,同时你的收益和损失收入(Profit & Loss, PnL)账户带来收益200元。因此,这个月你的净收益增加200元,而不是2200元。而你这个月也可以多消费200元(笑)。

从本质上看,Beancount的复式记账提供了更加灵活的记账能力,也因此引入了我们想要讨论的一个核心话题:是使用上面这个例子使用的权责发生制还是收付实现制来记账?让我来说明一下:

任何一个会计学教材都会跟你说,财务报表最重要的三大表是资产负债表(Balance Sheet)、利润表(Income Statement)和现金流量表(Cash Flow Statement),他们的主要特点如下:

  • 资产负债表衡量了一个时刻的所有资产和负债的表格。资产负债表主要考察如下三类财务科目,资产(Assets)、负债(Liabilities)和所有者权利(Equity,或称净资产)。在Beancount中,引入了如下的会计恒等式:资产+负债+净资产=0。值得注意的是,在Beancount中,负债和净资产等贷方账户都使用负数来记账。
  • 利润表衡量了一段时间内的收入、费用和净利润等情况,主要考察了如下两类财务科目支出(Expenses)和收入(Income)。利润表使用的是权责发生制来记录,即在这个交易产生权利和责任的那一天入账(例如发生交易、签订合同等)。假设你租房每个月房租是2000元,三个月一付。按权责发生制,则应该记录为在实际使用房屋的时候(即每个月),产生支付义务的时刻记账:
2025-03-01 * "房租实预缴"
  Assets:Cash                -6000 CNY
  Assets:Prepaid              6000 CNY

2025-03-01 * "房租(按月发生)"
  Expenses:House              2000 CNY
  Assets:Prepaid             -2000 CNY

2025-04-01 * "房租(按月发生)"
  Expenses:House              2000 CNY
  Assets:Prepaid             -2000 CNY

2025-05-01 * "房租(按月发生)"
  Expenses:House              2000 CNY
  Assets:Prepaid             -2000 CNY

; 当然,后面会介绍使用beancount-periodic插件更优雅地处理摊销的办法
  • 现金流量表则衡量了一段时间内现金的流入流出情况。和利润表不同的是,现金流量表按照收付实现制来记账的,也就是在资金实际变动的那一天记一次账。上面租房的例子如果按照收付实现制,则应该记录为如下一笔:
2025-03-01 * "房租实缴纳"
  Assets:Cash                -6000 CNY
  Expenses:House              6000 CNY

显然,权责发生制和收付实现制各有所长。从上面这个例子可以看出,权责发生制更容易方便我们进行中期和长期的个人财务管理。比如,将房租消费摊销到每一个月之后,你就清楚地看到每个月房租支出2000元。因此每个月的预算可能需要扣除这2000元。同时,基于收付实现制的现金流量表也同样重要,可以用于展示个人短期的现金压力。比如,当你身上只有7000元现金的时候,一次性支付6000元房租也势必会对个人现金流产生极大压力。

然而,Beancount默认情况下只提供了一种资金流动的视图,这意味着你只能在权责发生制和收付实现制两种记账方法之间选择一种。对于大多数人来说,使用收付实现制通常是一个简单且实用的选择,也就是跟其他所有个人记账软件一样,在实际发生交易的时候进行记账。但是对于我来说,随着房租、车、信用卡等资产构成日益复杂,我既需要了相对更复杂的权责发生制来进行固定资产折旧摊销等管理,同时也想对个人实际收付的现金流进行管理。对此,我首先使用了权责发生制来进行利润表构建。而后,我使用了fava-dashborad插件,通过类似【间接法】的方式,编撰了现金流量表。具体构建方法下文会提到。通过这种方式,我同时实现了三大财务报表的构建,让我对个人财务状况也有了清晰的管理和认知。

在本章节的最后,我简单摘录了我在个人账本中常使用的会计科目。这些科目和企业常用的中国会计准则(CAS)及国际财务报告准则(IFRS)中的标准科目显然不对应,仅用做参考:

; 资产(Assets)对应了各类银行账户、固定资产等
2017-01-01 open Assets:Bank:BoC:Cash CNY ; 中国银行储蓄卡
2017-01-01 open Assets:Bank:BoC:Positions CNY ; 中国银行理财或定期
; ... 其他借记卡账户
2017-01-01 open Assets:Web:AliPay:Cash CNY ; 支付宝活期或余额宝
2017-01-01 open Assets:Web:AliPay:Positions "FIFO" ; 支付宝定期或基金
2017-01-01 open Assets:Web:WeChat:Cash CNY ; 微信钱包零钱或零钱通
2017-01-01 open Assets:Receivables ; 应收账款
2017-01-01 open Assets:Receivables:LandLord ; 租房房东押金,应收账款
; ... 其他应收账款
2017-01-01 open Assets:Fixed:House CNY ; 固定资产——房产
2017-01-01 open Assets:Fixed:Car CNY ; 固定资产——汽车
2017-01-01 open Assets:Fixed:Park CNY ; 固定资产——车位
2017-01-01 open Assets:Fixed:Laptop CNY ; 固定资产——笔记本电脑
2025-06-01 open Assets:Voucher ; 各种工会福利或代金券
2017-01-01 open Assets:Salary:HousingFund ; 公积金账户
2017-01-01 open Assets:Salary:HealthInsurance ; 个人医疗保险账户
2017-01-01 open Assets:Salary:SocialInsurance ; 社保账户
2017-01-01 open Assets:Salary:PersonalSocialInsurance ; 个人社保账户(企业年金)

; 负债(Liabilities)账户
2017-01-01 open Liabilities:Bank:BoC:CN CNY ; 中国银行信用卡
2017-01-01 open Liabilities:Bank:BoC:US USD ; 中国银行信用卡(Visa)
2017-01-01 open Liabilities:Bank:CCB:CN CNY ; 建设银行公务卡
2017-01-01 open Liabilities:Others ; 其他负债

; 所有者权益或净资产(Equity)
1990-01-01 open Equity:Opening-Balances ; 开始记账的初始资金
1990-01-01 open Equity:Family ; 来自家人的大额转账
1990-01-01 open Equity:Salary ; 缴纳的社保、医保、公积金和企业年金费用(不计入income)
1990-01-01 open Equity:Others ; 其他资金
2017-01-01 open Equity:Amortization:Rent ; 预付房租,摊还账户

; 收入(Income)
2017-01-01 open Income:Salary:Company ; 工资
2017-01-01 open Income:Family ; 来自家庭成员的红包和收入
2017-01-01 open Income:PnL ; 各种投资收益
2017-01-01 open Income:Others ; 其他收入

; 各项支出(Expenses)
2017-01-01 open Expenses:Clothing ; 衣服的支出
2017-01-01 open Expenses:Food ; 食物的支出
2017-01-01 open Expenses:Traffic ; 交通费用,包括但不限于单车、出租车等
2017-01-01 open Expenses:Gas ; 汽油费用
2017-01-01 open Expenses:Shop ; 日常购物费用
2017-01-01 open Expenses:Entertainment ; 娱乐支出
2017-01-01 open Expenses:Gift ; 给各种人的礼物费
2017-01-01 open Expenses:Rent ; 租房费用
2017-01-01 open Expenses:Expense ; 水、电、燃气费和电话费等
2017-01-01 open Expenses:Subscribe ; 各种软件订阅费用
2017-01-01 open Expenses:Tax ; 各种税和手续费
2017-01-01 open Expenses:Depreciation:Fixed:House ; 房屋折旧费用
2017-01-01 open Expenses:Depreciation:Fixed:Park ; 停车位折旧费用
2017-01-01 open Expenses:Depreciation:Fixed:Car ; 汽车折旧费用
2017-01-01 open Expenses:Depreciation:Fixed:Laptop ; 笔记本电脑折旧费用
2017-01-01 open Expenses:Others ; 其他支出

工资、预付账款和应收账款等的管理

最简单的交易的记账方法通常不会存在什么歧义,因此这个博客也就不再赘述了。我们直接从相对来说比较困扰的一些条目开始说起,比如说工资怎么记录。

工资通常有好几种记录方法,详细程度各有区别。举一个例子,假设小王税前工资10000元,其中应付个人所得税50元,个人缴纳公积金账户1000元,税后实得工资8950元。那么最简单的记录方法可以是:

2025-06-01 * "5月份工资"
  Assets:Cash                8950 CNY
  Income:Salary:Company     -8950 CNY

当然,你也可以记录的更加详细,来同时记录税前工资、税后工资、税和各种缴纳费用。比如:

2025-06-01 * "5月份工资"
  Assets:Cash                 8950 CNY
  Income:Salary:Company     -10000 CNY
  Expenses:Tax                  50 CNY ; 个人所得税 
  Expenses:HousingFund        1000 CNY ; 公积金,个人缴纳部分

通过这种方式,你记录了收入为10000元,同时有50元支出和1000元缴纳个人公积金。更进一步地,如果你想使用一个独立的账户来管理你的公积金、养老金、个人医保账户等,你也可以更进一步记录如下:

2025-06-01 * "5月份工资"
  Assets:Cash                 8950 CNY ; 个人现金增加 8950 元
  Income:Salary:Company     -10000 CNY ; 个人税前工资 10000 元
  Expenses:Tax                  50 CNY ; 个人所得税 50 元
  Equity:Salary:Company      -1000 CNY ; 企业按1:1缴纳的公积金 1000 元,记录在了所有者权益之下。
  Assets:HousingFund          2000 CNY ; 个人缴纳1000元,企业缴纳1000元,公积金账户实际增长2000元。

我们进一步引入了一个权益账户(Equity:Salary:Company)来记录公司为你支付的工资之外的额外报酬。在这个例子中,企业和个人按照1:1缴纳公积金,因此企业支付工资外的费用为 1000 元。同时,个人的公积金账户资产(Assets:HousingFund)增长为2000元。

通过这种方式,你使用公积金来支付房贷的时候,你就可以使用类似如下的方式来记录:

2025-06-01 * "6月公积金支付房贷"
  Assets:HousingFund        -1000 CNY ; 公积金账户支出1000元
  Liabilities:Hoursing       1000 CNY ; 支付房贷1000元

另一类比较复杂的记账是公务出差或者多个人一起出门旅游的场景。这种情况下通常会让人不知道该如何记账。假设你公务出差,5月1日个人使用支付宝支付了1000元机票,6月1日公司完成报销。那么有如下几种记账方式:

  • 1.按照收付实现制记账:
2025-05-01 * "个人垫付机票"
  Expenses:Traffic        1000 CNY
  Assets:Alipay          -1000 CNY

2025-06-01 * "公司报销"
  Income:Company         -1000 CNY
  Assets:Cash             1000 CNY

但是这样似乎有一个问题,没有办法将这两笔交易关联起来。在5月1日和6月1日之间,你没有办法记录公司实际上欠了你1000元。对此,按照复式记账法,似乎可以使用【应收账款】科目来记录如下:

  • 2.使用应收账款科目记录:
2025-05-01 * "个人垫付机票"
  Assets:Alipay                        -1000 CNY
  Assets:Receivables:Company            1000 CNY ; 公司的应收账款科目增加应收款1000元

2025-06-01 * "公司报销"
  Assets:Cash                           1000 CNY
  Assets:Receivables:Company           -1000 CNY

但是这样同样又引入了一个问题,因为有人说我这一笔支出确实是一笔用于交通费的支出,但是上述的记账方式确实没有体现。那么应该如何处理呢?

  • 3.使用权责发生制记账
2025-05-01 * "个人垫付机票"
  Expenses:Traffic                      1000 CNY ; 个人支付记录1000元
  Assets:Alipay                        -1000 CNY
  Income:Company                       -1000 CNY ; 
  Assets:Receivables:Company            1000 CNY ; 公司的应收账款科目增加应收款1000元

2025-06-01 * "公司报销"
  Assets:Cash                           1000 CNY
  Assets:Receivables:Company           -1000 CNY

这里我们增加了一个来自公司的收入1000元(Income:Company),这是因为我们使用权责发生制来进行记账。当公司准许我们出差报销时候,已经代表了公司报销飞机票的义务已经产生,按权责发生制可以计提为收入。只是,这一个收入并非是作为现金的形式,而是作为应收账款的形式出现。

这种记账最大的好处在于我们可以同时导出利润表和现金流量表:

(1)从利润表的角度,支出(Expenses)和收入(Income)科目相抵,代表本次交易的飞机票实际上并不会对个人收支产生影响,这是因为飞机票实际上是公司付款的。从预算管理的角度,你也不需要因为5月份出差提前垫付了飞机票而需要扣除自己1000元生活费。

(2)从现金流量表的角度,我们假设Assets:Alipay和Assets:Cash都是我们的现金账户。因此我们可以轻松得到处实时的个人现金量变换。当垫付飞机票之后,我们确实可以看出现金减少1000元(Assets:Alipay -1000 CNY);当公司报销后,现金确实增加了1000元(Assets:Cash 1000 CNY)。那要具体怎么才能导出现金流量表呢?一种简单的做法是使用 Beancount Query Language (BQL),这是一个类似SQL的查询语言:

select account, SUM(position) from (account ~ "Assets:(Cash|Alipay)") and date > 2025-05-01 and date  2025-06-01 order by account;

当前,这种做法只能按现金账户分别统计金额变动情况。本文后面会介绍一种类似与现行CAS标准的现金流量表,依据经营、投资和融资产生的现金流三块开展分析的个人现金流量表编制方法。

固定资产折旧和无形资产摊销的管理

更进一步的,我们来介绍更复杂一些的情形。首先是固定资产(Fixed Asset)入资和折旧处理。对于个人来说,固定资产通常是房、车等。假设小明在5月1号花100万元购买了房产一套。此时存在两种记账方式,其一是将其记录为一笔支出:

  • 1.记录购房为支出
2025-05-01 * "购买房子"
  Expenses:House        1,000,000 CNY
  Assets:Cash          -1,000,000 CNY

但如果这样做,会导致本月利润表出现一笔巨额支出费用,通常远超其他项目支出和预算,最终会导致利润表将无法可靠展示本月资金损益情况。显然,这个月开销是1万元或是100元对于这一笔100万元的支出而言都显得微不足道了。对此,通常将大额支出记录为固定资产投资。如下所示:

  • 2.记录购房为固定资产
2025-05-01 * "购买房子"
  Assets:House          1,000,000 CNY
  Assets:Cash          -1,000,000 CNY

此时,购买房屋的交易将100万元现金转换为了固定资产,从而不再利润表中出现。可是等等,这里有出现了两个新的问题:1)毕竟房屋使用是有成本的。如何以恰当的方式,在利润表里面衡量使用这个房子所产生的成本呢?2)未来随着房屋的入住、使用和老旧,这个房子可能不值100万元了。此时,房屋账面价值(Book Value)和公允价值(Fair Value)将存在较大偏差,进而影响整个账本的有效性。对此,应该如何处理呢?

根据一般会计准则,通常会对固定资产使用和损耗导致的资产价值变动进行刻画衡量,这一个过程被称为折旧(Depreciation)。在Beancount中,我们可以做如下处理:

  • 3.使用beancount-periodic插件进行资产折旧处理:
; pip install beancount-periodic
plugin "beancount_periodic.depreciate"

2025-05-01 * "购买房子"
    Assets:Cash               -1,000,000 CNY
    Assets:House               1,000,000 CNY
      depreciate: "20 Year @2026-01-01 /Monthly = 500000"

在上面的例子中,我们通过plugin指令使能了beancount_periodic插件的depreciate功能。而后在固定资产的Assets:House下加入了一个新的描述(Narration),意思是从2026年1月1日(假设为交房日起),连续20年,没一个月资产连续减值,直到期末价值为500,000万元。这意味着,我们测算每一个该房屋价值减少约为2083.33元,这等价于该插件从2026年1月1日起,每个月自动插入了如下一笔交易:

 2026-01-01 * "购买房子(资产折旧)"
   Assets:House                -2083.33 CNY
   Expenses:Depreciate:House    2083.33 CNY
 2026-02-01 * "购买房子(资产折旧)"
   Assets:House                -2083.33 CNY
   Expenses:Depreciate:House    2083.33 CNY 
;...   
 2046-01-01 * "购买房子(资产折旧)"
   Assets:House                -2083.33 CNY
   Expenses:Depreciate:House    2083.33 CNY 

从本质上看,这种记账方式并不是将购买该房屋的1,000,000元一致性记录为支出,而是将其分担到了未来的20年中,每一个月支出的房屋使用成本为2083.33元。这样做带来了如下两个好处:

1)使用了权责发生制,将购买该资产的成本均摊到未来没一个月中,将更精准的衡量房屋的使用成本,从而使得当期和未来的利润表更能反映当期个人盈利状况。

2)同时也可以作为个人预算规划使用。该使用成本表示,如果未来每个月的利润表(计入了该项房屋使用成本)恰好实现收支平衡,也就是说当期其他各项支出节约出来2083.33元,则在20年后,所积攒资金可以重新购置同等价值的资产。

最后,固定资产也可以被卖出处理。例如,假设20年后,该房产以120万元价格被卖出。我推荐进行如下两步的简化处理:(1)第一步,首先注释掉原始交易中的depreciate叙述。也就是我们不再需要通过估算方式来计算每个月的使用成本了。(2)进行正常的资产交易处置:

2046-01-01 * "出售房子"
    Assets:Cash                1,500,000 CNY
    Assets:House              -1,000,000 CNY
    Income:PnL                   500,000 CNY; 房屋出售利润

当然你可以更加严谨地正常计算固定资产的折旧,并使用如下等价处理:

2046-01-01 * "出售房子"
    Assets:Cash                1,500,000 CNY
    Assets:House                -500,000 CNY; 由于房屋折旧,当时房屋剩余价值为50万元,全部清空
    Income:PnL                 1,000,000 CNY; 因此实际利润为100万元(包括你使用房屋成本的50万元和出售额外获得的50万元)

除了房屋、汽车等固定资产外,另一类需要做类似处理的资产是无形资产,通常包括各项每年缴纳的汽车保险、租房等信息。还是以上文中介绍的那个租房三个月一付,每个月房屋2000元的例子来谈。为了将租房成本均摊到没一个月(同样是为了更好的进行利润和预算管理),我们同样可以使用beancount_periodic插件的摊销模块,如下所示:

2025-06-01 * "三个月房租"
  Assets:Cash             -6000 CNY
  Expenses:Rent            6000 CNY
    amortize: "3 Months /Monthly"

对于这一笔交易,Beancount会自动将其转化为如下几个交易:

2025-06-01 * "三个月房租"
  Assets:Cash                        -6000 CNY
  Equity:Amortization:Rent            6000 CNY

2025-06-01 * "三个月房租" ; 6月份房租
  Equity:Amortization:Rent            -2000 CNY
  Expenses:Rent                        2000 CNY

2025-07-01 * "三个月房租" ; 7月份房租
  Equity:Amortization:Rent            -2000 CNY
  Expenses:Rent                        2000 CNY

2025-08-01 * "三个月房租" ; 8月份房租
  Equity:Amortization:Rent            -2000 CNY
  Expenses:Rent                        2000 CNY

在支付房租的交易中,beancount_periodic插件将房租首先存放到了Equity:Amortization:Rent科目中,然后在后续每个月中,按照实际房租金额2000元计提到房租支出。再次强调,这么做的好处就是可以清晰地在每一个月的利润表中看出来支付了2000元房租,因而为实现良好的财务状况,可能需要从工资(Income)中保留该2000元。

基金投资和浮动收益管理

本博客其实之前介绍了我们自研的基金实时估值面板和Realworld基金筛选模型。其实,确实本人进行了长期的基金和股票投资。因此,对于我个人来说,对基金、股票甚至比特币这些浮动资产进行记账同样也是必要需求。我们简单讨论一下如何计算股票、基金这些浮动收益。假设我们需要购买代码为000001的基金(这里只用作例子,不做实际推荐),假设6月1日买入1000元,成本为1元,手续费为0.5%。我们可以进行如下记账:

2017-01-01 open Assets:AliPay:Positions "FIFO" ; 基金账户开户

2025-06-01 commodity FUND_000001      ; 注册了名字为 000001 的通货代币
  price: "CNY:eastmoneyfund/000001"   ; 这里使用 beanprice 插件,支持自动从天天基金网获取该基金的最新净值

2025-06-01 * "buy 000001"
    Assets:Cash                    -1000 CNY
    Expenses:Tax                       5 CNY                 ; 手续费
    Assets:AliPay:Positions         +995 FUND_000001 {1 CNY} ; 按1元成本购入了995份额的000001基金

简单解释一下上面的指令:

  1. open指令进行开户操作,新开的账户名称为Assets:AliPay:Positions。值得注意的是,我们标注了其具有FIFO属性。这意味着当同一个基金多次购入(例如定投)的情况下,在卖出时候按照先买先卖(First-In-First-Out)的顺序执行交易,也就是首先卖出最早购买的份额,然后再卖出后买入的份额。除了FIFO,还有LIFO(Last-In-First-Out)和手动指定的多种卖出策略选项。显然不同的卖出策略会带来净收益的略微差异。
  2. commodity 指令定义了一个新的代币,在本例中的代币名称是FUND_000001。此外,注意到下面的price描述"CNY:eastmoneyfund/000001",这里我们其实使用了beanprice插件。这个插件将自动帮助我们更新来自于Coinbase、Yahoo或天天基金网的各类资产的实时价格。在本利中,我们将货币FUND_000001和天天基金网上代码为000001的基金绑定起来。这之后,我们只需要每天在收盘后执行如下指令即可完成实时价格的自动导入:
bean-price --update main.beancount > fund.beancount

这个指令的意思是说,将读取main.beancount账本(包括其include的子账本)中的各种货币,并更新其价值到fund.beancount 文件中。此时fund.beancount文件的可能输出是:

2025-06-01 price FUND_000001                        1.1 CNY
2025-06-02 price FUND_000001                        1.2 CNY

显然意味着各种基金的实时价格已经被导入我们的账本之中了。我们可以使用fava这个beancount的webgui界面点击“按市价”或“按成本”按钮,即可实现资产的成本价和市场价的切换显示,也可以灵活现实该资产的实时浮动收益。在卖出的资产时候,也同样十分简单。只需要执行如下记账即可:

2025-07-01 * "sale 000001"
    Assets:Cash                     1194 CNY
    Assets:AliPay:Positions         -995 FUND_000001 @ 1.2 CNY

其中,一个美元符号代表指定了本次交易的单价,上个例子代表了本次交易的担架为每份额 1.2 CNY 价格卖出。

两个美元符号代表了指定了本次交易的总价。例如:

2025-07-01 * "sale 000001"
    Assets:Cash                     1194 CNY
    Assets:AliPay:Positions         -995 FUND_000001 @@ 1194 CNY

则代表了 995 份额的FUND_000001按照总价1194 CNY价格卖出。

Fava-dashboards 现金流量表

在前文的所有叙述中,我们都是依据权责发生制来记账的,因此beancount可以直接渲染处较为精确的利润表。但是我们在前文也提到了,观察中短期的现金流量变化同样非常重要。举一个例子,比如上述购房的交易:

2025-05-01 * "购买房子"
  Assets:House          1,000,000 CNY
  Assets:Cash          -1,000,000 CNY

虽然没有被记录为一笔支出(Expenses),但是短期100万元的现金流出也一定会对现金流产生较大影响。因此现金流量表也是我希望能够长期观察的重要财务报表之一。然而Beancount默认并不支持展示现金流量表,我们只能自己手搓了。我给出的解决办法是使用 fava-dashboards 插件。这个插件的主要功能是可以自定义各种BQL查询语句,然后通过html、jinja2等多种方式,将数据、图表渲染到fava webgui中。首先安装和配置 fava-dashboards:

; pip install git+https://github.com/andreasgerstmayr/fava-dashboards.git

2099-01-01 custom "fava-extension" "fava_dashboards" "{
    'config': 'dashboards.yaml'
}"

在配置文件中我们制定了dashboards.yaml为fava-dashboards的主配置文件。

下面具体介绍我们是怎么构建现金流量表的,我们使用了现金流量表的【间接法】编制方法,也就是以本期净利润为起点,通过调整不涉及现金的收入、费用、应收应付等项目的增减变动的方式,计算并列报本期现金流量净值变动

整个调整过程比较复杂,但是我们可以举前文所述的租房的例子来说明。

步骤一:计算出当期净利润

首先,我们可以简单计算出净利润:

/* 计算净收入 */
select sum(number) as value where account ~ "Income.*" and NOT (account ~ "Income:PnL.*") and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY"

/* 计算净支出 */
select sum(number) as value where account ~ "Expenses.*" and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";

/* 注:ledger.dateFirst 和 ledger.dateLast 是 fava-dashboards引入的两个变量,分别代表着本期期初日期和期末日期*/

通过上面两个值作差,我们可以简单的计算出当期净利润(Net Profit)。

步骤二:对非现金交易进行调整

然后,我们再对每一笔涉及到非现金的交易进行调整。例如在前文所述的租房案例中,我们涉及到对如下两种交易进行调整:

  • 1.在实际每三个月一付房租的交易中
2025-06-01 * "三个月房租"
  Assets:Cash                        -6000 CNY
  Equity:Amortization:Rent            6000 CNY

由于这一笔交易并不涉及Income和Expenses账户,因此该交易没有被统计在利润表中(利润表正是使用Income和Expenses账户编制的)。但是这里发生了实际的现金变动,表现为Equity:Amortization:Rent账户的增加。对此,我们使用如下的BQL语句将上述交易提取出来,获得其资金变化(6000元现金支出)并将其调整到净利润中,也就是净利润需要减去6000元:

/* 计算租房一次性付出的房租 */
select sum(number) as value where account ~ "Equity:Amortization.*" and number > 0 and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";
  • 2.对于每一个月支付的房租2000元:
2025-06-01 * "三个月房租" ; 6月份房租
  Equity:Amortization:Rent            -2000 CNY
  Expenses:Rent                        2000 CNY

2025-07-01 * "三个月房租" ; 7月份房租
  Equity:Amortization:Rent            -2000 CNY
  Expenses:Rent                        2000 CNY

2025-08-01 * "三个月房租" ; 8月份房租
  Equity:Amortization:Rent            -2000 CNY
  Expenses:Rent                        2000 CNY

我们是使用摊销的方式计提到利润表中,而这里其实并没有发生实际的现金交易,所以我们同样需要对净利润进行调整,也就是将净利润加上这里支出的费用。我们使用如下BQL查询元数据来提取上述交易,并汇总其金额:

select sum(number) as value where account ~ "Expenses:Rent.*" and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";

在举一个购买固定资产的例子。还是购入房产的例子,这里同样涉及到两类交易:

  • 1.购买固定资产时候的实际金额支付:
2025-05-01 * "购买房子"
    Assets:Cash               -1,000,000 CNY
    Assets:House               1,000,000 CNY

显然这里是发生了现金流量变动。这类交易的特征为固定资产账户金额增加,因此我们使用如下BQL语言查询到所有类似的交易,并将其调整到净利润中:

select sum(number) as value where account ~ "Assets:House.*" and number > 0 and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";
  • 2.对于每个月固定资产折旧开销:
 2026-01-01 * "购买房子(资产折旧)"
   Assets:House                -2083.33 CNY
   Expenses:Depreciate:House    2083.33 CNY

显然其记录到了利润表中(因为Expenses账户发生变动),而这笔交易实际上并不是现金交易。因此我们需要对所有的折旧交易进行提取,并将其调整到利润表中:

select sum(number) as value where account ~ "Expenses:Depreciation.*" and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";

同样,我们对报销、应收账款、预付账款、应付账款和各类信用卡交易均需要做类似调整,具体方式不再赘述。

步骤三:最终根据净利润和上述各项调整值,编制现金流量表

我们最后根据先前通过多个BQL查询获取的净利润和各项调整值,按照经营活动、投资活动和融资活动为分类,最终汇总形成现金流量表。fava-dashboards的config代码为:

- name: 现金流量表
  panels:
  - title: 现金流量表
    width: 100%
    height: 700px
    queries:
    - bql: select sum(number) as value where account ~ "Income.*" and NOT (account ~ "Income:PnL.*") and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY"
    - bql: select sum(number) as value where account ~ "Expenses.*" and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";
    - bql: select sum(number) as value where account ~ "Expenses:Depreciation.*" and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";
    - bql: select sum(number) as value where account ~ "Expenses:Rent.*" and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";
    - bql: select sum(number) as value where account ~ "Equity:Amortization.*" and number > 0 and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";
    - bql: select sum(number(value(position))) as value where account ~ "Liabilities.*" and number > 0 and (narration ~ "(公务卡|信用卡|还款|转账)" or payee ~ "(公务卡|信用卡|还款|转账)") and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";
    - bql: select sum(number(value(position))) as value where account ~ "(Liabilities|Assets:Salary).*" and number < 0 and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";
    - bql: select sum(number) as value where ((account ~ "Assets:Receivables:LandLord" and number < 0) or account ~ "Assets:Receivables:(Lab|USTC)") and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";
  
    - bql: select sum(number) as value where account ~ "Assets:Fixed.*" and number > 0 and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";
    - bql: select sum(number(value(position))) as value where account ~ "Assets:.*:Positions" and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}};
    - bql: select sum(number) as value where account ~ "Equity:Family" and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";
    - bql: select sum(number) as value where account ~ "Equity:(Others|Opening-Balances)" and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";
    - bql: select sum(number) as value where account ~ "Income:PnL.*" and date >= {{ledger.dateFirst}} and date <= {{ledger.dateLast}} and currency = "CNY";

    - bql: select sum(number(value(position))) as value from (account ~ "Assets:.*Cash") and date < {{ledger.dateFirst}}
    - bql: select sum(number(value(position))) as value from (account ~ "Assets:.*Cash") and date <= {{ledger.dateLast}}
    type: html
    script: |
      const attris = ["income", "expense", "depreciation", "amortization", "prepaid", 
        "pay_liability", "expense_use_liability", "receivables", "fix", "positions", "equity_family", "equity_others", "equity_future"];
      
      console.log(attris);

      const attr_map = {};

      for (let idx in attris) {
        let key = attris[idx]
        let result = panel.queries[idx].result;
        let value = 0;
        if (result.length > 0) {
          value = -result[0].value
        }
        attr_map[key] = value
      }

      attr_map['prev'] = panel.queries[13].result[0].value;
      attr_map['final'] = panel.queries[14].result[0].value;

      attr_map['depreciation'] = - attr_map['depreciation']
      attr_map['amortization'] = - attr_map['amortization']

      attr_map['cash_expense'] = attr_map['expense']

      attr_map['part_0_net_income'] = attr_map['income'] + attr_map['cash_expense']
      attr_map['part_1'] = attr_map['part_0_net_income'] + attr_map['depreciation'] + attr_map['amortization'] + attr_map['prepaid'] + attr_map['receivables']
      attr_map['part_2'] = attr_map['fix'] + attr_map['positions'] + attr_map['equity_future']
      attr_map['part_3'] = attr_map['equity_family'] + attr_map['equity_others'] + attr_map['pay_liability']  + attr_map['expense_use_liability'] 
      attr_map['part_4'] = attr_map['part_1']+attr_map['part_2'] + attr_map['part_3']

      const adjust = attr_map['final'] - attr_map['prev'] - attr_map['part_4']

      console.log(attr_map)
      let content = `
        <table>
          <tr><th><strong>课目明细名称</strong></th><th><strong>本期变动</strong></th></tr>
          <tr><td><strong>1. 经营活动产生的现金流量</strong></td><td><strong></strong></td></tr>
          <tr><td>现金收入</td><td>${attr_map['income'].toFixed(3)}</td></tr>
          <tr><td>现金支出</td><td>${attr_map['cash_expense'].toFixed(3)}</td></tr>
          <tr><td><strong>净利润</strong></td><td><strong>${attr_map['part_0_net_income'].toFixed(3)}</strong></td></tr>
          <tr><td>固定资产折旧</td><td>${attr_map['depreciation'].toFixed(3)}</td></tr>
          <tr><td>无形资产摊销</td><td>${attr_map['amortization'].toFixed(3)}</td></tr>
          <tr><td>预付款产生的现金</td><td>${attr_map['prepaid'].toFixed(3)}</td></tr>
          <tr><td>应收账款产生的现金</td><td>${attr_map['receivables'].toFixed(3)}</td></tr>
          <tr><td><strong>经营活动产生的现金流量</strong></td><td><strong>${attr_map['part_1'].toFixed(3)}</strong></td></tr>
          <tr><td><strong>2. 投资活动产生的现金流量</strong></td><td><strong></strong></td></tr>
          <tr><td>收回投资收到的现金</td><td>${attr_map['positions'].toFixed(3)}</td></tr>
          <tr><td>投资活动产生的损益</td><td>${attr_map['equity_future'].toFixed(3)}</td></tr>
          <tr><td>固定资产产生的现金</td><td>${attr_map['fix'].toFixed(3)}</td></tr>
          <tr><td><strong>投资活动产生的现金流量</strong></td><td><strong>${attr_map['part_2'].toFixed(3)}</strong></td></tr>
          <tr><td><strong>3. 筹资活动产生的现金流量</strong></td><td><strong></strong></td></tr>
          <tr><td>来自家庭的现金</td><td>${attr_map['equity_family'].toFixed(3)}</td></tr>
          <tr><td>来自其他的现金</td><td>${attr_map['equity_others'].toFixed(3)}</td></tr>
          <tr><td>新增债务带来的现金</td><td>${attr_map['expense_use_liability'].toFixed(3)}</td></tr>
          <tr><td>偿还负债付出的现金</td><td>${attr_map['pay_liability'].toFixed(3)}</td></tr>
          <tr><td><strong>筹资活动产生的现金流量</strong></td><td><strong>${attr_map['part_3'].toFixed(3)}</strong></td></tr>
          <tr><td><strong>4. 本期现金增加值</strong></td><td><strong>${attr_map['part_4'].toFixed(3)}</strong></td></tr>
          <tr><td><strong>5. 上期末现金结余</strong></td><td><strong>${attr_map['prev'].toFixed(3)}</strong></td></tr>
          <tr><td>直接间接调整计算</td><td><strong>${adjust.toFixed(3)}</strong></td></tr>
          <tr><td><strong>6. 本期末现金结余</strong></td><td><strong>${attr_map['final'].toFixed(3)}</strong></td></tr>
        </table>
      `
      return content

最终效果如下所示:

可以看到如下两点:

  1. 直接统计各现金账户资金变动和使用间接法(也就是上文介绍的调整方法)计算得到的现金流量差异可以很小,例如本人账单2025年5月的差异仅有0.05元。但和直接测绘现金账户资金变动相比,使用间接法可以更好的看到现金的类别和流动情况。
  2. 其次,本人使用的现金流量表格式和CAS表依然有不小的差异。这是因为CAS是企业使用的格式,直接应用于个人通常仍然不适用。需要进一步调整。

AI+Beancount快捷记账方案

正如前文所述,我从今年开始引入了LLM来参与我的记账,确实会给我的效率带来很大提升。之前需要大概每个月需要两个小时左右的对账工作,引入LLM之后只需要约15分钟即可。相比传统beancount-import、csv-importer等方式,使用LLM可以帮助自动推理每一个交易所属类别,而不再需要手动设置复杂的正则匹配和分配规则,且可以处理各种复杂的交易入账工作。我想这是因为Beancount使用的是文本文件的形式进行记账,这和LLM的工作原理恰好相似,正好能发挥LLM在排版和逻辑推理上的长处。

AI目前参与记账主要可以用于执行如下两类工作:

1. 支付宝、微信和信用卡账单自动入账

我主要使用支付宝、微信和信用卡来进行支付。支付宝和微信钱包都支持导出csv格式的账单。那么AI所做的工作就是将 csv格式的账单转换为beancount格式的账单。这种文本文件(csv和beancount都可以认为是文本文件)之间的格式转换正好的LLM擅长的工作之一。此外,LLM还可以在这个过程中,自动帮我推断应该使用什么样的会计科目来记账。以支付宝为例,我通常使用的LLM prompt如下:

我正在使用beancount记账,下面是我的账户分类:

option "operating_currency" "CNY"
option "operating_currency" "USD"

2017-01-01 open Assets:Bank:Cash CNY               ; XX银行储蓄卡(尾号xxxx)
2017-01-01 open Assets:Bank:Positions CNY          ; XX银行理财或定期
2017-01-01 open Assets:Web:AliPay:Cash CNY         ; 支付宝活期或余额宝
2017-01-01 open Assets:Web:AliPay:Positions "FIFO" ; 支付宝定期或基金
2017-01-01 open Assets:Web:WeChat:Cash CNY         ; 微信钱包零钱或零钱通
2017-01-01 open Assets:Receivables                 ; 应收账款
2017-01-01 open Assets:Receivables:Company         ; 垫付应报销账户
2017-01-01 open Assets:Receivables:LandLord        ; 房东押金是应收账款

2017-01-01 open Assets:Fixed:House CNY             ; 房产
2017-01-01 open Assets:Fixed:Car CNY               ; 汽车
2017-01-01 open Assets:Fixed:Park CNY              ; 车位
2017-01-01 open Assets:Fixed:Laptop CNY            ; 笔记本电脑

2017-01-01 open Assets:Salary:HousingFund          ; 公积金账户
2017-01-01 open Assets:Salary:HealthInsurance      ; 个人医疗保险账户
2017-01-01 open Assets:Salary:SocialInsurance      ; 社保账户
2017-01-01 open Assets:Salary:PersonalSocialInsurance ; 个人社保账户(企业年金)

2017-01-01 open Liabilities:Bank:CN CNY            ; XX银行信用卡 (尾号xxxx)
2017-01-01 open Liabilities:Bank:US USD            ; XX银行信用卡(尾号xxxx)
2017-01-01 open Liabilities:Bank:Company:CN CNY    ; XX银行公务卡(尾号xxxx)
2017-01-01 open Liabilities:Others ; 其他负债

1990-01-01 open Equity:Opening-Balances            ; 初始资金
1990-01-01 open Equity:Others                      ; 其他资金
1990-01-01 open Equity:Salary                      ; 缴纳的社保、医保、公积金和企业年金费用(不计入income)
2017-01-01 open Equity:Amortization:Rent           ; 预付房租,摊还账户


2017-01-01 open Income:Salary:Company              ; 工资
2017-01-01 open Income:Family                      ; 来自家庭成员的红包、转账和收入
2017-01-01 open Income:PnL                         ; 各种投资收益或损失
2017-01-01 open Income:Others                      ; 其他收入

2017-01-01 open Expenses:Clothing                  ; 衣服的支出
2017-01-01 open Expenses:Food                      ; 食物的支出
2017-01-01 open Expenses:Traffic                   ; 交通费用,包括但不限于单车、出租车等
2017-01-01 open Expenses:Gas                       ; 汽油费用
2017-01-01 open Expenses:Shop                      ; 日常购物费用
2017-01-01 open Expenses:Entertainment             ; 娱乐支出
2017-01-01 open Expenses:Rent                      ; 租房费用
2017-01-01 open Expenses:Expense                   ; 水、电、燃气费和电话费等
2017-01-01 open Expenses:Subscribe                 ; 各种软件订阅费用
2017-01-01 open Expenses:Tax                       ; 各种税和手续费
2017-01-01 open Expenses:Depreciation:Fixed:House  ; 房屋折旧费用
2017-01-01 open Expenses:Depreciation:Fixed:Park   ; 停车位折旧费用
2017-01-01 open Expenses:Depreciation:Fixed:Car    ; 汽车折旧费用
2017-01-01 open Expenses:Depreciation:Fixed:Laptop ; 笔记本电脑折旧费用
2017-01-01 open Expenses:Others                    ; 其他支出

请你根据下面支付宝csv账单内容,产生beancount格式的账单。注意:

1. 支付宝的默认支付账户是尾号xxxx的XX银行信用卡账户。
2. 来自支付宝的"不计收入"的退款记录为收入,收款账户也是信用卡账户。从Expenses对应类别中支出金额。
3. 给他人转账记录为支出到Expenses:Gift账户。收到来自"Person1","Person2", "Person3", "Person4"的转账收入记录为Income:Family,收到其他人转账记录到Income:Others中
4. "投资理财"余额宝收益请汇总后只产生一条记录,记录收入来源为Income:PnL。
5. 请注意,根据Beancount的记账规则,每一个账目需要确保金额求和为0。例如,Income(收入)和净资产(Equity)增加通常记为负数。
6. 请依次输出所有条目,确保不漏掉任何条目。如果有无法解析的条目,请使用注释标记。
7. 请直接给出输出转换后的结果,不需要有思考过程,也不需要输出其他内容。/no_think

输出格式如下:

```
2025-01-01 * "张三" "微信给我转账"
    Income:Others                                    -155.00 CNY
    Assets:Web:WeChat

2025-04-13 * "奈雪的茶" "购买奶茶"
    Expenses:Food                                    45.00 CNY
    Liabilities:Bank:BoC:CN

2025-04-14 * "Person1" "转账"
    Expenses:Gift                                  48.00 CNY
    Liabilities:Bank:BoC:CN

2025-04-19 * "超翔科技-退款" "电动车充电退款"
    Expenses:Traffic                                -0.83 CNY
    Liabilities:Bank:BoC:CN

2025-04-19 * "美团平台商户" "退款"
    Expenses:Others                                 -11.50 CNY
    Liabilities:Bank:BoC:CN
```
具体账单列表如下
<支付宝账单文件.csv>

通过上面的指令,我通常可以一次性获得由AI帮助我自动生成的,包含了一整个月的支付宝账单。对微信账单也使用类似操作即可。当然,整个过程还有如下一些注意事项:

  1. 需要给各个账户一个备注,如果是银行卡账户也需要给出银行卡尾号。这样,AI就可以自动根据CSV文件中的相关信息准确的判断应该使用什么账户来记账。
  2. 目前, 我没有直接将LLM直接引用beancount-import完全自动化的记账工作流中。这主要是因为LLM输出的结果依然可能存在错误或需要调整的地方。我一般习惯手动调整确认无误后再导入beancount账本体系中。
  3. Beancount支持子账本记账。我通常习惯采用如下的子账本体系:
include "2023/2023.beancount"
include "2024/2024.beancount"
include "2025/01.beancount"
--include "01-alipay.beancount"
--include "01-wechat.beancount"
include "2025/02.beancount"
--include "02-alipay.beancount"
--include "02-wechat.beancount"
include "2025/03.beancount"
--include "03-alipay.beancount"
--include "03-wechat.beancount"
include "2025/04.beancount"
--include "04-alipay.beancount"
--include "04-wechat.beancount"
include "2025/05.beancount"
--include "05-alipay.beancount"
--include "05-wechat.beancount"
include "2025/06.beancount"
--include "05-alipay.beancount"
--include "05-wechat.beancount"
include "fund.beancount"

其中,“YYYY/MM.beancount"是YYYY年MM月份的主账本,而我自己手动记账的条目也在其中。在这个文件中,会递归的引入"MM-alipay.beancount"和"XX-wechat.beancount"文件,也就是在对应的子账本中引入支付宝和微信账单。而"fund.beancount"正是前文中介绍的使用beanprice自动产生的各类浮动收益资产(基金、股票等)的价格账本。此外,在每个子账本中,我习惯添加如下语句:

2025-06-01 custom "fava-option" "default-file"
2025-06-01 custom "fava-option" "insert-entry" ".*"

这两个语句将指定Fava WebGUI将这个子账户设置为默认展示的文件和默认入账文件。

AI 自动账目查重

确实,由于我日常也会手动记账。其中有少部分条目可能会和支付宝或微信账单条目重复。对此,我通常会将所有账单全部输入给AI,让AI帮我检查疑似重复的账本。AI通常输出的结果相对比较正确,我也能快速识别并检查重复的账目并将其剔除。

灵活地使用AI

我也并没有严格要求我的工作流使用AI,我认为是否使用AI应当取决于是否能够更高效地记账。举一个例子,在上个月,我自己手动记账比较全面。我发现最终各账户资金和实际账户资金相差不超过几百元。那么这个时候再用AI来导入账单就会存在大量去重工作,工作效率反而会降低。对此,我使用了如下两个命令来简单处理了一下实际资产和账目资产的差异:

2025-05-30 pad Assets:Web:WeChat:Cash Expenses:Others
2025-05-31 balance Assets:Web:WeChat:Cash    x CNY

上面两条指令的意思是,确保微信账户Assets:Web:WeChat:Cash在2025年5月底的金额=实际金额x CNY。如果账户金额和实际金额不符,那么差值的部分则记录为类别为Expenses:Others的消费。通过这种方式,我虽然损失了一些记账的精度,但是我确实极大提升了对账效率。这种方式在实际金额和账本金额差别不大的时候,是可以谨慎使用的。

本人保留对侵权者及其全家发动因果律武器的权利

版权提醒

如无特殊申明,本站所有文章均是本人原创。转载请务必附上原文链接:https://www.elliot98.top/post/life/ai_beancount_personal_bookkeeping/

如有其它需要,请邮件联系!版权所有,违者必究!