先说结论
如果你们在 Odoo 企业版里启用了 /home/ubuntu/odoo-temp/enterprise/account_budget_purchase,最容易踩坑的一句话就是:
采购预算不是“收货就释放”,而是“确认采购先记 committed,账单过账再记 achieved;在 budget report 里,未开票部分会继续留在 committed 里”。
这意味着:
- 确认采购单后,预算就可能被占用;
- 收货完成,不代表预算占用立刻消失;
- 供应商账单过账后,金额才真正进入
achieved_amount; - 而
committed_amount在报表里看的不是“未收货”,而更接近已确认但尚未开票完的采购承诺。
这正是很多实施现场会困惑的地方:
- 仓库说已经收完了,为什么预算还红着?
- 财务说账单还没过账,为什么预算报表已经占上了?
- 采购说我已经部分开票,为什么 committed 只降一部分?
答案都在这个企业版模块的三层实现里。
第一层:采购预算不是从 PO 表头开始,而是从 analytic distribution 开始
在 models/purchase_order_line.py 里,企业版先给采购行加了几个关键字段:
analytic_jsonbudget_line_idsis_above_budget
其中真正的入口不是采购金额本身,而是 analytic_distribution。
也就是说,Odoo 不是看到一张采购单就自动去撞预算,而是先问:
- 这条采购行有没有分析分摊;
- 这些分析账户能不能映射到某条
budget.line; - 采购日期是否落在预算区间内;
- 各 analytic plan 维度是不是完整对上。
_compute_analytic_json() 会把 analytic_distribution 重新整理成 JSON 结构,并按 root plan 拆成预算报表可识别的字段。随后 _compute_budget_line_ids() 才用这些维度去找匹配预算行。
这一步特别关键,因为它说明:
企业版采购预算控制的核心,不是“采购模块自己有预算字段”,而是“采购行带着 analytic 分布,去接预算体系”。
所以如果实施里只是开了预算模块,却没把采购行 analytic distribution 设计好,预算预警看起来就会时灵时不灵。
第二层:为什么采购单还没确认,界面上就可能提示超预算
models/purchase_order.py 和 models/purchase_order_line.py 里都定义了 is_above_budget。
它的计算思路不是“等财务过账后再说”,而是先做一个前置预警:
- 对采购行:如果
budget.committed_amount + 当前行未开票金额 > budget.budget_amount,就标红; - 对采购单:如果任一预算行加上整张单的未开票金额后超出预算,表头按钮会变红。
这里最值得注意的是计算口径:
uncommitted_amount = line.price_unit * (line.product_qty - line.qty_invoiced)
以及表头也是按:
line.price_unit * (line.product_qty - line.qty_invoiced)
来汇总。
也就是说,前台预警关注的是:
这张 PO 还有多少金额尚未被账单“吃掉”
而不是:
- 收了多少货;
- 仓库做没做入库;
- 供应商是否已经发票到了。
所以你会看到一个很反直觉的现象:
- 采购单已经确认;
- 货甚至已经全收;
- 但只要
qty_invoiced还没跟上,预算压力仍然可能存在。
这不是 bug,而是企业版故意把“采购承诺”提前暴露出来。
第三层:为什么 budget_line_ids 查匹配预算时又会看 qty_received
看到这里很多人会继续疑惑:
_compute_budget_line_ids() 里有个条件:
if line.analytic_json and line.product_qty - line.qty_received > 0:
这看起来又像在看“未收货数量”。
这里别混淆两件事。
这一层的目标不是计算最终报表金额,而是:
判断当前采购行还要不要继续挂着预算匹配关系和界面预警上下文。
换句话说:
budget_line_ids更像是“这条采购行当前应关注哪些预算行”;- 它会受收货进度影响;
- 但预算报表里的
committed_amount真正怎么汇总,不是靠这段逻辑直接决定的。
这就是为什么很多人只盯 purchase_order_line.py 会越看越乱。
真正决定预算报表的,是 reports/budget_report.py。
第四层:budget report 里的 committed 为什么不是“未收货金额”,而是“未开票金额”
reports/budget_report.py 里重写了 budget.report,新增了一类 line_type = 'committed' 的报表行。
最关键的 SQL 在 _get_pol_query():
- 先汇总每条采购行已经过账的账单数量
qty_invoiced; - 然后只保留
po.state = 'purchase'的确认采购单; - 最后用:
(pol.product_qty - COALESCE(qty_invoiced_table.qty_invoiced, 0))
去算 committed。
注意这里不是 qty_received,而是 qty_invoiced。
所以企业版预算报表表达的其实是:
这笔确认采购里,还有多少金额尚未被正式账单转化。
这就解释了为什么:
- 收货完成后,
committed_amount不一定下降; - 只有账单过账、
qty_invoiced真正增加后,committed 才会往下走; - 多次分批开票时,committed 也会按未开票部分逐步下降。
从财务治理视角看,这个设计很合理。 因为“货到了”解决的是供应履约问题, 但“预算是否已经落成真实费用”在很多组织里仍要看账单入账。
第五层:为什么 achieved 要等账单过账,而不是建了 draft bill 就算
models/budget_line.py 里,committed_amount 与 achieved_amount 最终都来自 budget.report 聚合。
在测试 tests/test_commited_achieved_amount.py 里,官方明确覆盖了一个场景:
- 采购单确认;
- 创建供应商账单;
- 账单还没过账;
- committed 保持不变,achieved 仍然是 0;
- 直到
action_post()之后,achieved 才增加。
这说明企业版在预算口径上非常克制:
draft bill 不代表真实发生,posted bill 才代表 achieved。
所以 Odoo 这里分得很清楚:
- committed:已确认采购承诺 + 已入账部分一起构成的预算承诺口径;
- achieved:真正过账到会计的已实现费用;
- draft bill:只是中间态,不应该污染 achieved。
这也是实施里最该讲给财务和采购的一点:
预算报表不是仓库看板,也不是 AP 草稿箱,它表达的是采购承诺与会计实现之间的过渡结构。
第六层:为什么部分收货、部分开票时最容易看不懂
这个模块在测试里反复覆盖了:
- 分批收货;
- 分批开票;
- 多次账单;
- 多币种;
- 含折扣、价税合并;
- credit note 冲回。
它背后的统一原则其实很简单:
1. committed 看“还没被发票吃掉多少”
不是看仓库收了多少,而是看确认采购里还有多少没变成已过账账单。
2. achieved 看“已经正式入账多少”
只有已过账账单、退款、相关 analytic line,才会进 achieved。
3. 收货是业务进度,不是预算转实现的唯一触发器
收货会影响某些预算匹配与采购流程,但不会直接把 committed 整体搬到 achieved。
所以如果你们是:
- 先全收,月底统一开票;
- 或分批收、分批票;
- 或仓库早收货、财务晚过账;
那预算报表就会出现“货都收了,但 committed 还在”的现象。
这正是源码设计出来的结果,不是数据错。
第七层:新手最容易误解的 5 件事
1. 以为收货完成就等于预算已实现
不对。企业版预算实现更依赖 posted bill,而不是单纯 receipt。
2. 以为 committed 只代表“未收货采购”
不对。budget_report.py 明确更偏向未开票余额。
3. 以为 draft bill 已经会影响 achieved
不对。官方测试明确要求 draft bill 不应推进 achieved。
4. 以为预算预警和预算报表完全同一口径
不对。表单预警、预算匹配、预算报表是三层逻辑,不是一个字段改名到处复用。
5. 以为只要有采购金额就能命中预算
不对。没有正确的 analytic_distribution,采购行可能根本接不上预算线。
实战里最该怎么用
1. 先给业务讲清三套语言
- 采购看的是:这张单是否超预算;
- 仓库看的是:收货进度;
- 财务看的是:已过账费用;
- 预算报表看的是:承诺与实现的组合。
别拿一种口径去要求另外一种报表。
2. 如果客户坚持“收货就该释放预算”,要先确认他们想要的是哪种管理哲学
Odoo 企业版默认更偏财务承诺口径。
如果企业内部要做“按收货释放预算”的管理,通常意味着要定制 budget.report 的 committed 计算逻辑,而不是只改前端标签。
3. analytic distribution 是实施成败关键
预算匹配依赖分析分摊。没有这层,采购预算控制就只剩 UI 错觉。
4. 多次账单和贷项通知单一定要一起验证
因为 achieved 与 committed 的切换不是一次性的,退款、负价、跨币种都会影响最终口径。
一句话记忆法
Odoo 企业版采购预算里,收货解决的是履约进度,账单过账解决的是费用实现;
account_budget_purchase用 committed 和 achieved 把这两者故意分开了。
DISCUSSION
评论区