目录

11 · ops/balance.py 与 ops/pad.py:余额断言与自动补齐

核对基线 · 范围 · 依赖

核对基线:beancount 仓库 commit 97472138(2026-08-22)。路径相对仓库根目录,引用格式 文件:起-止行 (名称),行号已逐条核对。 本篇范围beancount/ops/balance.py(185 行)与 beancount/ops/balance_test.py(395 行);beancount/ops/pad.py(203 行)与 beancount/ops/pad_test.py(677 行)。 上游依赖beancount/core/realization.pyRealAccountget_or_createcompute_balancepostings_by_account)、beancount/core/inventory.pyget_currency_units)、beancount/core/amount.pysub)、beancount/core/account.pyparent_matcher)、beancount/core/data.pyBalancePadposting_sortkey)、beancount/core/interpolate.pyBalanceError)、beancount/core/number.pyONEZERO)。beancount/ops/pad.py:15,104 还直接复用 balance.get_balance_tolerance下游使用者:两者都是插件,唯一装载点是 beancount/loader.py:65-68 (PLUGINS_POST)Balance.diff_amountbeancount/parser/printer.py:317 渲染成 ; Diff: 注释,由 beancount/core/compare.py:25 排除在条目比较之外。

1. 两个模块解决什么问题

beancount/ops/balance.py 实现 balance 指令:check():48-60)把 Balance 指令里用户从对账单抄下的期望 Amount,与截至该日期开始时刻累加出的运行余额比较,超出容差就以 BalanceError 报告差额。

beancount/ops/pad.py 方向相反:不核对,而是凭下一条 balance 反推差额、合成一笔交易把余额填平,用于把历史账搬进账本时将开账日之前的流水折叠成一笔期初余额。beancount/ops/pad.py:34-36 自称 "a very simple kind of pad"。两者共用同一套"带容差比较余额"的逻辑。

2. 结构一览

名称 位置 作用
get_balance_tolerance beancount/ops/balance.py:20-45 Balance 指令算容差:显式 ~ 优先,否则按小数位推断
check beancount/ops/balance.py:48-185 插件入口,逐条处理 Balance,失败时记错误并替换条目
PadError beancount/ops/pad.py:21-26 三字段错误记录,entry 指向 Pad
pad beancount/ops/pad.py:29-203 插件入口,按账户分组扫描,合成补齐交易并插入
Pad / Balance 指令 beancount/core/data.py:155-174 / :177-203 meta/date/account/source_accountmeta/date/account/amount/tolerance/diff_amount
SORT_ORDER / entry_sortkey beancount/core/data.py:705 / :708-719 同日排序:Open −2、Balance −1、其它 0、Document 1、Close 2

3. Balance:断言核对的是什么

3.1 只比一种币种的 units 总和,跨批次、不看成本

subtree_balance = realization.compute_balance(real_account, leaf_only=False)

# Get only the amount in the desired currency.
balance_amount = subtree_balance.get_currency_units(expected_amount.currency)

# Check if the amount is within bounds of the expected amount.
diff_amount = amount.sub(balance_amount, expected_amount)

beancount/ops/balance.py:148-154。四层语义由此确定:

beancount/ops/balance.py:140-145 留有 FIXME:每遇一条 Balance 就重算整棵子树,作者提出可以只算目标币种、并在子树未被新 position 弄脏时跳过重算——"Do this."。这段与 compute_balance 调用一起由 2020-05-02 commit fff2d5f7 引入。

3.2 运行余额只为被断言的子树维护

beancount/ops/balance.py:75-87 收集所有 Balance.account,再用 account.parent_matcher 造谓词,只把"被断言账户或其后代"get_or_create 进一棵临时 RealAccount 树。累加时 realization.get 返回 None 就跳过:# The account will have been created only if we're meant to track it.beancount/ops/balance.py:96-99)。beancount/ops/balance.py:64-70 说明这是刻意与完整 realization 分开、按不同顺序进行的;性能优化来自 2014-07-13 commit 5421694fparent_matcher 编译的正则带词边界 {account}($|:),所以 Assets:Bank:CheckingOld 不算 Assets:Bank:Checking 的子账户(beancount/ops/balance_test.py:269-286)。

累加分支还有一条注释:# Note: Always allow negative lots for the purpose of balancing. This error should show up somewhere else than here.beancount/ops/balance.py:100-101)——负持仓合不合法不是余额断言的职责,check 只管加;分工由 2014-06-14 commit 3ebb4849 把负成本检查移到 validation 时定下。

