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"。两者共用同一套"带容差比较余额"的逻辑。
| 名称 | 位置 | 作用 |
|---|---|---|
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_account;meta/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 |
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。四层语义由此确定:
get_currency_units(beancount/core/inventory.py:277-290)只把
units.currency 等于目标币种的 position 的
units.number 相加。账户同时有 USD 和 CAD
时,balance ... 100 USD 对 CAD
不作声明(beancount/ops/balance_test.py:64-101)。units,cost
被完全忽略:1 HOOL {501 USD} 满足
balance 1 HOOL(beancount/ops/balance_test.py:198-209);get_currency_units
按 units.currency 求和时不区分 position 是否带
cost(beancount/core/inventory.py:277-290),成本买入后按市价减到零同样满足
balance 0 HOOL(:288-304)。断言成本币种的价值本模块做不到。compute_balance(..., leaf_only=False)(beancount/core/realization.py:473-484)合并该节点及全部后代的
balance,注释写明 "for the total sum of their
subaccounts"(beancount/ops/balance.py:136-138);父账户自身的
posting
也计入(beancount/ops/balance_test.py:170-197)。beancount/ops/balance.py:140-145 留有 FIXME:每遇一条
Balance
就重算整棵子树,作者提出可以只算目标币种、并在子树未被新 position
弄脏时跳过重算——"Do this."。这段与 compute_balance
调用一起由 2020-05-02 commit fff2d5f7 引入。
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 5421694f。parent_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 时定下。
# - 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-705。check
严格按 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)。
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.5(beancount/parser/options.py:537-540)乘 2
得
1.0,容差恰是最后一位的一个完整单位(0.0 USD → 0.1、0.00 → 0.01、1.01 → 0.01,beancount/ops/balance_test.py:340-370);整数则容差为
ZERO,必须精确相等。
放宽一倍的理由见注释:交易内部的推断走
beancount/core/interpolate.py:188 的
ONE.scaleb(expo) * tolerance_multiplier(0.5
倍最后一位),而 Balance/Pad
的数字是用户手抄的,舍入误差往往更大。比较用严格大于(beancount/ops/balance.py:159),差额恰等于容差时通过:余额
23.024 对 23.023 ~ 0.001 与
23.025 ~ 0.001 成立,对
23.022、23.026
失败(beancount/ops/balance_test.py:372-392)。
失败时只做三件事:追加一条 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-183;beancount/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 仍为
None 的 Balance;被追加进返回值
new_entries 的才是带 diff_amount
的替换版(:183)——同一次失败对应两个不同的
Balance 对象,只有后者流向打印器和后续处理。
另两类错误在同一分支产出但不替换条目:账户查不到
open_close_map 报
Invalid reference to unknown account(:109-118);Open
限定了币种而断言用别的币种报
Invalid currency ...(:120-134,beancount/ops/balance_test.py:103-113)。前者过去会
continue 掉整条断言,2024-07-05 commit
664e4b32 改成保留条目、交给 validation
再报一次(beancount/ops/balance_test.py:306-319
同时期待两类错误,条目数仍是 3)。这两类错误都不
continue,代码继续往下算
subtree_balance、diff_amount:账户未知或币种非法时数值比较仍会执行,同一条
Balance
可能同时产生引用/币种错误与数值失败错误,数值失败时依旧被
_replace 成带 diff_amount
的版本(:104-181)。
pad 把所有 Pad 按 account
分组(beancount/ops/pad.py:52-53),用
realization.postings_by_account 把账本摊成"账户 →
TxnPosting 或指令"(:56)。对每个有 pad
的账户,收集它自己和全部后代账户的条目、按
data.posting_sortkey
排序(:66-72),单趟扫描维护一个
Inventory:TxnPosting
累加(:80-83);Pad 且
entry.account == account_ 时记为 active_pad
并清空
padded_lots(:85-90);Balance
则比容差、必要时补齐(:92-189)。那个账户判断必不可少——postings_by_account
把每条 Pad 同时挂到 account 与
source_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)。
| 限制 | 实现 | 效果 |
|---|---|---|
| 同一 pad 下每种币种只补一次 | padded_lots(beancount/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,后面一条失败的断言会被静默补平、错误报到错误的位置上。
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_postings(beancount/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 实例本身不可哈希,不能直接作字典键;同时它是
NamedTuple(beancount/core/data.py:155-174),按字段值比较,字段全同的两个不同
Pad 对象会相等。用 id(active_pad)
作键既绕开不可哈希的问题,也按对象身份而非字段值关联生成条目。
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,不中断加载。
beancount/loader.py:65-68 的 PLUGINS_POST
依次是
beancount.ops.pad、beancount.ops.balance。pad
必须先跑:它合成的交易要成为 balance
眼里的真实交易。两者都排在用户自己的 plugin
之后,这样用户插件生成的交易能被两者看到。位置由 2024-01-06 commit
60f1c40f 改定,此前 pad 在
PLUGINS_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
在跑任何插件之前先对 entries 按 entry_sortkey
排序(SORT_ORDER:Balance −1、Pad
默认
0,beancount/core/data.py:695-705),pad()
内部又按同一 SORT_ORDER 的
posting_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_AUTO、PLUGINS_POST
才会依次并入执行序列(beancount/loader.py:662-670);"raw"
模式只运行账本 option "plugin" 显式列出的插件,不会自动跑
pad/balance,本节所述先后顺序也无从谈起。
| 决策 | 理由 | 证据 |
|---|---|---|
| 断言只比单一币种的 units 总和;父账户汇总整棵子树 | 对账单给的就是"某币种若干",成本与批次是账本内部结构;一张对账单常对应多个子账户 | beancount/ops/balance.py:136-138,148,151;beancount/core/inventory.py:277-290;beancount/ops/pad.py:95-99 |
同日 Balance 排在 Transaction 之前 |
断言语义定义为"当日开始时" | beancount/core/data.py:695-705;beancount/ops/balance_test.py:214-227 |
未写 ~ 时按小数位自动推断的容差,是交易内部同一
exponent 基础推断值的 2 倍;整数断言容差为零;显式 ~
不受此约束 |
手抄余额的舍入误差比单笔交易内部大;写 100 USD
表达的就是精确整数;用户显式给出的容差不该再被放大 |
beancount/ops/balance.py:29-43;beancount/core/interpolate.py:145-198 |
| 断言失败不把运行余额强制改成期望值,只替换条目 | 失败不会掩盖差额:diff_amount 只写入当前
Balance
的替换条目,未消除的差额会被同账户后续断言继续看到;diff_amount
随条目流到打印器 |
beancount/ops/balance.py:92-104,146-183;beancount/ops/pad_test.py:549-570;beancount/parser/printer.py:317 |
| 只为被断言的子树建 realization | 完整 realization 对只有几条断言的账本是浪费 | beancount/ops/balance.py:64-87;5421694f |
pad 只补 pad 指定的那个账户 |
自称 "very simple kind of pad",够用且易懂 | beancount/ops/pad.py:34-36;beancount/ops/pad_test.py:277-329 |
| 每种币种每条 pad 只补一次,且每条 Balance 都消费额度(不论成败) | 一条 pad 表达"这一刻之前的历史",不该反复兜底后续遗漏;修 issue 78:否则失败的断言会被后一条的补齐静默抹平 | beancount/ops/pad.py:111,188-189;9b632075 |
| 账户存在按成本计价持仓时,为每个这样的 position 报错(但仍照常补齐);没用上的 pad 报错 | 补出的 lot 无成本,与有成本的持仓混在一起会让"按成本"与"不按成本"的余额语义混淆,用报错提醒用户核对;静默失效会让用户以为补齐生效了 | beancount/ops/pad.py:112-139,199-201;beancount/ops/pad_test.py:239-275,450-465 |
pad 排在 balance 之前且都在
PLUGINS_POST |
补齐交易要被断言看到;用户插件的交易要被 pad 看到 | beancount/loader.py:65-68;60f1c40f |
| 现象 | 后果 | 证据 |
|---|---|---|
【文档漂移】beancount/ops/balance.py 的模块 docstring
是 """Automatic padding of gaps between entries.""" |
与 beancount/ops/pad.py:1 逐字相同,描述的是 pad
而非余额断言 |
beancount/ops/balance.py:1;beancount/ops/pad.py:1 |
【文档漂移】beancount/core/data.py 中
Balance 的 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 USD
与 0.00 USD 语义不同 |
差额恰等于容差时通过;前者容差 0,后者 0.01 | beancount/ops/balance.py:34-43,159;beancount/ops/balance_test.py:340-370,372-392 |
pad 分组按 account_ 的整棵子树聚合条目 |
子账户上的 Balance
也进入这个循环、与子树总额比较,而补齐 posting 仍只落在
pad 指定账户上 |
beancount/ops/pad.py:66-72,100;beancount/ops/pad_test.py:277-329 |
| 带成本持仓时 pad 报错后仍继续补齐 | :120-131 无
continue/break,补齐交易照样生成,故不会再报
Unused Pad;多个成本批次各报一条 |
beancount/ops/pad.py:112-139;beancount/ops/pad_test.py:450-465 |
失败的断言不把 pad_balance 拉平 |
后一条同额断言仍看到真实余额,pad 因此仍生效 | beancount/ops/pad.py:92-189;beancount/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 |
beancount/ops/balance_test.py(TestBalance
:13、TestBalancePrecision
: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 | 同产 BalanceError 与
ValidationError,条目不被吞掉 |
test_get_balance_tolerance__explicit /
__regular |
324-370 | 显式 ~ 覆盖一切;九条推断结果,整数为 0 |
test_balance_with_tolerance |
372-392 | 边界值用 > 判定 |
beancount/ops/pad_test.py(TestPadding
: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:147 的
assert real_account is not None
只被成功路径反复执行到(entry.account 必在
asserted_accounts 中,因而必被
beancount/ops/balance.py:83-87
预建),没有测试触发它的失败分支,按当前预建账户逻辑该分支也不可达;负的显式容差;beancount/ops/pad.py:181-186
的 raise ValueError(补齐 lot
无成本,构造不出触发条件);同币种多个成本批次产生多条
PadError;A→B 与 B→A 互相 pad。
2017-04-30 之前的提交,文件路径带 src/python/ 前缀,由
commit 859f341e 统一迁移到 beancount/。
| 日期 | 提交 / 记录 | 变化 |
|---|---|---|
| 2014-06-14 | 3ebb4849、ae90524f |
负成本检查移出 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 | fab8211e、5f364406、25ca256e |
按小数位推断容差(初期默认关闭);5f364406 于 05-10
引入实验性显式容差语法 ~ <tolerance>;pad
改用同一套容差检查 |
| 2015-05-17 | CHANGES:3894-3898 |
发布记录追认显式容差语法为 EXPERIMENTAL 特性,同批带出
default_tolerance、account_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 | ef5f10c2、99ee920a |
显式容差转正;移除 use_legacy_fixed_tolerances |
| 2017-01-21 / 09-17 | a4100093、b4faf7ae、0edac4a6 |
断言币种须在 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.pad 从 PLUGINS_PRE 移到
PLUGINS_POST 的 balance 之前 |
| 2021-01-30 / 2024-07-05 | 4504c2b1、664e4b32 |
未知账户错误文案与 Transaction 统一;不再丢弃该条 Balance |
| 2024-09-15 | 2321a201 |
pad 生成的 posting 用 Balance 的 meta 取代空 dict |
| 2025-05-23 | ae5d5f14 |
新增正式名 tolerance_multiplier;旧名
inferred_tolerance_multiplier
保留为已弃用别名,映射到同一选项 |
check 自建裁剪过的
RealAccount
树(get_or_create、get、compute_balance);pad
用 postings_by_account
摊平账本(beancount/core/realization.py:304-307 把每条
Pad 挂到两个账户下)。两者都不做完整 realization。Inventory,比较入口
get_currency_units(beancount/core/inventory.py:277-290);pad
的差额是 Position.from_amounts 造的无成本
position,安全检查用
is_negative_at_cost(beancount/core/position.py:315-321)。amount.sub(beancount/ops/balance.py:154、beancount/ops/pad.py:101),币种不一致由它兜底。BalanceError 定义在
beancount/core/interpolate.py:38-43,但生产代码只由
beancount/ops/balance.py 构造并专用于余额断言错误;交易
booking 失败用
BookingError(beancount/parser/booking.py:20-23),插值失败用
InterpolationError(beancount/parser/booking_full.py:789-795),三者互不相通。容差系数与
:155,188 共享,只是这里乘 2。SORT_ORDER、entry_sortkey、posting_sortkey(beancount/core/data.py:705,708-719,736-747)提供排序保证;flags.FLAG_PADDING(beancount/core/flags.py:10)标记合成交易。:65-68),插件后重排序(:740),插件异常收成
LoadError(:717-736)。validate_duplicate_balances(beancount/ops/validation.py:121-157)检查同一
(账户, 币种, 日期)
上金额不同的重复断言;ALLOW_AFTER_CLOSE 允许
Balance 出现在 Close
之后(:47)。beancount/parser/printer.py:317,324-328
渲染 ~ <tolerance> 与 ; Diff:
注释;beancount/core/compare.py:25 比较条目时忽略
diff_amount。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-392;beancount/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-290;beancount/core/position.py:315-321;beancount/core/interpolate.py:38-43,155,188,236,245-259;beancount/core/flags.py:10;beancount/core/compare.py:25;beancount/loader.py:65-68,717-736,740,760,790-795;beancount/ops/validation.py:47,121-157;beancount/parser/booking.py:20-23;beancount/parser/booking_full.py:789-795,1011-1016;beancount/parser/options.py:537-540,544-558;beancount/parser/grammar.py:729-731;beancount/parser/printer.py:317,324-328;CHANGES:309-311,469-471,1259-1260,3737-3743,3894-3898,3944-3945。
commit:3ebb4849(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)。