企业采购预算

Odoo 采购预算为什么“收货了还占预算”:account_budget_purchase 里 committed / achieved 与未开票余额链路讲透

很多人在 Odoo 企业版里启用采购预算后,会以为收货一完成,预算占用就该立刻减少。可 `account_budget_purchase` 的真实设计并不是“按收货释放预算”,而是把确认采购单、已过账账单、未开票余额拆成三层口径。本文从 `budget.line`、`budget.report`、`purchase.order.line` 源码出发,把 committed / achieved 的切换时机讲透。

企业 采购
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

如果你们在 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_json
  • budget_line_ids
  • is_above_budget

其中真正的入口不是采购金额本身,而是 analytic_distribution

也就是说,Odoo 不是看到一张采购单就自动去撞预算,而是先问:

  1. 这条采购行有没有分析分摊;
  2. 这些分析账户能不能映射到某条 budget.line
  3. 采购日期是否落在预算区间内;
  4. 各 analytic plan 维度是不是完整对上。

_compute_analytic_json() 会把 analytic_distribution 重新整理成 JSON 结构,并按 root plan 拆成预算报表可识别的字段。随后 _compute_budget_line_ids() 才用这些维度去找匹配预算行。

这一步特别关键,因为它说明:

企业版采购预算控制的核心,不是“采购模块自己有预算字段”,而是“采购行带着 analytic 分布,去接预算体系”。

所以如果实施里只是开了预算模块,却没把采购行 analytic distribution 设计好,预算预警看起来就会时灵时不灵。


第二层:为什么采购单还没确认,界面上就可能提示超预算

models/purchase_order.pymodels/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()

  1. 先汇总每条采购行已经过账的账单数量 qty_invoiced
  2. 然后只保留 po.state = 'purchase' 的确认采购单;
  3. 最后用:
(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_amountachieved_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

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。