3.3 同一天:断言先于记账

# - Balance entries should appear before Transactions, because
#   they are defined to apply at the beginning of the day.
SORT_ORDER = {Open: -2, Balance: -1, Document: 1, Close: 2}

beancount/core/data.py:695-705check 严格按 entries 既有顺序单趟扫描,Transaction 累加、Balance 立即比较(beancount/ops/balance.py:92-104),所以"同日 Balance 排在 Transaction 前"这条排序约定就是"断言在当天开始时刻生效"的全部实现。beancount/loader.py:740 在每个插件跑完后无条件 entries.sort(key=data.entry_sortkey)(注释 "Don't trust the plugins themselves")。beancount/ops/balance_test.py:214-227 锁定:05-02 有一笔 +100 USD,同日 balance 0 USD 成立、次日 balance 100 USD 成立;Open 排在 Balance 前,开户当天即可断言 0(:255-267)。

3.4 容差:显式优先,否则按小数位推断并放宽一倍

if balance_entry.tolerance is not None:
    tolerance = balance_entry.tolerance
else:
    expo = balance_entry.amount.number.as_tuple().exponent
    if expo < 0:
        # Be generous and always allow twice the multiplier on Balance and
        # Pad because the user creates these and the rounding of those
        # balances may often be further off than those used within a single
        # transaction.
        tolerance = options_map["tolerance_multiplier"] * 2
        tolerance = ONE.scaleb(expo) * tolerance
    else:
        tolerance = ZERO

beancount/ops/balance.py:29-43。三条路径:写了 ~ 0.002 直接采用,与断言金额本身的小数位无关(beancount/ops/balance_test.py:324-338,六条不同写法的容差都是 0.002);没写且带小数位,取最后一位的量级 ONE.scaleb(expo)tolerance_multiplier * 2,默认 0.5beancount/parser/options.py:537-540)乘 2 得 1.0,容差恰是最后一位的一个完整单位(0.0 USD → 0.10.00 → 0.011.01 → 0.01beancount/ops/balance_test.py:340-370);整数则容差为 ZERO,必须精确相等。

放宽一倍的理由见注释:交易内部的推断走 beancount/core/interpolate.py:188ONE.scaleb(expo) * tolerance_multiplier(0.5 倍最后一位),而 Balance/Pad 的数字是用户手抄的,舍入误差往往更大。比较用严格大于(beancount/ops/balance.py:159),差额恰等于容差时通过:余额 23.02423.023 ~ 0.00123.025 ~ 0.001 成立,对 23.02223.026 失败(beancount/ops/balance_test.py:372-392)。

3.5 失败的处理:记错误、换条目、不动余额

失败时只做三件事:追加一条 BalanceError,消息形如 Balance failed for '<account>': expected <X> != accumulated <Y> (<diff> too much|too little)beancount/ops/balance.py:160-174);用 entry._replace(meta=entry.meta.copy(), diff_amount=diff_amount) 造一个填了差额的新 Balance 放回列表(:181);不做补偿。real_account.balance 在整个 Balance 分支里从未被写过,所以一次失败不会把运行余额强制改成期望值,未消除的差额会被同账户后续断言继续看到(:92-104,146-183beancount/ops/pad_test.py:549-570 的注释直接写明 "A failing balance check does not automatically bring the balance to its value")。beancount/ops/balance.py:176-180 的注释对替换条目这一做法自留异议:maybe leaving the original check intact and insert a new error entry might be more functional or easier to understand.

BalanceError 的构造(:160-174)先于 _replace:181),所以 BalanceError.entry 指向原始、diff_amount 仍为 NoneBalance;被追加进返回值 new_entries 的才是带 diff_amount 的替换版(:183)——同一次失败对应两个不同的 Balance 对象,只有后者流向打印器和后续处理。

另两类错误在同一分支产出但不替换条目:账户查不到 open_close_mapInvalid reference to unknown account:109-118);Open 限定了币种而断言用别的币种报 Invalid currency ...:120-134beancount/ops/balance_test.py:103-113)。前者过去会 continue 掉整条断言,2024-07-05 commit 664e4b32 改成保留条目、交给 validation 再报一次(beancount/ops/balance_test.py:306-319 同时期待两类错误,条目数仍是 3)。这两类错误都不 continue,代码继续往下算 subtree_balancediff_amount:账户未知或币种非法时数值比较仍会执行,同一条 Balance 可能同时产生引用/币种错误与数值失败错误,数值失败时依旧被 _replace 成带 diff_amount 的版本(:104-181)。

