先说结论
Odoo 的补货异常处理,不是“碰到一条错就立刻停下并吐一句日志”。
stock.rule.run() 更像一个调度入口,它会先:
- 接收一批 procurement 需求
- 为每个需求找规则
- 按动作分组
- 执行
_run_pull()/_run_push()等具体实现 - 把过程中出现的失败统一收集
- 最后再决定是抛
UserError,还是抛ProcurementException
所以你在界面上看到的一句补货报错,背后经常不是“一条记录出问题”,而是:
一整批 procurement 在同一轮 run 里被聚合后的结果。
run() 的第一层职责:先把需求标准化
在 addons/stock/models/stock_rule.py 里,run() 开头会先给每条 procurement 补一些默认值,例如:
company_idprioritydate_planned
然后还会通过 _skip_procurement() 跳过本来就不该继续处理的情况,比如:
- 不是 consumable / storable 范畴
- 数量按 UoM 精度看等于 0
这说明:
进入规则匹配之前,Odoo 先确保“这是一条值得处理的补货需求”。
如果你排错时连这一层都没看,就很容易把“需求压根被跳过”误判成“规则没触发”。
第二层:找不到规则时,错误会先累积,不一定立刻抛
run() 会对每条 procurement 调 _get_rule()。
如果某条需求根本匹配不到规则,系统不会马上炸掉整个 Python 栈,而是先把这条错误追加进:
procurement_errors
典型报错文本就是:
- 在某个 location 找不到 replenishment rule
- 请检查产品路线配置
这一步很关键,因为它说明 Odoo 在这里的设计目标是:
尽量把一批问题一次性收集齐,再统一告诉你。
对于实施和运维来说,这比“一次只报一条”更适合补货场景。
第三层:按 action 分组,再调用 _run_*
当规则找到了以后,run() 不会直接对每条 procurement 各自处理到底,而是先按照 rule action 分组:
pullpushpull_push(会转成pull主链处理)
然后再统一调用对应的:
_run_pull()_run_push()- 或其他扩展模块注入的
_run_xxx()
这意味着一个很重要的排错原则:
不要只盯报错那条 procurement,本轮还有可能有同 action 的其他需求一起被处理。
也正因为如此,某一轮 scheduler 或 orderpoint 补货失败,往往会伴随多条关联异常一起出现。
ProcurementException 的意义:不是普通报错,而是“批量失败容器”
源码里 ProcurementException 不是简单字符串,而是带着:
procurement_exceptions
也就是一组 (procurement, error_message) 元组。
这说明它被设计出来,就不是为了表达“单次失败”,而是为了表达:
这轮补货里,有若干具体需求失败了,各自对应什么错误。
于是 run() 在调用 _run_pull() 等方法时,会把子过程抛出的 ProcurementException 再汇总回主错误列表。
这一步特别重要,因为它解释了为什么有些补货失败信息看起来像“好几类错误混在一起”:
- 有的缺 route
- 有的缺 vendor
- 有的缺 BOM
- 有的只是 location 配置不通
它们可能真的是同一轮里一起失败的。
orderpoint 调度为什么更像“边跑边剔失败项”
在 stock_orderpoint.py 里,scheduler 对 orderpoint 的处理又往前走了一步。
它会:
- 构造 procurements 批次
- 在 savepoint 里调用
stock.rule.run() - 如果捕获到
ProcurementException - 就把失败的 orderpoint 挑出来,记录异常,再把失败项从当前批次剔除
然后剩下能跑的继续跑。
这套设计的价值非常现实:
一个补货点坏了,不应该把整批其他正常补货全部拖死。
所以你看到 scheduler 最终留下了一堆 warning activity,并不代表整个调度完全没做事;更可能是“成功的已经成功,失败的被单独标出来了”。
实战里最稳的排查顺序
当你遇到 replenishment / scheduler / orderpoint 异常时,建议按下面顺序排:
1. 先确认 procurement 本身有没有被跳过
数量是不是 0,产品类型是不是不进入当前补货逻辑。
2. 再看 _get_rule() 能不能找到规则
重点检查:
- 产品 route
- 类别 route
- 仓库 route
- location
- 公司上下文
3. 再看规则对应的 action 会走哪条 _run_*
不同 action 下,失败根因完全可能不同。
4. 最后才去看界面上的最终报错文案
因为那句文案常常已经是聚合结果,而不是第一现场。
最常见的误区
误区 1:看见一条错误,就以为只有一条 procurement 出问题
实际上很可能是一批。
误区 2:把 scheduler failure 理解成“整批都没跑”
很多时候只是部分失败,其他批次已经成功落地。
误区 3:只查前端报错,不看 rule action 分流
这样会把 route、vendor、BOM、location 这几类根因混在一起。
最后的判断句
在 Odoo 里,stock.rule.run() 的本质不是“立刻执行一条补货规则”,而是:
把一批 procurement 需求做统一分流、统一执行、统一收集失败,并把失败结果组织成可回溯的异常集合。
理解了这一点,你排库存补货问题时就不会再只盯着最后那一句报错,而会回到更可靠的源头:
- 需求有没有成立
- 规则有没有命中
- 命中的 action 是什么
- 哪些失败是同一轮被一起聚合出来的
这才是补货问题真正稳定的排法。
DISCUSSION
评论区