应付账款(AP)是指你的企业因已收到但尚未付款的供应商商品或服务而欠对方的款项。在会计领域,应付账款在资产负债表上被归类为流动负债——通常需要在一年内偿还,且往往在30到60天之内。
这一概念是权责发生制会计的核心,即在账单到达时记录费用和相应的负债,而不是在实际支付现金时才记录。本指南将向你展示如何使用纯文本记账工具Beancount,清晰高效地管理整个应付账款工作流程。
快速概览
在深入细节之前,我们先来了解一些要点:
- 应付账款(AP) 代表你欠供应商的短期债务。你可以在资产负债表的
Liabilities(负债)部分找到它。 - 权责发生制 vs. 现金制: 应付账款是一个仅在你按 权责发生制 记账时才存在的概念。Beancount 完全支持权责发生制的工作流程,其网页界面 Fava 也会正确显示你的负债。
- AP vs. AR: 这很简单:应付账款(AP)是你 欠 别人的,而应收账款(AR)是别人 欠你 的。
应付账款在Beancount(及Fava)中的位置
要开始跟踪应付账款,你首先需要在你的账本中为其声明一个账户。一个标准的惯例是:
Liabilities:AccountsPayable
你可以选择为主要供应商创建子账户(例如,Liabilities:AccountsPayable:ForestPaintSupply)。
在Fava中,该账户将出现在你的资产负债表的Liabilities下。你可以点击它进行下钻,查看构成余额的过账记录。你甚至可以在Fava的公开示例账本中看到实际效果,该账本包含一个Liabilities:AccountsPayable账户。
符号约定: Beancount将负债余额存储为贷方(负数)。Liabilities:AccountsPayable上的-500.00 USD余额意味着你欠 $500。下面的未清项目报告使用-SUM(number)翻转符号,使每张未付发票显示正数的未清金额。完全结算的发票合计为零,并通过HAVING排除。
你会用到的 Beancount 构建模块
一个稳健的 AP 工作流依赖于几个核心功能:
- 账户: 你主要会用到
Liabilities:AccountsPayable账户、现金账户(如Assets:Bank:Checking)以及你的各种费用账户(例如Expenses:Supplies)。 - 元数据: 你可以将键值数据附加到任何交易上。对于 AP,你会使用像
invoice:、due:、terms:和document:这样的元数据。设置option "documents"指向一个文件夹,这样 Beancount 就能发现其中的文件;在 Fava 中,将文件拖放到日志行上会插入一个document:元数据条目,而启用plugin "fava.plugins.link_documents"则会将那些条目链接到已发现的 Document 指令上。 - 标签与关联: 使用
#tags(如#ap)方便筛选,并用^links(如^INV-10455)以编程方式将账单及其后续付款关联起来。这会为日志创建一条清晰、可审计的轨迹——但仅靠关联不足以下面的未结账单查询。 - 查询 (BQL): Beancount 的 SQL 类查询语言 (BQL) 允许你直接在命令行使用
bean-query或通过 Fava 的“查询”页面运行强大的报表,例如按到期日排序显示所有未支付项。
发票身份(未结账单查询所需): 每张账单以及每笔结算付款——全额、部分或早期折扣——都必须携带相同的 invoice: 和 due: 交易元数据,以及相同的 ^link。查询按 payee、any_meta('invoice') 和 any_meta('due') 分组。如果付款省略了那些键,其 AP 过账将落入单独的 (payee, null, null) 组,已结算的账单看起来仍处于未结状态。确保跨供应商保持 (payee, invoice) 唯一。诸如 terms: 和 document: 这类可选的仅限账单的字段无需在付款上重复。
Beancount 中的核心应付账款工作流程
在账本中管理应付账款涉及两个或三个关键步骤:记录账单、支付账单,有时还需要处理部分付款或折扣。
1) 记录供应商账单(这会产生负债)
首先,当发票到达时,你需要登记费用并创建应付账款。
; Optionally set your documents folder in your main Beancount file:
option "documents" "documents"
2025-08-05 * "Forest Paint Supply" "Paint order INV-10455" ^INV-10455 #ap
invoice: "INV-10455"
due: "2025-09-04"
terms: "2/10, n/30"
document: "invoices/2025-08-05-forest-paint-INV-10455.pdf"
Expenses:Supplies:Paint 500.00 USD
Liabilities:AccountsPayable -500.00 USD这一笔分录实现了两个关键目标:
- 它立即在正确的期间(八月)确认了 500 美元的费用——仅此一次。不要在付款时再次记录该费用。
- 它增加应付账款 500 美元(
-500.00 USD),记录了你欠 Forest Paint Supply 这笔金额。
^INV-10455 链接加上 invoice: / due: 元数据构成了发票的身份信息。之后对此账单的每一次付款都必须重复使用这三项。
2) 支付账单(这用于清除负债)
当你支付发票时,你需要创建一个从银行账户转移资金以清除负债的交易。将账单上的 invoice: 和 due: 复制到付款上,以便两个分录共享同一个查询组。
a) 标准付款(无折扣):
2025-09-01 * "Forest Paint Supply" "Payment INV-10455" ^INV-10455
invoice: "INV-10455"
due: "2025-09-04"
Liabilities:AccountsPayable 500.00 USD
Assets:Bank:Checking -500.00 USD此分录将你的应付账款余额减少 500 美元,同时你的支票账户余额也减少相同金额。该发票的负债现已清除(该组的应付账款之和为零)。费用仍保留在八月的账单上——付款不会涉及 Expenses: 账户。
b) 提前付款折扣(例如“2/10,n/30”)——请只选择一种方法:
如果付款条件为“2/10,n/30”,你在 10 天内付款可获得 2% 的折扣。对于 500 美元的发票,也就是 10 美元的折扣。下面的分隔区域展示了两种互斥的备选方案:方案 1 是生效的;方案 2 被完全注释掉了,因此粘贴时不可能同时记录两笔。若要改用方案 2,请注释掉方案 1,并取消注释方案 2。切勿让两者同时生效——这会重复清除应付账款,并凭空产生第二笔 10 美元的收益。使用哪个收入或费用账户是你的账目会计政策选择;Beancount 并不强制要求其中任何一种。
; Pick EXACTLY ONE of the two transactions below for a given invoice.
; Do not paste both into the same ledger — that clears AP twice.
; Option 1: Record the discount as other income
2025-08-12 * "Forest Paint Supply" "Early payment discount INV-10455" ^INV-10455
invoice: "INV-10455"
due: "2025-09-04"
Liabilities:AccountsPayable 500.00 USD
Assets:Bank:Checking -490.00 USD
Income:Discounts:Payables -10.00 USD
; Option 2: Reduce the original expense directly
; 2025-08-12 * "Forest Paint Supply" "Early payment discount INV-10455" ^INV-10455
; invoice: "INV-10455"
; due: "2025-09-04"
; Liabilities:AccountsPayable 500.00 USD
; Assets:Bank:Checking -490.00 USD
; Expenses:Supplies:Paint -10.00 USD无论采用哪种备选方案,你都会清除完整的 500 美元负债,将银行余额减少你实际支付的 490 美元,并对这 10 美元的收益进行记账。请用所选方案替换标准的全额付款交易——不要在 500 美元的现金付款之上叠加折扣付款,也不要对同一张发票将本分隔区域与第 2a 节组合使用。
3) 处理部分付款
在每一笔分期付款中复用相同的 invoice: / due: / ^link 身份信息,以便每次付款都减少同一个未结发票组。
; Invoice for $1,200
2025-08-10 * "Acme Parts" "INV-9001" ^INV-9001
invoice: "INV-9001"
due: "2025-09-09"
Expenses:Parts 1200.00 USD
Liabilities:AccountsPayable -1200.00 USD
; First payment of $400
2025-08-20 * "Acme Parts" "Payment INV-9001 (1/3)" ^INV-9001
invoice: "INV-9001"
due: "2025-09-09"
Liabilities:AccountsPayable 400.00 USD
Assets:Bank:Checking -400.00 USD
; Final payment of $800
2025-09-05 * "Acme Parts" "Payment INV-9001 (final)" ^INV-9001
invoice: "INV-9001"
due: "2025-09-09"
Liabilities:AccountsPayable 800.00 USD
Assets:Bank:Checking -800.00 USD首次付款后,发票组的应付账款之和为 -800.00 USD(你仍欠 800 美元)。最终付款后,该金额变为零。按 ^INV-9001 过滤日志仍会显示完整历史记录。
有用的查询(BQL)
你可以在 Fava 的“查询”选项卡中或使用 bean-query(BQL 参考)在命令行中运行这些查询。以下内容已使用 Beancount 3.2.3 和 beanquery 0.2.0 验证。
提示:
any_meta()函数首先在过账条目上查找元数据键,然后回退到交易 — 因此交易级别的invoice:/due:值会出现在结果集中。
按供应商的未结应付账款(余额视图):
此查询按供应商汇总负债库存。贷方(负数)总计表示你仍欠该供应商的金额。
SELECT payee, COST(SUM(position)) AS amount
WHERE account ~ "^Liabilities:AccountsPayable"
GROUP BY payee
ORDER BY payee;截至固定日期的未开账单(未结项目报告):
未结项目报告回答“还有哪些未支付?”它必须(1)在明确的截止日期截断,以便后续付款不会静默清除早期快照,(2)按共享的发票标识(payee + invoice: + due:)分组,(3)排除已结算(零)组,并且(4)显示正数的未结余额 — 而不是原始的带符号的应付账款贷方。
下文各处使用的报告日期: 2025-09-04。
可下载的应付账款入门账本
下载 accounts-payable-starter.bean — 一个自包含的账本,由下面的完整代码块生成(账户开设、虚构的初始现金、已结算 / 部分结算 / 未结算发票,截至 2025-09-04)。每个名称和金额均为虚构的示例数据。不需要发票 PDF、文档文件夹或其他文件 来验证或查询它。
首次在干净的临时目录中运行(固定 Beancount 3.2.3 / beanquery 0.2.0):
# Save the download as accounts-payable-starter.bean in this directory, then:
uvx --from beancount==3.2.3 bean-check accounts-payable-starter.bean
uvx --from beanquery==0.2.0 --with beancount==3.2.3 bean-query accounts-payable-starter.bean \
"SELECT payee, any_meta('invoice') AS invoice, any_meta('due') AS due, -SUM(number) AS outstanding WHERE account ~ '^Liabilities:AccountsPayable' AND date <= 2025-09-04 GROUP BY payee, invoice, due HAVING SUM(number) < 0 ORDER BY due, payee"bean-check 退出码为 0 且无输出。该查询返回预期表格中的五个未结行(Forest 不在其中;Acme 未结余额为 800.00;正数未结余额总和 = 1500.00)。编辑代码块后,使用 yarn generate:ap-starter 重新生成已提交的下载文件;如果产物过期,yarn generate:ap-starter --check 将失败。
粘贴此完整夹具(账户开设 + 初始现金 + 三种发票形式,外加到期 / 逾期 / 边界情况行用于账龄分析)。INV-10455 的提前折扣替代方案保持完全注释 — 仅当你替换标准的 Forest 付款时才激活恰好一个;切勿同时启用两个折扣选项与该付款并存。
; accounts-payable-starter — complete runnable AP workflow
option "operating_currency" "USD"
2025-01-01 open Assets:Bank:Checking USD
2025-01-01 open Equity:Opening-Balances USD
2025-01-01 open Liabilities:AccountsPayable USD
2025-01-01 open Expenses:Supplies:Paint USD
2025-01-01 open Expenses:Parts USD
2025-01-01 open Expenses:Office USD
2025-01-01 open Income:Discounts:Payables USD
; Fictional opening cash — enough to cover every payment in this ledger
2025-01-01 * "Opening balance"
Assets:Bank:Checking 5000.00 USD
Equity:Opening-Balances -5000.00 USD
; Settled before as-of — must disappear from the open report
2025-08-05 * "Forest Paint Supply" "Paint order INV-10455" ^INV-10455 #ap
invoice: "INV-10455"
due: "2025-09-04"
Expenses:Supplies:Paint 500.00 USD
Liabilities:AccountsPayable -500.00 USD
2025-09-01 * "Forest Paint Supply" "Payment INV-10455" ^INV-10455
invoice: "INV-10455"
due: "2025-09-04"
Liabilities:AccountsPayable 500.00 USD
Assets:Bank:Checking -500.00 USD
; Early-payment discount alternatives for INV-10455 — mutually exclusive with
; the standard payment above AND with each other. To take 2/10 instead: delete
; (or comment out) the 2025-09-01 payment, then uncomment EXACTLY ONE option.
; Never leave both options active — that clears AP twice.
;
; Option 1: Record the discount as other income
; 2025-08-12 * "Forest Paint Supply" "Early payment discount INV-10455" ^INV-10455
; invoice: "INV-10455"
; due: "2025-09-04"
; Liabilities:AccountsPayable 500.00 USD
; Assets:Bank:Checking -490.00 USD
; Income:Discounts:Payables -10.00 USD
;
; Option 2: Reduce the original expense directly
; 2025-08-12 * "Forest Paint Supply" "Early payment discount INV-10455" ^INV-10455
; invoice: "INV-10455"
; due: "2025-09-04"
; Liabilities:AccountsPayable 500.00 USD
; Assets:Bank:Checking -490.00 USD
; Expenses:Supplies:Paint -10.00 USD
; Partial before as-of; final $800 payment is AFTER 2025-09-04
2025-08-10 * "Acme Parts" "INV-9001" ^INV-9001
invoice: "INV-9001"
due: "2025-09-09"
Expenses:Parts 1200.00 USD
Liabilities:AccountsPayable -1200.00 USD
2025-08-20 * "Acme Parts" "Payment INV-9001 (1/3)" ^INV-9001
invoice: "INV-9001"
due: "2025-09-09"
Liabilities:AccountsPayable 400.00 USD
Assets:Bank:Checking -400.00 USD
2025-09-05 * "Acme Parts" "Payment INV-9001 (final)" ^INV-9001
invoice: "INV-9001"
due: "2025-09-09"
Liabilities:AccountsPayable 800.00 USD
Assets:Bank:Checking -800.00 USD
; Unpaid — not yet due as of 2025-09-04
2025-08-15 * "Bright Office Co" "INV-220" ^INV-220
invoice: "INV-220"
due: "2025-09-14"
Expenses:Office 300.00 USD
Liabilities:AccountsPayable -300.00 USD
; Unpaid — due today (as-of == due)
2025-08-20 * "Helix Industries" "INV-440" ^INV-440
invoice: "INV-440"
due: "2025-09-04"
Expenses:Parts 200.00 USD
Liabilities:AccountsPayable -200.00 USD
; Unpaid — 30 days overdue as of 2025-09-04
2025-07-06 * "Metro Hardware" "INV-77" ^INV-77
invoice: "INV-77"
due: "2025-08-05"
Expenses:Parts 150.00 USD
Liabilities:AccountsPayable -150.00 USD
; Overpayment — credit balance (not an amount due; see edge cases)
2025-08-01 * "Credit Vendor" "INV-C1" ^INV-C1
invoice: "INV-C1"
due: "2025-08-20"
Expenses:Parts 100.00 USD
Liabilities:AccountsPayable -100.00 USD
2025-08-18 * "Credit Vendor" "Overpay INV-C1" ^INV-C1
invoice: "INV-C1"
due: "2025-08-20"
Liabilities:AccountsPayable 150.00 USD
Assets:Bank:Checking -150.00 USD
; Missing due metadata — still open; age into a separate bucket, do not drop
2025-08-12 * "No Due Co" "INV-ND" ^INV-ND
invoice: "INV-ND"
Expenses:Office 50.00 USD
Liabilities:AccountsPayable -50.00 USD未结项目查询(beanquery 0.2.0 支持 HAVING SUM(number) < 0;它不支持将 inventory 与 0 比较时使用 COST(SUM(position)) != 0):
SELECT payee,
any_meta('invoice') AS invoice,
any_meta('due') AS due,
-SUM(number) AS outstanding
WHERE account ~ "^Liabilities:AccountsPayable"
AND date <= 2025-09-04
GROUP BY payee, invoice, due
HAVING SUM(number) < 0
ORDER BY due, payee;截至 2025-09-04 的预期行(独立于交易派生:账单 − 日期 ≤ as-of 的付款;Forest 净值为 0 且不在其中):
| payee | invoice | due | outstanding |
|---|---|---|---|
| No Due Co | INV-ND | (empty) | 50.00 |
| Metro Hardware | INV-77 | 2025-08-05 | 150.00 |
| Helix Industries | INV-440 | 2025-09-04 | 200.00 |
| Acme Parts | INV-9001 | 2025-09-09 | 800.00 |
| Bright Office Co | INV-220 | 2025-09-14 | 300.00 |
正数未结余额总和 = 1,500.00。Forest 的 $500 账单已消失。Acme 仍显示 $800($1,200 − $400);2025-09-05 的 $800 付款在 as-of 日期之后,因此它不能关闭此快照。
对账到同一截止日期的 AP 负债:
SELECT COST(SUM(position)) AS ap_balance
WHERE account ~ "^Liabilities:AccountsPayable"
AND date <= 2025-09-04;预期: ap_balance = (-1450.00 USD)。独立计算:未结正数 $1,500 减去 $50 供应商贷方(见下文)= $1,450 欠款 → 负债库存 -1450.00 USD。
将 as-of 日期移至一天后(date <= 2025-09-05)并重新运行未结项目查询:Acme 的行消失(最终付款已包含),ap_balance 变为 (-650.00 USD)。报告日期之后的付款绝不会重写早期的报告。
供应商贷方 / 多付款: HAVING SUM(number) < 0 仅保留你仍欠的金额。多付款会使组内的 AP 总和变为正数。请单独列出这些 — 不要将正的 AP 余额视为“应付金额”:
SELECT payee,
any_meta('invoice') AS invoice,
any_meta('due') AS due,
SUM(number) AS credit
WHERE account ~ "^Liabilities:AccountsPayable"
AND date <= 2025-09-04
GROUP BY payee, invoice, due
HAVING SUM(number) > 0
ORDER BY payee;预期: 一行 — Credit Vendor / INV-C1 / 2025-08-20 / credit = 50.00(你多支付了 $50)。
缺少 due:: INV-ND 仍出现在未结报告中,且 due 为空。将其计入专用的 missing-due 分组(见下文)以进行账龄分析;切勿静默省略它。
列出带有附加 PDF 的账单:
SELECT date, payee, any_meta('invoice') AS invoice, any_meta('document') AS file
WHERE account ~ "^Liabilities:AccountsPayable"
ORDER BY date DESC;在 Fava 中查看应付账款
- 资产负债表: 导航至
资产负债表→负债→应付账款查看总余额,并可下钻至交易详情。 - 日记账: 使用
account:Liabilities:AccountsPayable或特定链接(如^INV-xxxx)筛选日记账,可查看账单的完整生命周期。 - 文档: 设置
option "documents"后,Beancount 会扫描该目录树下的文件。Fava 的文档视图及link_documents插件(见上文)会将这些文件与带有document:元数据的交易关联起来。
应收账款账龄、周转率与现金流意识
账龄明细表(可复现的导出与分桶): beanquery 0.2.0 可以输出上述未结项目行,但它不提供基于 due 元数据的内置账龄分桶表达式。导出未结报告,然后针对相同的截止日期进行确定性分桶。
bean-query -f csv -o open-ap.csv accounts-payable-starter.bean \
"SELECT payee, any_meta('invoice') AS invoice, any_meta('due') AS due, -SUM(number) AS outstanding WHERE account ~ '^Liabilities:AccountsPayable' AND date <= 2025-09-04 GROUP BY payee, invoice, due HAVING SUM(number) < 0 ORDER BY due, payee"CSV 列:payee(收款人)、invoice(发票)、due(到期日)、outstanding(未结金额)。
截止日期 D = 2025-09-04 及每行的 due(ISO YYYY-MM-DD)的分桶规则:
| 条件 | 分桶 |
|---|---|
due 为空 | missing-due |
due > D | not-yet-due |
due == D | due-today |
1 ≤ (D − due) ≤ 30 | 1-30-overdue |
31 ≤ (D − due) ≤ 60 | 31-60-overdue |
(D − due) ≥ 61 | 61+-overdue |
实现该表格的紧凑 Python 代码(仅使用标准库):
from datetime import date
AS_OF = date(2025, 9, 4)
# Apply bucket() to each CSV row's due column (ISO YYYY-MM-DD or empty).
def bucket(due: str | None) -> str:
if not due:
return "missing-due"
days = (AS_OF - date.fromisoformat(due)).days
if days < 0:
return "not-yet-due"
if days == 0:
return "due-today"
if days <= 30:
return "1-30-overdue"
if days <= 60:
return "31-60-overdue"
return "61+-overdue"截至 2025-09-04 未结行的预期账龄:
| invoice | due | 逾期天数 | 分桶 | outstanding |
|---|---|---|---|---|
| INV-ND | (空) | — | missing-due | 50.00 |
| INV-77 | 2025-08-05 | 30 | 1-30-overdue | 150.00 |
| INV-440 | 2025-09-04 | 0 | due-today | 200.00 |
| INV-9001 | 2025-09-09 | −5 | not-yet-due | 800.00 |
| INV-220 | 2025-09-14 | −10 | not-yet-due | 300.00 |
到期今日不归入“当前”类:due == as-of 是单独的桶。未到期需要 due > as-of。
- 应收账款周转率:
供应商总采购量 ÷ 平均应收账款。相关指标,应付账款周转天数(DPO) 约为365 ÷ 周转率。 - 如果无法按时支付: 应收账款旨在处理短期债务。如果供应商同意正式的长期还款,将其从应收账款中重新分类为应付票据——在转换时保留相同的发票标识,以便应收账款组清零。
2025-10-01 * "Helix Industries" "Convert overdue AP to 12-month note" ^INV-1110
invoice: "INV-1110"
due: "2025-09-15"
Liabilities:AccountsPayable 2000.00 USD
Liabilities:NotesPayable -2000.00 USD纯文本账本中应付账款的最佳实践
- 无纸化办公: 将发票PDF存放在你的文档目录下,并使用
document:元数据键进行关联(若使用Fava,还可通过Fava上传/link_documents)。 - 付款时复用发票标识: 在账单及每一笔结算付款上标注相同的
invoice:、due:和^link。 - 按截止日期生成报表: 使用
date <= YYYY-MM-DD限制未清项目和账龄查询,这样后续付款不会重写早前的快照;账龄计算采用同一D。 - 保持元数据整洁: 一致使用
invoice:、due:和terms:有助于搜索、查询及财务审查。 - 全程权责发生制: 若需要实用的应付账款报表,请坚持按权责发生制记账。Beancount和Fava无需特殊插件即可在资产负债表中处理权责发生制余额。
复制粘贴入门:供应商账单 + 付款
如需完整的未清项目工作流(已结清 + 部分结清 + 未结清、截至日期、账龄分析),请优先使用上面可下载的 应付账款入门文件。下面最简的账单+付款组合仍能显示两边的身份——请先开设账户(或将这些开设操作包含在你的主账本中)。此处的可选 document: 元数据被省略,因此无需账本以外的任何内容。
option "operating_currency" "USD"
2025-01-01 open Assets:Bank:Checking USD
2025-01-01 open Liabilities:AccountsPayable USD
2025-01-01 open Expenses:Supplies:Paint USD
; ---- Bill ----
2025-08-05 * "Forest Paint Supply" "Paint order INV-10455" ^INV-10455 #ap
invoice: "INV-10455"
due: "2025-09-04"
Expenses:Supplies:Paint 500.00 USD
Liabilities:AccountsPayable -500.00 USD
; ---- Payment (no discount) ----
2025-09-01 * "Forest Paint Supply" "Payment INV-10455" ^INV-10455
invoice: "INV-10455"
due: "2025-09-04"
Liabilities:AccountsPayable 500.00 USD
Assets:Bank:Checking -500.00 USD使用 Beancount 3.2.3 和 beanquery 0.2.0,加载此入门文件不会产生错误,应付账款账户净额为零,发票+到期查询返回单个组 (Forest Paint Supply, INV-10455, 2025-09-04),其金额为空(已结清)——而不是一张已开票的行加上一个孤立的 (…, null, null) 付款行。
本指南仅供教育用途,不构成税务、法律或财务建议。
参考资料与延伸阅读:
- Beancount 语言语法——账户、元数据、标签、链接和文档(已于 2026-09-11 验证)
- Beancount 查询语言(BQL)参考(已于 2026-09-11 验证)
- Fava 功能——文档上传和
link_documents(已于 2026-09-11 验证) any_meta()用于查询交易/过账元数据(Martin Blais,Beancount 邮件列表;已于 2026-09-11 验证)