4. Pad:怎么补、什么时候拒绝补

4.1 主流程

pad 把所有 Padaccount 分组(beancount/ops/pad.py:52-53),用 realization.postings_by_account 把账本摊成"账户 → TxnPosting 或指令"(:56)。对每个有 pad 的账户,收集它自己和全部后代账户的条目、按 data.posting_sortkey 排序(:66-72),单趟扫描维护一个 InventoryTxnPosting 累加(:80-83);Padentry.account == account_ 时记为 active_pad 并清空 padded_lots:85-90);Balance 则比容差、必要时补齐(:92-189)。那个账户判断必不可少——postings_by_account 把每条 Pad 同时挂到 accountsource_account 两个键下(beancount/core/realization.py:304-307)。分组循环解包出的 pad_list 未被使用(:62),驱动扫描的是 postings 里的 Pad 条目。

比较用的是本分组自己维护的 pad_balance,算式与 check 相同:get_currency_units 取该币种总额、amount.sub 求差、balance.get_balance_tolerance 取容差、abs(diff) > tolerance 判定(beancount/ops/pad.py:100-106)。beancount/ops/pad.py:95-99 的注释重申与 3.1 相同的语义:this does not check a single position, but rather checks that the total amount for a particular currency (which itself is distinct from the cost)

4.2 四条限制

限制 实现 效果
同一 pad 下每种币种只补一次 padded_lotsbeancount/ops/pad.py:75,90,111,189 后续同币种断言必须靠真实交易对上
账户存在按成本计价的持仓时报错(不阻止补齐) beancount/ops/pad.py:112-131 该币种下每个 cost is not None 的 position 报一条 Attempt to pad an entry with cost for balance: ...;报错分支无 continue/break,随后仍生成无成本补齐交易
pad 没生成任何交易就报 Unused Pad beancount/ops/pad.py:195-201 写了却没用上不是静默无操作
连续两条 pad 中间没有 Balance 消费,前一条作废 beancount/ops/pad.py:89 覆盖 active_pad 旧 pad 的条目列表保持空,最后落进 Unused Pad

第一条限制的关键在 padded_lots.add(check_amount.currency) 的位置(beancount/ops/pad.py:188-189):它在 if abs(diff_amount.number) > tolerance: 之外,所以每一条 Balance 都把自己的币种标记为已处理,无论是否触发补齐、是否通过。这正是 2015-11-07 commit 9b632075 修 issue 78 的改动——此前这行在补齐分支内部,通过的断言不消费 pad,后面一条失败的断言会被静默补平、错误报到错误的位置上。

4.3 生成的交易长什么样

narration = ("(Padding inserted for Balance of {} for difference {})").format(
    check_amount, diff_position)
new_entry = data.Transaction(
    active_pad.meta.copy(), active_pad.date, flags.FLAG_PADDING,
    None, narration, data.EMPTY_SET, data.EMPTY_SET, [])

beancount/ops/pad.py:142-154。narration 是固定格式 (Padding inserted for Balance of <断言金额> for difference <差额 position>),被 beancount/ops/pad_test.py 的多个 assertEqualEntries 逐字锁定(:44:86:189)。flag 是 flags.FLAG_PADDING,即 "P"beancount/core/flags.py:10);日期与 meta 取自 Pad(浅拷贝),payee 为 None,tags/links 是 data.EMPTY_SET。两条 posting:active_pad.account 收差额,source_account 收相反数(:156-176);补齐 lot 一律无成本,# Thus our padding lot is without cost by default.:133-139)。两条 posting 的 meta 都设成触发补齐的那条 Balance 的 meta(:163,174),这是 2024-09-15 commit 2321a201 的改动,此前是空 dict 导致 posting 缺 filename/lineno。这些 posting 不带 __automatic__ 标记——pad 本身不设置这个键。该键(beancount/core/interpolate.py:236 (AUTOMATIC_META))在 booking 阶段用于两类 posting:插值补出缺失数字的(beancount/parser/booking_full.py:1011-1016),以及 get_residual_postingsbeancount/core/interpolate.py:245-255)为吸收舍入残差生成的(同时打上 __residual__);两者都与 pad 无关。

