跳到主要内容

QuickBooks 到 Beancount 迁移指南

· 阅读需 48 分钟
Mike Thrift
Mike Thrift
Marketing Manager

阶段一:从 QuickBooks 导出数据

迁移五年的数据,第一步是把所有 QuickBooks 记录以可用的格式导出来。QuickBooks 桌面版和 QuickBooks 在线版有不同的导出选项:

2021-12-01-from-quickbooks-to-plain-text-a-migration-playbook

1.1 QuickBooks 桌面版 – 导出选项

IIF (Intuit Interchange Format): QuickBooks 桌面版可以将列表(如会计科目表、客户、供应商)导出为 .IIF 文本文件。在 QuickBooks 桌面版中,进入 文件 (File) → 实用程序 (Utilities) → 导出 (Export) → 列表到 IIF 文件 (Lists to IIF),然后选择你需要的列表(例如,会计科目表、客户、供应商)。这将生成一个包含账户名称、类型和列表数据的文本文件。IIF 是一种专有但易于解析的纯文本格式。用它来获取你的会计科目表和联系人列表,以便在 Beancount 中参考。

总分类账/日记账(通过 CSV): 对于交易数据,QuickBooks 桌面版没有一键式完整导出功能,但你可以使用报表。推荐的方法是导出所需日期范围内的总日记账(所有交易)。在 QuickBooks 桌面版中,打开 报表 (Reports) → 会计与税务 (Accountant & Taxes) → 日记账 (Journal),将日期设置为从最早的交易到今天,然后点击 导出 (Export) → Excel。在移除报表页眉/页脚和空列后,将结果另存为 CSV。确保数值数据是干净的:包含小数(例如 3.00 而不是 3),没有多余的引号,CSV 中没有货币符号或双重负号。CSV 文件应包含 日期 (Date)、交易号 (Trans #)、名称 (Name)、账户 (Account)、备注 (Memo)、借方 (Debit)、贷方 (Credit)、余额 (Balance) 等列(或根据报表格式只有单个金额列)。

提示: QuickBooks 桌面版 2015+ 也可以通过 查找 (Find) 对话框导出交易。使用 编辑 (Edit) → 查找 (Find) → 高级 (Advanced),设置五年的日期范围,然后将结果导出为 CSV。警告: 某些版本将导出限制在 32,768 行。如果你的数据量非常大,请逐年(或分更小的块)导出以避免数据被截断,然后再将它们合并。确保日期范围不重叠以防止重复。

其他格式 (QBO/QFX/QIF): QuickBooks 桌面版可以通过 .QBO (Web Connect) 或 .QFX/.OFX 文件导入银行交易,但对于 QuickBooks 导出,这些不是常规选项。如果你的目标只是提取银行交易,你可能已经从银行那里获得了 QBO/OFX 文件。然而,对于完整的分类账导出,请坚持使用 IIF 和 CSV。QuickBooks 桌面版不能直接导出到 QIF (Quicken Interchange Format) 格式,除非使用第三方工具。如果你确实找到了获取 QIF 的方法,请注意一些账本工具(旧版的 Ledger 2.x)可以读取 QIF,但在我们的流程中,最好还是使用 CSV。

1.2 QuickBooks 在线版 – 导出选项

内置 Excel/CSV 导出: QuickBooks 在线版 (QBO) 提供了一个导出数据工具。进入 设置 ⚙ → 工具 (Tools) → 导出数据 (Export Data)。在导出对话框中,使用报表 (Reports) 标签选择数据(例如总分类账或交易列表),并使用列表 (Lists) 标签选择列表(会计科目表等),选择所有日期 (All dates),然后导出到 Excel。QuickBooks 在线版将下载一个 ZIP 文件,其中包含所选报表和列表的多个 Excel 文件(例如,利润表、资产负债表、总分类账、客户、供应商、会计科目表等)。然后你可以将这些 Excel 文件转换为 CSV 进行处理。

交易明细报表: 如果 QBO 的默认导出不包含单个总分类账文件,你可以手动运行一个详细报表:

  1. 导航到报表 (Reports) 并找到按账户交易明细 (Transaction Detail by Account)(在某些 QBO 版本中是总分类账 (General Ledger))。
  2. 报表期间 (Report period) 设置为完整的五年范围。
  3. 在报表选项下,将分组依据 (Group by) 设置为无 (None)(以便列出单个交易而不是小计)。
  4. 自定义列,至少包括:日期 (Date)、交易类型 (Transaction Type)、编号 (Number)、名称 (Name, Payee/Customer)、备注/描述 (Memo/Description)、账户 (Account)、借方 (Debit)贷方 (Credit)(或单个金额列),以及余额 (Balance)。如果使用了类别 (class) 或地点 (location),也请包含它们。
  5. 运行报表,然后导出到 Excel (Export to Excel)

这将生成一个包含所有交易的详细分类账。将其另存为 CSV。每一行代表一笔交易的一个分录 (split/posting)。之后你需要按交易对这些行进行分组以进行转换。

会计科目表及其他列表: QuickBooks 在线版可以通过 会计 (Accounting) → 会计科目表 (Chart of Accounts) → 批量操作 (Batch Actions) → 导出到 Excel (Export to Excel) 来导出会计科目表。这样做可以获取账户名称和类型。同样,如果你想保留名称作为元数据,也请导出客户、供应商等列表。

QuickBooks Online API (可选): 对于编程方法,Intuit 为 QBO 数据提供了 REST API。高级用户可以创建一个 QuickBooks Online 应用(需要开发者账户),并使用 API 以 JSON 格式获取数据。例如,你可以查询 Account 端点获取会计科目表,查询 JournalEntryGeneralLedger 报表端点获取交易。有像 python-quickbooks 这样的 Python SDK 可以封装 API。然而,使用 API 涉及 OAuth 身份验证,对于一次性迁移来说有些小题大做,除非你偏爱自动化。对于大多数情况,** 手动导出为 CSV/Excel 更简单且不易出错**。


阶段二:转换和清理数据

一旦你有了 CSV (和/或 IIF) 格式的 QuickBooks 数据,下一步就是将其转换为 Beancount 的纯文本账本格式。这包括解析导出的文件,将 QuickBooks 账户映射到 Beancount 的会计科目表,以及按 Beancount 语法格式化交易。

2.1 使用 Python 解析 QuickBooks 导出文件

使用 Python 将确保转换的准确性和可复现性。我们将概述两个关键任务的脚本:** 导入会计科目表转换交易**。

账户导入和映射: 在添加交易之前,在 Beancount 中设置好账户至关重要。QuickBooks 的账户有类型(银行、应收账款、费用等),我们将它们映射到 Beancount 的层级结构(资产、负债、收入、费用等)。例如,我们可以使用如下映射:

# QuickBooks 账户类型到 Beancount 根类别的映射
AccountTypeMap = {
'BANK': 'Assets',
'CCARD': 'Liabilities',
'AR': 'Assets', # 应收账款作为资产
'AP': 'Liabilities', # 应付账款作为负债
'FIXASSET': 'Assets',
'OASSET': 'Assets', # 其他资产
'OCASSET': 'Assets', # 其他流动资产
'LTLIAB': 'Liabilities', # 长期负债
'OCLIAB': 'Liabilities', # 其他流动负债
'EQUITY': 'Equity',
'INC': 'Income',
'EXP': 'Expenses',
'EXINC': 'Income', # 其他收入
'EXEXP': 'Expenses', # 其他费用
}

使用 QuickBooks 桌面版的 IIF 导出文件或 QBO 的账户列表 CSV,我们获取每个账户的名称和类型。然后:

  • 创建 Beancount 账户名称: QuickBooks 有时在账户名称中使用冒号 (:) 来表示子账户(例如 “Current Assets:Checking)。Beancount 使用相同的冒号表示法来表示层级。你通常可以直接重用该名称。如果 QuickBooks 账户名称不以类别开头,则在前面加上映射的类别。例如,一个类型为 BANK、名为 "Checking" 的 QuickBooks 账户在 Beancount 中将变为 Assets:Checking。一个 EXP (费用) 账户 "Meals" 变为 Expenses:Meals,依此类推。

  • 确保命名有效: 移除或替换任何可能混淆 Beancount 的字符。QuickBooks 允许名称中包含 &/ 等字符。明智的做法是剔除或替换特殊字符(例如,用 and 替换 &,移除斜杠或空格)。此外,确保转换后所有账户名称都是唯一的——QuickBooks 可能允许在不同父账户下有相同的子账户名,这没问题,但在 Beancount 中,完整的名称(包括父账户)必须是唯一的。如果需要,重命名或附加一个限定符以区分它们。

  • 输出账户开设指令: 在 Beancount 中,每个使用的账户都必须用 open 指令开设。你可以选择一个在第一笔交易之前的日期(例如,如果迁移 2019–2023 年的数据,对所有开设指令使用 2018-12-31 或更早的日期)。脚本将写出如下行: 2018-12-31 open Assets:Checking USD 2018-12-31 open Expenses:Meals USD 为每个账户都这样做(假设 USD 是主要货币)。为每个账户使用适当的货币(见下面的多币种说明)。

交易转换: 主要挑战是将 QuickBooks 导出的交易(CSV)转换为 Beancount 条目。每个 QuickBooks 交易(发票、账单、支票、日记账分录等)可能有多条分录(行),必须将它们收集到一个 Beancount 交易中。

我们将使用 Python 的 CSV 阅读器来迭代导出的行并累积成分录:

import csv
from collections import defaultdict

# 从 QuickBooks 日记账 CSV 中读取所有行
rows = []
with open('quickbooks_exported_journal.csv', 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for line in reader:
rows.append(line)

# 按交易分组(假设 'Trans #' 标识交易)
transactions = defaultdict(list)
for line in rows:
trans_id = line.get('Trans #') or line.get('Transaction ID') or line.get('Num')
transactions[trans_id].append(line)

现在 transactions 是一个字典,其中每个键是交易 ID/编号,值是该交易的分录列表。接下来,我们将每个组转换为 Beancount 格式:

def format_date(qb_date):
# QuickBooks 的日期可能是 "12/31/2019" 这样的格式
m, d, y = qb_date.split('/')
return f"{y}-{int(m):02d}-{int(d):02d}"

output_lines = []
for trans_id, splits in transactions.items():
# 如果需要,按行顺序对分录排序(通常它们是按顺序导出的)
splits = sorted(splits, key=lambda x: x.get('Line') or 0)
first = splits[0]
date = format_date(first['Date'])
payee = first.get('Name', "").strip()
memo = first.get('Memo', "").strip()
# 交易标题
output_lines.append(f"{date} * \"{payee}\" \"{memo}\"")
if first.get('Num'): # 如果有参考编号,则包含它
output_lines.append(f" number: \"{first['Num']}\"")
# 遍历每个分录/记账
for split in splits:
acct_name = split['Account'].strip()
# 将 QuickBooks 账户名映射到 Beancount 账户(使用之前的映射)
beancount_acct = account_map.get(acct_name, acct_name)
# 确定带符号的金额:
amount = split.get('Amount') or ""
debit = split.get('Debit') or ""
credit = split.get('Credit') or ""
if amount:
# 某些导出文件有一个 Amount 列(贷方为负数)
amt_str = amount
else:
# 如果有单独的 Debit/Credit 列
amt_str = debit if debit else f"-{credit}"
# 为安全起见,移除数字中的逗号
amt_str = amt_str.replace(",", "")
# 附加货币
currency = split.get('Currency') or "USD"
amt_str = f"{amt_str} {currency}"
# 分录的备注/描述
line_memo = split.get('Memo', "").strip()
comment = f" ; {line_memo}" if line_memo else ""
output_lines.append(f" {beancount_acct:<40} {amt_str}{comment}")
# 交易结束 – 空行
output_lines.append("")

这个脚本逻辑执行以下操作:

  • 将日期格式化为 Beancount 的 YYYY-MM-DD 格式。

  • 使用收款人 (Name) 和备注 (Memo) 作为交易的叙述。例如: 2020-05-01 * "ACME Corp" "Invoice payment" (如果没有收款人,你可以使用 QuickBooks 的交易类型或留空收款人引号)。

  • 如果存在参考编号(支票号、发票号等),则添加一个 number 元数据。

  • 迭代每一条分录行:

    • 使用字典 account_map(从会计科目表步骤中填充)将 QuickBooks 账户名映射到 Beancount 账户。
    • 确定金额。根据你的导出文件,可能有一个金额列(带有正/负值)或独立的借方和贷方列。上面的代码处理了这两种情况。它确保贷方表示为负金额,因为在 Beancount 中,每个记账都使用带符号的单个数字。
    • 附加货币(除非存在不同的货币列,否则假设为 USD)。
    • 用账户、金额和带有行备注的注释写入 Beancount 记账行。例如: Assets:Checking 500.00 USD ; Deposit Income:Sales -500.00 USD ; Deposit 这反映了一笔 500 美元的存款(从收入到支票账户)。
  • 列出所有分录后,用一个空行分隔交易。

多币种处理: 如果你的 QuickBooks 数据涉及多种货币,请在每个记账上包含货币代码(如上所示)。确保外币账户以该货币开设。例如,如果你有一个欧元银行账户,你会输出 open Assets:Bank:Checking EUR,并且该账户中的交易将使用 EUR。Beancount 支持多币种账本并会跟踪隐式转换,但如果你想在报表中转换为基础货币,可能需要添加汇率的价格条目。还建议在 Beancount 文件的顶部声明你的主要经营货币(例如,option "operating_currency" "USD")。

运行转换: 保存 Python 脚本(例如,qb_to_beancount.py)并在你导出的文件上运行它。它应该会生成一个包含所有账户和交易的 .beancount 文件。

2.2 处理边缘情况和数据清理

在转换过程中,请注意以下常见的陷阱以及如何解决它们:

  • 账户名称不匹配: QuickBooks 的账户名称可能与 Beancount 的层级名称冲突。例如,QuickBooks 可能有两个不同的父账户,每个都有一个名为 "Insurance" 的子账户。在 Beancount 中,Expenses:Insurance 必须是唯一的。在导出前通过重命名其中一个(例如,“Insurance-Vehicle” vs “Insurance-Health”)来解决此问题,或者在脚本中将它们映射到唯一的 Beancount 账户。一致的命名约定(无特殊字符,并使用层级结构)将省去很多麻烦。如果需要,使用重映射文件的方法:维护一个旧名称 → 新 Beancount 名称的 CSV 或字典,并在转换期间应用它(我们的示例代码使用了一个 account_map,并且可以从文件中加载覆盖项)。

  • 日期和格式: 确保所有日期格式一致。上面的脚本将 M/D/Y 规范化为 ISO 格式。另外,如果你的五年跨度跨越了年终,请注意财政年度与日历年度的问题。Beancount 不关心财政年度的界限,但你以后可能为了方便而按年拆分文件。

  • 数值精度: QuickBooks 处理货币到分,所以以分为单位工作通常没问题。理想情况下,CSV 中的所有金额都应有两位小数。如果任何金额变成了整数(没有小数)或带有逗号/括号(表示负数),请在脚本中清理它们(去除逗号,将 (100.00) 转换为 -100.00 等)。如果按照指示正确导出 CSV,应该已经避免了这些格式问题。

  • 负数和符号: QuickBooks 报表有时将负数显示为 -100.00(100.00),甚至在某些 Excel 导出中显示为 --100.00。清理步骤应该处理这些情况。确保每笔交易的借贷方总和为零。Beancount 会强制执行这一点(如果不平衡,导入时会抛出错误)。

  • 交易重复: 如果你必须分批导出交易(例如,逐年或逐账户),请小心合并它们,不要重叠。检查一年的第一笔交易是否也是前一批的最后一笔,等等。在边界处很容易意外复制一些交易。如果你怀疑有重复,可以按日期对最终的 Beancount 条目进行排序并查找相同的条目,或者使用 Beancount 的唯一交易标签来捕获它们。一种策略是将 QuickBooks 交易号作为元数据包含进来(例如,使用 Trans # 或发票号作为 txn 标签或 quickbooks_id 元数据),然后确保这些 ID 没有重复。

  • 不平衡的分录 / 暂记账户: QuickBooks 可能有奇怪的情况,比如一笔交易有不平衡,QuickBooks 会自动将其调整到“期初余额权益”或“留存收益”账户。例如,在设置初始账户余额时,QuickBooks 通常会将差额记入一个权益账户。这些会出现在导出的交易中。Beancount 将要求显式平衡。你可能需要引入一个用于期初余额的权益账户(通常是 Equity:Opening-Balances)来镜像 QuickBooks。在账本的第一天有一个建立所有账户期初余额的条目是很好的做法(见阶段五)。

  • 多币种边缘情况: 如果使用多币种,QuickBooks 的导出可能会以本国货币或其原生货币列出所有金额。理想情况下,获取每个账户的原生货币数据(QuickBooks 在线版的报表通常会这样做)。在 Beancount 中,每个记账都带有一个货币。如果 QuickBooks 提供了汇率或本国货币换算,你可以忽略这些,并依赖 Beancount 的价格条目。如果 QuickBooks 没有导出汇率,你可能需要手动添加关键日期的价格记录(例如,使用 Beancount 的 price 指令)以匹配估值。然而,对于基本的账本完整性,只要交易以其原始货币平衡就足够了——除非你想要相同的报告,否则不必明确记录未实现损益。

  • 应收账款 / 应付账款: QuickBooks 跟踪发票和账单的详细信息(到期日、支付状态等),这些在纯账本中不会完全转移。你会得到应收(A/R)和应付(A/P)的交易(发票增加 A/R,付款减少 A/R 等),但不会有发票文件或每个发票的客户余额。因此,迁移后,你应该验证 Beancount 中的 A/R 和 A/P 账户余额是否等于 QuickBooks 中客户/供应商的未结余额。如果你需要跟踪发票,可以使用 Beancount 的元数据(例如,包含一个 invoice 标签或链接)。QuickBooks 的发票号应该已经通过 NumMemo 字段导出了——我们的脚本将 Num 保留为交易元数据中的 number: "..."

  • 不活动或已关闭的账户: IIF 导出文件可能包含不活动的账户(如果你选择包含它们)。导入它们没问题(如果它们真的不活动,它们将没有交易并且余额为零)。你可以在最后一笔交易日期之后,用 close 指令在 Beancount 中将它们标记为已关闭。这可以使你的账本保持整洁。例如: 2023-12-31 close Expenses:OldAccount ; migrated after migration 这是可选的,主要是为了整洁。

通过仔细清理和映射上述数据,你将拥有一个在结构上与你的 QuickBooks 数据相匹配的 Beancount 账本文件。下一步是验证它在数值上也与 QuickBooks 相匹配。


阶段三:数据验证和对账

验证是会计数据迁移中至关重要的一步。我们需要确保 Beancount 账本与 QuickBooks 账簿精确到每一分钱。可以使用多种策略和工具:

3.1 试算平衡表对账

试算平衡表报告列出了所有账户的期末余额(带有借方和贷方或正/负标记),并且净额应为零。在两个系统中运行同一日期的试算平衡表是确认整体准确性的最快方法。

  • 在 QuickBooks 中: 运行最后一年的最后一天(例如,2023年12月31日)的试算平衡表 (Trial Balance) 报告。这份报告显示了每个账户的余额。导出它或记下关键数字。

  • 在 Beancount 中: 使用 Beancount 的报告功能生成试算平衡表。一个简单的方法是通过命令行:

    bean-report migrated.beancount balances

    balances 报告就是一个试算平衡表,列出了所有账户及其余额。你也可以在 Fava(Beancount 的网页界面)中打开文件,并查看 BalancesBalance Sheet 部分。Beancount 中的每个账户余额都应与 QuickBooks 的试算平衡表相匹配。例如,如果 QuickBooks 显示应收账款 = $5,000,那么 Beancount 的 Assets:Accounts Receivable 账户总额应为 $5,000(借方)。如果销售收入 = $200,000,那么 Beancount 中的 Income:Sales 应显示 $200,000(贷方,如果使用将贷方显示为负数的试算平衡表,可能会显示为 -200,000)。

如果存在差异,找出它们:

  • 检查是否整个账户丢失或多余(我们是否忘记了一个账户,或者包含了一个在迁移期之前已经关闭的账户?)。
  • 如果余额不对,深入检查:QuickBooks 可以为该账户运行账户快速报告 (Account QuickReport) 或分类账明细,你可以将其与 Beancount 中该账户的登记簿 (bean-report migrated.beancount register -a AccountName) 进行比较。差异有时来自丢失的交易或重复的交易。

同时验证 Beancount 试算平衡表中所有账户的总和为零(它会打印一个总计,应该为零或非常接近零)。Beancount 强制执行复式记账,所以如果你有任何不为零的不平衡,意味着资产减去负债加权益不为零,表明存在问题(QuickBooks 通常也不允许这种情况,但如果某些数据丢失了可能会发生)。

3.2 账户余额比较

除了试算平衡表,你还可以比较特定的财务报表:

  • 资产负债表: 运行 QuickBooks 最终日期的资产负债表和 Beancount 的资产负债表 (bean-report migrated.beancount balsheet)。这与试算平衡表类似,但按资产、负债、权益组织。数字应该按类别对齐。为了更精细的检查,比较主要账户的总额:现金、应收账款、固定资产、应付账款、权益等。

  • 利润表 (损益表): 在 QuickBooks 和 Beancount 中运行五年期间(或逐年)的利润表 (bean-report migrated.beancount income 用于整个期间的利润表)。Beancount 的净收入应与 QuickBooks 每个期间的净收入相等。如果你迁移了所有五年,累计净收入应该匹配。你还可以比较单个收入和费用总额,以确保没有类别被遗漏或重复计算。

  • 随机抽样交易: 随机挑选几笔交易(特别是从每年和每个主要账户中挑选)并验证它们是否正确迁移。例如,在 QuickBooks 中找到 3 年前的一张发票,然后在 Beancount 文件中搜索其金额或备注(因为所有交易都是文本,你可以在文本编辑器中打开 .beancount 文件或使用搜索工具)。检查日期、金额和账户是否匹配。这有助于捕捉任何日期格式问题或账户映射错误。

3.3 自动化完整性检查

利用 Beancount 自身的验证工具:

  • bean-check: 运行 bean-check migrated.beancount。这将解析文件并报告任何语法或平衡错误。如果脚本遗漏了像未开设的账户或不平衡的交易之类的问题,bean-check 会标记出来。干净的通过(没有输出)意味着文件至少在内部是一致的。

  • 余额断言: 你可以在账本中为关键账户添加明确的余额断言作为额外检查。例如,如果你知道某个日期银行账户的余额,添加一行: 2023-12-31 balance Assets:Bank:Checking 10000.00 USD 然后 bean-check 将确保在该日期,账本中的余额确实是 $10,000。这是可选的,但对于非常重要的账户很有用。你可以从 QuickBooks 获取期末余额(例如,每年年底),并在 Beancount 文件中进行断言。如果任何断言失败,Beancount 将报告差异。

  • 试算平衡表滚动检查: 如果你愿意,可以进行逐期检查。对于每一年,比较净变化。例如,QuickBooks 2020 年的净收入与 Beancount 2020 年的净收入,等等,以确保每年都正确地结转到权益中(QuickBooks 在每个新年自动将净收入滚入留存收益;在 Beancount 中你只会看到累计的权益)。如果你看到差异,这可能表明特定年份的数据存在问题。

  • 交易计数和重复: 计算 QuickBooks 与 Beancount 中的交易数量。QuickBooks 不容易直接显示计数,但你可以通过计算 CSV 中的行数(每个交易标题与分录)来估计。在 Beancount 中,一个快速的方法是计算文件中 txn* " 的出现次数。它们应该等于或略高于 QuickBooks(如果你添加了期初余额交易或调整)。显著的不匹配意味着某些内容可能被遗漏或重复了。我们使用元数据中的唯一 ID 可以提供帮助:如果你怀疑有重复,可以在 Beancount 文件中搜索相同的支票号或发票号是否出现了两次。

  • 对账状态: 我们在脚本中根据 QuickBooks 的已清算状态包含了一个 rec: "y""n" 的元数据(在示例中为 rec)。这不是一个标准的 Beancount 功能(Beancount 不像 Ledger 那样跟踪已清算/待处理状态),但它可以作为有用的元数据。你可能需要验证在 QuickBooks 中所有已对账的交易都存在。最终,在 Beancount 中重新对账银行账户(使用你的银行对账单)可能是证明没有任何遗漏的最终证据。

通过执行这些验证,你可以建立对迁移保留了数据的信心。在这个阶段要花足够的时间——现在修复异常比几个月后依赖这些账簿时要容易得多。如果验证失败,常见问题包括:账户的期初余额丢失、交易日期超出范围,或条目的符号反转。一旦确定,这些都是可以修复的。


阶段四:提交到 Beancount 账本

在清理和验证之后,是时候将数据正式化到你的 Beancount 账本结构中了。“提交”在这里既指最终确定账本文件,也可能指将它们检入版本控制系统以备审计。

4.1 组织账本文件和配置

决定如何组织 Beancount 账本文件。对于五年的数据,你可以将所有内容保存在一个文件中,或者按年份或类别拆分。一个常见、清晰的结构是:

  • 主账本文件: 例如,ledger.beancount – 这是可以 include 其他文件的入口点。它可能包含全局选项,然后包含年度文件。
  • 账户文件: 定义会计科目表和期初余额。例如,accounts.beancount 包含所有 open 指令(由脚本生成)。你也可以在这里列出商品(货币)。
  • 交易文件: 每年一个,例如 2019.beancount, 2020.beancount 等,包含该年的交易。这使每个文件的大小易于管理,并允许你在需要时专注于某一年。或者,你可以按实体或账户拆分,但按时间拆分对于财务数据来说很直接。

主文件示例:

option "title" "我的商业账本"
option "operating_currency" "USD"

include "accounts.beancount"
include "2019.beancount"
include "2020.beancount"
...
include "2023.beancount"

这样,当你运行报告时,所有数据都会被汇总,但你保持了秩序。

Beancount 不要求多个文件——你可以只有一个大文件——但上述结构提高了清晰度和版本控制。根据 Beancount 的最佳实践,使用清晰的节标题并按逻辑对相关条目进行分组是很好的做法。

4.2 设置期初余额和权益

如果你的迁移不是从绝对零开始,你需要处理期初余额。两种情况:

  • 从零开始的账簿: 如果五年期始于业务成立之初(例如,你从 2019 年 1 月开始使用 QuickBooks,所有账户除了初始权益外都为零),那么你可能不需要一个单独的期初余额交易。2019 年的第一批交易(如向银行账户注入初始资金)将自然地建立期初余额。只需确保任何初始资本之前的留存收益都通过权益交易入账。

  • 中途开始的账簿(部分历史): 如果你更早开始使用 QuickBooks,而 2019 年是一个中点,那么截至 2019 年 1 月 1 日,每个账户都有一个结转余额。QuickBooks 会将这些作为期初余额或留存收益。在 Beancount 中,通常的做法是在你的开始日期前一天创建一个期初余额条目:

    • 使用一个名为 Equity:Opening-Balances(或类似名称)的权益账户来抵消所有期初金额的总和。
    • 例如:如果在 2018-12-31,现金为 $10,000,应收账款为 $5,000,应付账款为 $3,000(贷方),你会写一笔交易: 2018-12-31 * "Opening Balances" Assets:Cash 10000.00 USD Assets:Accounts Receivable 5000.00 USD Liabilities:Accounts Payable -3000.00 USD Equity:Opening-Balances -12000.00 USD 这使得 Opening-Balances 账户的余额为负数总和(–$12k),从而平衡了该条目。现在,所有资产/负债账户在 2019 年初都具有正确的余额。这应该与 QuickBooks 的任何“留存收益”或结转余额相对应。
    • 或者,使用 Beancount 的 padbalance 指令:对于每个账户,你可以从 Opening-Balances pad 它,并断言其余额。这是一种更自动化的方式。例如: 2018-12-31 pad Assets:Cash Equity:Opening-Balances 2018-12-31 balance Assets:Cash 10000.00 USD 这告诉 Beancount 插入任何必要的条目(到 Opening-Balances),以便现金在该日期等于 10000 USD。对每个账户都这样做。结果类似,但像第一种方法那样写一个明确的交易也很直接。
  • 留存收益: QuickBooks 不会明确导出一笔“留存收益”交易——它只是计算它。迁移后,你可能会注意到 Equity:Retained Earnings 是零,如果你没有创建它的话。在 Beancount 中,留存收益只是前几年的利润。你可以选择创建一个留存收益账户,并在每个新年的第一天将以前的利润转入其中,或者干脆让权益成为所有收入/费用的总和(这在报告的权益部分下显示)。为了透明度,一些用户每年都会做结账分录。这是可选的,主要用于展示。由于我们迁移了所有交易,如果你按年运行报告,每年的利润自然会累计。

  • 比较检查: 设置期初余额后,在开始日期运行资产负债表,以确保一切正确(它应该显示那些期初余额与期初权益相抵为零)。

4.3 最终确定和版本控制

现在数据已是 Beancount 格式并已结构化,明智的做法是将文件提交到版本控制仓库(例如,git)。对账本的每次更改都可以被跟踪,并且你有一个所有修改的审计追踪。这是纯文本会计的一个主要优势。例如,在 QuickBooks 中,更改可能不容易进行差异比较,但在 Beancount 中,你可以看到逐行的差异。正如一些用户指出的,使用 Beancount 你可以获得透明度以及在需要时恢复更改的能力——每个条目都可以追溯到变更历史。

考虑将这次初始迁移的提交标记为 v1.0 或类似标记,这样你就知道它代表了从 QuickBooks 导入时的账簿状态。今后,你将直接在 Beancount 中输入新交易(或从银行源导入等),并且你可以使用常规的软件开发实践(每月或每天提交,使用分支进行实验等)。

设置 Fava 或其他工具: Fava 是 Beancount 的一个 Web 界面,可以方便地查看报告。提交后,运行 fava ledger.beancount 来浏览财务报表,并最后一次与你的 QuickBooks 报告进行比较。你可能会在 UI 中更容易发现微小的差异(例如,一个本应为零但显示微小余额的账户表明有遗漏的结账分录或一个孤立的交易)。

命名约定和一致性: 你现在拥有完全的控制权,所以要确保一致性:

  • 所有账户都应该有清晰的名称,以大写的类别名称开头(Assets, Liabilities 等)。如果任何名称看起来奇怪(例如,由于 QuickBooks 的大小写不匹配导致的 Assets:assets:SomeAccount),在账户文件中重命名它们并更新交易(对文件进行快速查找/替换可以做到这一点,或者使用 Beancount 的 bean-format 或编辑器的多光标功能)。
  • 商品符号(货币代码)应该一致。对于美元,到处都使用 USD(而不是 $US$)。对于其他货币,使用标准代码(EUR, GBP 等)。这种一致性对于 Beancount 的价格查找和报告很重要。
  • 移除任何可能已创建的临时或虚拟账户(例如,如果你在脚本中使用 Expenses:Miscellaneous 作为未知账户的通用账户,尝试通过正确映射所有账户来消除它们)。

关闭 QuickBooks: 此时,你应该在 Beancount 中有与 QuickBooks 相匹配的并行账簿。有些人选择在短时间内并行运行两个系统,以确保没有遗漏。但如果验证是可靠的,你可以“关闭” QuickBooks 账簿:

  • 如果这是一个公司环境,考虑导出所有 QuickBooks 源文件(发票、账单、收据)作为记录,因为除非你手动附加,否则它们不会存在于 Beancount 中。
  • 保留 QuickBooks 数据的备份(公司文件和导出文件)。
  • 从今往后,将 Beancount 账本作为主要的记录系统。

通过将数据提交到 Beancount 账本,你已经完成了迁移流程。最后一步是进行审计并展示财务报表的一致性,以让你自己(以及任何利益相关者或审计师)满意迁移是成功的。


阶段五:迁移后审计和示例

为了说明迁移的成功,准备一份财务报表的前后对比,并可能提供交易的差异对比。这为账簿的一致性提供了证据。

5.1 验证财务报表

从 QuickBooks 和 Beancount 中生成相同日期的关键财务报告并进行比较:

  • 截至 2023 年 12 月 31 日的资产负债表: 逐行比较资产、负债和权益总额。它们应该匹配。例如,如果 QuickBooks 显示总资产 = $150,000,** 总负债 + 权益 = $150,000**,那么 Beancount 的资产负债表也应显示相同的总额。如果你对账户结构做了微调(比如合并了一些子账户),在比较时要考虑到这一点,或者分解到下一级以确保总和相等。

  • 2019–2023 年利润表: 确保每年(或整个期间)的总收入、总费用和净利润是相同的。如果 QuickBooks 在报告上进行了一些四舍五入,可能会出现微小差异,但交易通常精确到分,所以净利润应该是精确的。如果某年的利润不同,深入研究该年的数据——这通常是该时期有遗漏或重复条目的迹象。

  • 试算平衡表差异: 如果可能,创建一个电子表格,列出每个账户以及来自 QuickBooks 和 Beancount 的余额。由于我们期望它们匹配,这可能是一个全为零的差异列。这本质上是我们讨论过的试算平衡表交叉检查,但把它写出来有助于记录。

5.2 示例比较(迁移前 vs 迁移后)

下面是一个示例片段,展示了数据的一致性。假设我们 2023 年 12 月 31 日的 QuickBooks 试算平衡表是:

账户QuickBooks 余额 (2023年12月31日)
资产 (Assets)
  Assets:Bank:Checking$12,500.00 (借方)
  Assets:Accounts Receivable$3,200.00 (借方)
负债 (Liabilities)
  Liabilities:Credit Card$-1,200.00 (贷方)
  Liabilities:Loans Payable$-5,000.00 (贷方)
权益 (Equity)
  Equity:Opening-Balances$-7,500.00 (贷方)
  Equity:Retained Earnings$-2,000.00 (贷方)
  Equity:Current Year Profit$0.00

在 Beancount 中,导入并记入所有截至 2023 年的交易后,bean-report balances(试算平衡表)输出:

账户Beancount 余额 (2023年12月31日)
资产 (Assets)
  Assets:Bank:Checking12,500.00 USD (借方)
  Assets:Accounts Receivable3,200.00 USD (借方)
负债 (Liabilities)
  Liabilities:Credit Card-1,200.00 USD (贷方)
  Liabilities:Loans Payable-5,000.00 USD (贷方)
权益 (Equity)
  Equity:Opening-Balances-7,500.00 USD (贷方)
  Equity:Retained Earnings-2,000.00 USD (贷方)
  Equity:Profit (2019-2023)0.00 USD

(注意:权益部分的结构可能不同;关键是总额一致。在这里,Beancount 中的“利润 (2019-2023)”扮演了当年利润/留存收益合并的角色,显示为零是因为利润已结转至留存收益。)

如上所示,每个账户都精确到分。借方总额等于贷方总额。

此外,如果我们运行 2023 年的利润表:

  • QuickBooks: 收入 $50,000,费用 $48,000,净利润 $2,000。
  • Beancount: 收入 $50,000,费用 $48,000,净利润 $2,000(然后结转到留存收益或出现在年终资产负债表的权益部分)。

如果需要,你可以创建交易的差异对比,但由于 QuickBooks 数据不是账本格式,依赖报表更有效。可以将 QuickBooks CSV 和 Beancount 交易按日期排序,并比较关键字段作为最后检查(这可以在 Excel 中或用脚本完成)。然而,鉴于我们信任之前的验证,财务报表检查通常就足够了。

5.3 审计技巧

  • 如果审计师或利益相关者需要保证,请并排展示迁移前后的财务报表Beancount 的透明度实际上可以简化审计,因为你可以快速地从报表上的每个数字追溯到源条目(特别是使用 Fava 的下钻功能)。
  • 保留 QuickBooks 备份和导出的 CSV 文件作为审计追踪的一部分。记录迁移过程中所做的任何调整(例如,“为保持一致性将账户 X 重命名为 Y”或“为清晰起见将交易 Z 分为两个条目”,如果你做了这样的更改)。
  • 从今往后,在 Beancount 中实施定期检查。例如,每月对账银行账户并对其期末余额进行断言,有助于捕捉任何数据问题或录入错误。迁移提供了一个良好的基线;在新系统中保持纪律将确保持续的准确性。

最后,庆祝迁移完成:你已经成功地将五年的会计数据从 QuickBooks 转移到了 Beancount。数据现在以轻量级、版本控制的文本格式存在,具有完整的复式记账完整性。你导出了数据,用 Python 脚本转换了它,通过试算平衡表和报告验证了其完整性,并将其提交到一个组织良好的 Beancount 账本中。这个全面的过程确保了 Beancount 账本是你在五年期间 QuickBooks 账簿的准确、忠实的复制品,为你未来的精简会计工作奠定了基础。