预算功能如果只是一个“金额输入框”,企业很快就会放弃它。真正有用的预算系统,必须回答两个问题:改预算以后,历史怎么保留?;看执行进度时,理论上应该花到哪里? account_budget 的设计正是在解决这两个问题。
主要参考:
enterprise/account_budget/models/budget_analytic.pyenterprise/account_budget/models/budget_line.pyenterprise/account_budget/wizards/budget_split_wizard.py
一、预算修订不是覆盖原单,而是显式建立 parent/child 版本链
BudgetAnalytic 上有 parent_id 和 children_ids,说明企业版预算从一开始就没打算靠“直接改原预算”来完成调整。create_revised_budget() 会复制当前预算,生成带 REV(时间) 的新名字,并把原预算挂成父节点。
这件事非常关键。财务和业务在开会时常常争论“原来是这么定的吗”,如果没有 revision chain,预算只会变成一张会被不断改写的表。企业版选择把修订本身建模成对象,而不是让用户靠聊天记录回忆历史。
二、状态机会把“当前有效版本”与“历史版本”分开
action_budget_confirm() 会在确认新预算时,把原本仍处于 confirmed 的父预算改成 revised。换句话说,系统不希望多份“有效预算”同时悬空存在。它允许历史保留,但不鼓励口径并存。
新手常见误区是:以为 revision 只是复制一份备份。其实 revision 的真正意义是 把旧版本退出当前经营口径,同时把新版本推成当前参考基线。
三、理论金额不是简单月均摊,而是按起止日含首尾地滚动
BudgetLine._compute_theoritical_amount() 的逻辑很值得细看。它不是“预算 ÷ 12 × 当前月份”,而是:
- 预算线有明确
date_from和date_to; - 计算时把首尾日都算进去;
- 用今天落在区间中的相对进度去乘预算金额。
这意味着理论值是按日口径推进的,而不是按会计初学者想象的“均匀按月”。如果预算跨月、跨季或者只有半个月,这种口径差异会非常明显。
四、预算拆分向导处理的是“结构化生成”,不是复制粘贴
BudgetSplitWizard.action_budget_split() 会先按 analytic plan 组合生成预算行,再按 month / quarter / year 周期批量创建多份预算。也就是说,拆分不是为了方便录入,而是为了确保 时间切片和分析维度一起成型。
实战上这很有价值:
- 先按分析维度准备账户组合;
- 再按周期切开预算;
- 最终让后续 achieved/theoretical 的比较发生在同一结构里。
如果团队一开始就用 Excel 手动拆,再导入,往往最先乱掉的就是维度组合和周期边界。
五、聚合规则告诉你:理论值不是纯 SQL 可得字段
_read_group_select() 和 _read_group_postprocess_aggregate() 专门对 achieved_amount、theoritical_amount、theoritical_percentage 做额外处理。这说明预算理论值不适合被理解成一列静态数值,它更像一个依赖上下文和明细集合的结果。
这也是很多报表误差的来源:团队把理论值当作“数据库里现成的一列”,而不是“预算线当前时点上的计算结果”。
六、新手最容易忽略的实战边界
- 修订预算不会抹掉旧预算,历史本来就该保留。
- 理论值是按区间推进,不是按自然月机械平均。
- 拆分向导不只是提效工具,它在帮你锁定维度结构。
七、落地建议
- 预算会前先约定 revision 规则:什么情况允许新建修订版,什么情况只改草稿。
- 对跨月项目,培训业务看理论值时按“剩余天数/已过天数”理解,不要拿月均值硬对。
- analytic plan 先治理再拆预算,否则拆出来的只是更细的混乱。
八、结论
account_budget 的成熟之处,在于它把预算从“一个数字”变成了“一个带版本、带时间切片、带理论口径的经营对象”。这样预算调整才有历史,预算执行才有解释,预算报表才值得被拿来做决策。
DISCUSSION
评论区