生成的交易先存进 new_entries[id(active_pad)]:59,179),最后按原 entries 顺序紧跟对应 Pad 插入(:191-198)。Pad 含可变的 meta 字典(Meta = dict[str, Any]beancount/core/data.py:32),普通 Pad 实例本身不可哈希,不能直接作字典键;同时它是 NamedTuplebeancount/core/data.py:155-174),按字段值比较,字段全同的两个不同 Pad 对象会相等。用 id(active_pad) 作键既绕开不可哈希的问题,也按对象身份而非字段值关联生成条目。

4.4 补齐后带成本持仓变负:直接 raise

pos, _ = pad_balance.add_position(diff_position)
if pos is not None and pos.is_negative_at_cost():
    raise ValueError("Position held at cost goes negative: {}".format(pos))

beancount/ops/pad.py:181-186。这条不进 pad_errors 而是抛异常:补齐 lot 必定无成本,而 is_negative_at_cost()beancount/core/position.py:315-321)要求 cost is not None,所以它是内部不变量的断言,不是用户输入错误——4.2 的四条限制都是用户能写出来的,一律走错误列表。插件抛出的异常被 beancount/loader.py:717-736 收成一条 LoadError,不中断加载。

5. 加载流水线里的先后

beancount/loader.py:65-68PLUGINS_POST 依次是 beancount.ops.padbeancount.ops.balancepad 必须先跑:它合成的交易要成为 balance 眼里的真实交易。两者都排在用户自己的 plugin 之后,这样用户插件生成的交易能被两者看到。位置由 2024-01-06 commit 60f1c40f 改定,此前 padPLUGINS_PRE,看不见用户插件产生的交易;beancount/ops/pad_test.py:605-674 是随该提交加的回归测试:插件把每笔 * 交易复制一份,pad 须把复制出的 20.0 USD 也算进去,最终补 60.0 而非 80.0。

beancount/loader.py:740 在每个插件之后重排 entries,补齐交易与 Pad 同日、SORT_ORDER 同为 0、meta["lineno"] 也相同(来自 active_pad.meta.copy()),排序稳定,相对位置不变。

两者能否配合还取决于两次排序:beancount/loader.py:602 在跑任何插件之前先对 entriesentry_sortkey 排序(SORT_ORDERBalance −1、Pad 默认 0,beancount/core/data.py:695-705),pad() 内部又按同一 SORT_ORDERposting_sortkey:733-747)把账户及其后代的条目重排一次(beancount/ops/pad.py:66-72)。因此同一天写在同一账户下的 Balance 总排在 Pad 之前被扫描到:处理这条 Balance 时,同日的这条 Pad 尚未被扫描、还没有成为 active_pad:85-92)——它不会被这条同日 Pad 补齐,只能靠此前已生效的 active_pad(若有)或真实交易对上;如果这是该账户第一次出现 Pad,这条 Balance 就完全得不到补齐,而后 Pad 若再等不到晚于它的 Balance 去消费,就落进 Unused Pad。

以上顺序只在 plugin_processing_mode == "default" 时成立:唯有此时 PLUGINS_PRE、用户插件、PLUGINS_AUTOPLUGINS_POST 才会依次并入执行序列(beancount/loader.py:662-670);"raw" 模式只运行账本 option "plugin" 显式列出的插件,不会自动跑 pad/balance,本节所述先后顺序也无从谈起。

6. 设计决策与理由

