很多团队第一次接触企业版预算时,会把预算行理解成一张“目标金额表”:填好预算、跑报表、看偏差。可真正上线后,最常见的问题不是预算没填,而是为什么同一条预算线,在不同时间点、不同 analytic 维度下看到的 actual / theoretical 金额都在变。
这里的关键不是 UI,而是三段后端逻辑怎么配合:
一、它先算实际发生额,而不是先看预算本身
BudgetLine._compute_all() 并不直接扫凭证行,而是通过 budget.report 做 _read_group() 聚合,把当前预算线 id 放进上下文后统一汇总 achieved。这说明预算行本质上只是“口径入口”,真正的已发生金额来自预算报表层的聚合结果。
这样设计有两个好处:
- 预算列表页和报表页共用同一套汇总口径,不容易出现“列表一个数、报表另一个数”。
- 后续如果 analytic plan、维度列、聚合逻辑变化,预算线不需要重新发明一套 SQL。
二、理论值不是平均摊,而是按包含首尾日的时间窗口推进
_compute_theoritical_amount() 很值得细看。它先确保 date_from/date_to 合法,然后把开始日和结束日都算进周期长度。源码里专门写了 “April 1 到 April 30 要算 30 天”。这意味着理论预算不是粗暴按月份分摊,而是按实际经过的天数比例推进。
新手最容易忽略的点有两个:
- 今天早于开始日,理论值会被压到起始边界;
- 今天晚于结束日,理论值会封顶到整个预算金额。
所以理论金额不是“实时进度条动画”,而是一个带上下界的时间函数。
三、analytic 口径决定你到底在看哪一类实际发生
action_open_budget_entries() 会遍历所有 analytic plans,把预算线上已设置的维度拼进 domain,然后打开 account_budget.budget_report_action。这一步非常关键:预算线不是只有一个金额字段,它其实隐含了多维 analytic 过滤条件。
也就是说,预算线钻取出来的明细,不是“这段时间里所有发生额”,而是“符合该预算 analytic 组合的发生额”。项目、部门、成本中心只要有一个维度没对齐,用户就会以为系统算错了。
四、真正的主链路是“预算定义 → 聚合计算 → 维度钻取”
结合 budget_analytic.py 可以把主链路梳理成:
- 预算主单进入确认态,预算线开始承担控制和分析语义;
- 预算线通过
_compute_all()从预算报表聚合实际值; - 通过
_compute_theoritical_amount()计算当前日期下的理论进度; - 用户再从
action_open_budget_entries()钻到具体报表行核对原因。
所以预算不是“静态录入 + 静态对比”,而是一个持续重算的分析视图。
五、新手最容易踩的坑
1. 以为预算超支就是源码 bug
很多“超支异常”其实来自 analytic 维度挂错,或者预算线时间窗口比业务理解更窄。
2. 以为理论值按月平均
源码按日期差算,不按自然月平均。跨月、半月预算、月末创建预算时,这个差异特别明显。
3. 以为钻取出来的是总账明细原样列表
实际上钻取动作仍然带预算维度 domain,看到的是预算口径下的报表结果,不是裸凭证全集。
六、实战注意事项
- 先统一 analytic 维度命名和挂载规则,再谈预算准确性。
- 预算线日期不要“图省事”写整年;窗口越粗,理论值越失真。
- 预算异常先看钻取 domain,再怀疑聚合逻辑。
- 如果要做二开,优先复用
budget.report口径,别绕开它直接写另一套统计。
结语
企业版预算线真正解决的问题,不是“给会计多一个目标金额字段”,而是把预算定义、实际发生、时间进度、analytic 维度压进同一条可追溯链路里。理解了这一点,很多“为什么数字不对”的问题,都会变成“到底是哪层口径没对齐”。
DISCUSSION
评论区