决策 理由 证据
断言只比单一币种的 units 总和;父账户汇总整棵子树 对账单给的就是"某币种若干",成本与批次是账本内部结构;一张对账单常对应多个子账户 beancount/ops/balance.py:136-138,148,151beancount/core/inventory.py:277-290beancount/ops/pad.py:95-99
同日 Balance 排在 Transaction 之前 断言语义定义为"当日开始时" beancount/core/data.py:695-705beancount/ops/balance_test.py:214-227
未写 ~ 时按小数位自动推断的容差,是交易内部同一 exponent 基础推断值的 2 倍;整数断言容差为零;显式 ~ 不受此约束 手抄余额的舍入误差比单笔交易内部大;写 100 USD 表达的就是精确整数;用户显式给出的容差不该再被放大 beancount/ops/balance.py:29-43beancount/core/interpolate.py:145-198
断言失败不把运行余额强制改成期望值,只替换条目 失败不会掩盖差额:diff_amount 只写入当前 Balance 的替换条目,未消除的差额会被同账户后续断言继续看到;diff_amount 随条目流到打印器 beancount/ops/balance.py:92-104,146-183beancount/ops/pad_test.py:549-570beancount/parser/printer.py:317
只为被断言的子树建 realization 完整 realization 对只有几条断言的账本是浪费 beancount/ops/balance.py:64-875421694f
pad 只补 pad 指定的那个账户 自称 "very simple kind of pad",够用且易懂 beancount/ops/pad.py:34-36beancount/ops/pad_test.py:277-329
每种币种每条 pad 只补一次,且每条 Balance 都消费额度(不论成败) 一条 pad 表达"这一刻之前的历史",不该反复兜底后续遗漏;修 issue 78:否则失败的断言会被后一条的补齐静默抹平 beancount/ops/pad.py:111,188-1899b632075
账户存在按成本计价持仓时,为每个这样的 position 报错(但仍照常补齐);没用上的 pad 报错 补出的 lot 无成本,与有成本的持仓混在一起会让"按成本"与"不按成本"的余额语义混淆,用报错提醒用户核对;静默失效会让用户以为补齐生效了 beancount/ops/pad.py:112-139,199-201beancount/ops/pad_test.py:239-275,450-465
pad 排在 balance 之前且都在 PLUGINS_POST 补齐交易要被断言看到;用户插件的交易要被 pad 看到 beancount/loader.py:65-6860f1c40f

7. 行为细节与边界

现象 后果 证据
【文档漂移】beancount/ops/balance.py 的模块 docstring 是 """Automatic padding of gaps between entries.""" beancount/ops/pad.py:1 逐字相同,描述的是 pad 而非余额断言 beancount/ops/balance.py:1beancount/ops/pad.py:1
【文档漂移】beancount/core/data.pyBalance 的 docstring 先讲 diff_amount 后讲 tolerance,字段定义顺序相反 按 docstring 顺序位置传参会把两个字段装反 beancount/core/data.py:190-195:201-203
beancount/ops/balance.py:120-121 先判 expected_amount is not None:151 又无条件取 .currency 防御分支不彻底;语法层 balance/amount_tolerance 规则不含 %empty 分支,amount 不可能为空(beancount/parser/grammar.y:607-633),:151 的判断实际不可达;beancount/parser/grammar.py:729-731 是构造并返回 Balance 的调用点 beancount/ops/balance.py:120-121,151
比较用 > 而非 >=0 USD0.00 USD 语义不同 差额恰等于容差时通过;前者容差 0,后者 0.01 beancount/ops/balance.py:34-43,159beancount/ops/balance_test.py:340-370,372-392
pad 分组按 account_ 的整棵子树聚合条目 子账户上的 Balance 也进入这个循环、与子树总额比较,而补齐 posting 仍只落在 pad 指定账户上 beancount/ops/pad.py:66-72,100beancount/ops/pad_test.py:277-329
带成本持仓时 pad 报错后仍继续补齐 :120-131continue/break,补齐交易照样生成,故不会再报 Unused Pad;多个成本批次各报一条 beancount/ops/pad.py:112-139beancount/ops/pad_test.py:450-465
失败的断言不把 pad_balance 拉平 后一条同额断言仍看到真实余额,pad 因此仍生效 beancount/ops/pad.py:92-189beancount/ops/pad_test.py:549-570
目标账户与来源账户互为对方的 pad 源码未处理,只有一条注释 You could try padding A into B and B into A to see if it works. beancount/ops/pad_test.py:433
diff_amount 不参与条目比较 beancount/core/compare.py:25 把它与 meta 一起排除,cmptest.assertEqualEntries 看不出断言是否失败 beancount/core/compare.py:25

8. 测试锁定了什么

beancount/ops/balance_test.pyTestBalance :13TestBalancePrecision :322):

测试 行号 锁定的行为
test_simple_error / _first / _cont 15-61 失败时 diff_amount-100 USD,成功时为 None;连续断言累加
test_simple_partial_currency_first / _cont 64-101 一条断言只覆盖一种币种
test_simple_invalid_currency 103-113 Open 限定 CAD 时断言 USD 报错
test_parents / _only / _with_postings 114-197 父账户断言等于子树总和;子账户可为负;父账户自身的 posting 计入
test_with_lots / test_balance_mixed_cost_and_no_cost 198-209、288-304 带成本 lot 按 units 计;成本买入后按市价全部卖出使 units 归零,balance 0 HOOL 成立(不是成本与无成本持仓并存时求和的测试)
test_check_samedate / test_balance_before_create 214-227、255-267 同日断言先于交易生效;Open 当日即可断言 0
test_precision / test_balance_with_prefix_account 229-253、269-286 0.00 USD 的容差 0.01 吞掉 0.00001 的累加;CheckingOld 不算 Checking 的子账户
test_balance_account_does_not_exist 306-319 同产 BalanceErrorValidationError,条目不被吞掉
test_get_balance_tolerance__explicit / __regular 324-370 显式 ~ 覆盖一切;九条推断结果,整数为 0
test_balance_with_tolerance 372-392 边界值用 > 判定

beancount/ops/pad_test.pyTestPadding :20,继承 cmptest.TestCase,多数测试用 assertEqualEntries 比对完整结果账本):

测试 行号 锁定的行为
test_pad_simple / test_pad_to_zero 22-95 补正、补负,narration 与两条 posting 逐字;posting 的 meta 是 dict(:54
test_pad_no_overflow / _used_twice_legally / _illegally 97-237 被补那条之后的断言不再被补;两条 pad 各服务其后一条断言;中间无断言时前一条报 Unused
test_pad_unused / test_pad_multiple_times 239-275、437-448 断言本就成立、或两条 pad 一条断言,报 PadError
test_pad_parents 277-329 补齐 posting 只落在 Assets:US,不下发到子账户
test_pad_multiple_currencies 331-383 一条 pad 为 USD、CAD 各生成一笔,EUR 因断言成立不生成
test_pad_check_balances 385-431 补齐交易确实计入运行余额(95 → 105 → 145)
test_pad_at_cost 450-465 Attempt to pad an entry with cost for
test_pad_parent / test_pad_issue362 467-483、572-603 CheckingOld 的持仓不污染 Checking 的 pad;后者函数体断言全被注释掉,仍由 @loader.load_doc()beancount/loader.py:760,790-795)锁定"无错误"
test_pad_tolerance 485-502 差额 0.05 对 ~ 0.05 不触发补齐,pad 转 Unused
test_pad_zero_padding_issue78a / _original 504-547 通过的 0 HKD 断言消费掉 pad;错误报在 11-02 那条而非 09-16
test_pad_zero_padding_issue78b 549-570 失败的断言不把余额拉平,其后的 pad 仍生效;只报一条错
test_pad_plugin_modify 605-674 用户插件新增的交易被 pad 计入,补 60.0 而非 80.0

未被测试覆盖:beancount/ops/balance.py:147assert real_account is not None 只被成功路径反复执行到(entry.account 必在 asserted_accounts 中,因而必被 beancount/ops/balance.py:83-87 预建),没有测试触发它的失败分支,按当前预建账户逻辑该分支也不可达;负的显式容差;beancount/ops/pad.py:181-186raise ValueError(补齐 lot 无成本,构造不出触发条件);同币种多个成本批次产生多条 PadError;A→B 与 B→A 互相 pad。

9. 演变史

2017-04-30 之前的提交,文件路径带 src/python/ 前缀,由 commit 859f341e 统一迁移到 beancount/

日期 提交 / 记录 变化
2014-06-14 3ebb4849ae90524f 负成本检查移出 balance 到 validation;pad 支持子账户
2014-06-15 4713e633 beancount.ops.check 改名 beancount.ops.balance
2014-07-13 5421694f 只为有断言的账户维护运行余额
2015-04-19 7da9e751 父账户匹配加词边界(CHANGES:3944-3945
2015-05-06 / 05-10 fab8211e5f36440625ca256e 按小数位推断容差(初期默认关闭);5f364406 于 05-10 引入实验性显式容差语法 ~ <tolerance>;pad 改用同一套容差检查
2015-05-17 CHANGES:3894-3898 发布记录追认显式容差语法为 EXPERIMENTAL 特性,同批带出 default_toleranceaccount_rounding 等选项
2015-06-06 3a767c99 新增 inferred_tolerance_multiplier 选项(CHANGES:3737-3743
2015-09-18 a78889c8 修复 pad_balance.add_position() 返回值可能为 None 却未判空的 bug,新增 test_pad_to_zero
2015-11-07 9b632075 修 issue 78:padded_lots.add 移出补齐分支
2016-10 ef5f10c299ee920a 显式容差转正;移除 use_legacy_fixed_tolerances
2017-01-21 / 09-17 a4100093b4faf7ae0edac4a6 断言币种须在 Open 允许的币种内(含一次回归修复);检测引用不存在的账户(CHANGES:1259-1260
2019-01-29 CHANGES:469-471 修 #362:父账户谓词未对齐账户名边界,导致 Pad 余额算错
2020-05-02 fff2d5f7 改用 realization.compute_balance 并留下性能 FIXME(CHANGES:309-311
2024-01-06 60f1c40f ops.padPLUGINS_PRE 移到 PLUGINS_POST 的 balance 之前
2021-01-30 / 2024-07-05 4504c2b1664e4b32 未知账户错误文案与 Transaction 统一;不再丢弃该条 Balance
2024-09-15 2321a201 pad 生成的 posting 用 Balance 的 meta 取代空 dict
2025-05-23 ae5d5f14 新增正式名 tolerance_multiplier;旧名 inferred_tolerance_multiplier 保留为已弃用别名,映射到同一选项

10. 与其他模块的关系

11. 参考索引

beancount/ops/balance.py:1 模块 docstring;17 __plugins__;20-45 get_balance_tolerance(29-31 显式、34-43 推断、36-39 注释、40-41 ×2、42-43 整数为零);48-60 check docstring;64-70 realization 说明;71 real_root;75-87 预建账户;90 open_close_map;92-102 Transaction 累加(99-102 注释);104-118 未知账户;120-134 币种校验;136-145 FIXME;146-148 子树余额;151 get_currency_units;154 amount.sub;157 容差;159-174 错误消息;176-181 _replace;183 append;185 return。

beancount/ops/pad.py:1 模块 docstring;18 __plugins__;21-26 PadError;29-48 docstring(34-36 不跨父子、38-40 单币种);52-53 分组;56 postings_by_account;59 new_entries;62 账户循环;64-65 active_pad;66-72 子树条目;74-75 padded_lots;77-83 运行余额;85-90 Pad 分支;92-99 Balance 分支与注释;100-106 比较;111 补齐条件;112-131 成本检查;133-139 差额 position;141-154 交易头;156-176 两条 posting;179 暂存;181-186 不变量 raise;188-189 标记已补;191-201 插入与 Unused Pad;203 return。

测试beancount/ops/balance_test.py:13,15-25,27-42,44-61,64-78,80-101,103-113,114-145,146-169,170-197,198-209,214-227,229-253,255-267,269-286,288-304,306-319,322,324-338,340-370,372-392beancount/ops/pad_test.py:20,22-54,57-95,97-143,145-198,200-237,239-275,277-329,331-383,385-431,433,437-448,450-465,467-483,485-502,504-522,525-547,549-570,572-603,605-674

其它beancount/core/data.py:155-174 (Pad):177-203 (Balance):701-707:705 (SORT_ORDER):708-719 (entry_sortkey):736-747 (posting_sortkey)beancount/core/realization.py:146-168 (get):171-195 (get_or_create):278-315 (postings_by_account):304-307 (Pad 双挂):473-484 (compute_balance)beancount/core/inventory.py:277-290beancount/core/position.py:315-321beancount/core/interpolate.py:38-43,155,188,236,245-259beancount/core/flags.py:10beancount/core/compare.py:25beancount/loader.py:65-68,717-736,740,760,790-795beancount/ops/validation.py:47,121-157beancount/parser/booking.py:20-23beancount/parser/booking_full.py:789-795,1011-1016beancount/parser/options.py:537-540,544-558beancount/parser/grammar.py:729-731beancount/parser/printer.py:317,324-328CHANGES:309-311,469-471,1259-1260,3737-3743,3894-3898,3944-3945

commit3ebb4849(2014-06-14)、ae90524f(2014-06-14)、4713e633(2014-06-15)、5421694f(2014-07-13)、7da9e751(2015-04-19)、fab8211e(2015-05-06)、5f364406(2015-05-10)、25ca256e(2015-05-10)、3a767c99(2015-06-06)、a78889c8(2015-09-18)、9b632075(2015-11-07)、ef5f10c2(2016-10-10)、99ee920a(2016-10-30)、a4100093(2017-01-21)、b4faf7ae(2017-01-21)、859f341e(2017-04-30)、0edac4a6(2017-09-17)、fff2d5f7(2020-05-02)、4504c2b1(2021-01-30)、60f1c40f(2024-01-06)、664e4b32(2024-07-05)、2321a201(2024-09-15)、ae5d5f14(2025-05-23)。