先说结论
如果你总觉得 Odoo 的库存路线和规则“很像”,那是因为它们确实都在参与决策;但它们做的不是同一层工作。
最实用的理解是:
stock.route负责表达“这单需求允许走哪些履约方向”,stock.rule负责把其中某一个方向真正落成 move、PO、MO 等可执行动作。
也就是说:
- route 更像策略集合 / 路线包
- rule 更像具体执行规则 / 落地动作
所以调试库存问题时,不能只问“产品挂了什么 route”,还要继续问:
- 命中了哪条
stock.rule - 这条 rule 的
action是什么 - 它的
procure_method是什么 - 它的来源库位、目标库位、操作类型、仓库上下文是什么
很多“为什么这次去采购、那次走内部调拨、还有一次直接出制造单”的答案,最后都在这里。
为什么很多人会把 route 和 rule 混在一起
因为前台界面里先看到的往往是路线。
比如你会看到:
- Buy
- Manufacture
- Replenish on Order (MTO)
- Dropship
- Cross-Dock
这些名字很像“动作”。
但从源码设计来看,它们更像:
给需求打一个供应策略标签。
真正执行的时候,Odoo 不是看到一个 route 名字就直接生成单据,而是继续去找与当前需求上下文匹配的 stock.rule。
所以 route 是“方向声明”,rule 才是“动作落点”。
先把两个对象讲成人话
stock.route 像什么
它更像:
一组可被启用的供应链策略。
它回答的是:
- 这个产品 / 这个仓 / 这条业务线
- 允许系统沿哪些路径去 fulfil 需求
所以 route 本质上是在提供候选路径。
stock.rule 像什么
它更像:
当某地出现需求时,系统应该怎么响应。
一条 rule 里最关键的,不是名字,而是这些字段组合:
actionprocure_methodlocation_src_idlocation_dest_idpicking_type_idwarehouse_idroute_iddelay
它们一起定义了:
- 这条需求是从哪拉货
- 拉到哪
- 用什么操作类型
- 是吃库存还是继续触发下一层规则
- 是否走采购或制造扩展
所以 rule 才是 Odoo 供应链“真正执行”的最小单元。
一条销售需求是怎么走进路线 / 规则引擎的
如果从销售单往下看,入口通常在:
sale.order.line._action_launch_stock_rule()
这一步不会直接决定“建出库单还是采购单”,而是先把销售行包装成 procurement。
源码里准备的上下文很关键,典型包括:
route_idswarehouse_iddate_planneddate_deadlinepartner_idlocation_final_idreference_ids
最值得你记住的是:
销售层只是在说“我现在有一个需求,请库存规则系统决定怎么 fulfil”。
真正做分流的是后面的:
self.env['stock.rule'].run(procurements)
这就是 route / rule 决策系统的总入口。
stock.rule._get_rule():Odoo 到底怎么选中某条规则
这是整套机制最值钱的一步。
很多人以为 Odoo 是“看产品上挂了哪条路线,然后直接执行”。
实际上不是。
源码里的 _get_rule(product_id, location_id, values) 做得更细,它会综合考虑:
- 当前需求的目标位置
- 显式传入的
route_ids - 包装对应路线
- 产品 / 产品分类上的路线
- 仓库路线
- 仓库上下文
- 位置层级(会沿着 location 父层向上找)
- route sequence 与 rule sequence
一个很容易忽视、但非常关键的事实
_get_rule() 匹配 pull rule 时,核心条件是:
location_dest_id命中当前需求位置action != 'push'
也就是说:
pull 规则首先是按“需求出现在哪里”来找,不是先按“我要从哪里发货”来找。
这会直接改变很多人的调试思路。
很多人配规则时只盯 source location,结果一旦 destination 或 warehouse 上下文不对,就会觉得系统“乱选规则”。
其实不是乱,而是你理解的匹配入口错了。
规则优先级大致怎么排
从源码搜索逻辑看,优先级大致是:
- procurement 显式带进来的
route_ids - 包装关联路线
- 产品 / 产品分类路线
- 仓库路线
而同一批候选规则里,又会按:
route_sequencesequence
来取更靠前的那一条。
所以当你发现“同一个产品为什么在两个仓表现不一样”,往往不是产品字段变了,而是:
- warehouse route 不同
- destination location 不同
- sequence 排序不同
- 销售行显式 route 抢了优先级
route 决定“可走哪些路”,rule 决定“这一步到底干嘛”
当 Odoo 找到匹配 rule 后,接下来不是统一动作,而是看 action。
最常见的分支是这些:
pullpull_pushbuymanufacturepush
这几个分支,决定了同一个需求最后会变成完全不同的业务对象。
_run_pull():最常见、也最容易被低估的路径
对很多库存链路来说,真正的主干不是采购也不是制造,而是 pull。
_run_pull() 的核心动作可以概括成三步:
- 校验 rule 上是否有
location_src_id - 根据 procurement + rule 生成
stock.movevalues - 按公司批量
sudo().create()move,然后执行_action_confirm()
这里有几个细节非常重要。
1. move 是用 rule 反推出来的
_get_stock_move_values() 会把 rule 里的这些东西灌进 move:
location_id←location_src_idlocation_dest_id/location_final_idpicking_type_idrule_idwarehouse_idprocure_methodroute_idsoriginreference_ids
所以你看到的一张库存移动,不是“销售单直接建出来的”,而是 规则翻译后的结果。
2. _action_confirm() 不是收尾,而是下一跳开始
很多人以为 move create 之后就结束了。
其实 _action_confirm() 才是后续链条继续长出来的地方。
特别是在 make_to_order 场景下,确认 move 本身又可能继续触发新的 procurement,形成:
- 需求触发 move
- move confirm 再触发上游补货
- 上游再命中新的 rule
所以你看到的“链条会自己长”并不是魔法,而是 confirm 阶段在继续传播需求。
3. Odoo 在这里故意 sudo
源码明确是以超级用户创建 move。
原因很现实:
- 业务触发人可能是销售员
- 销售员不一定有底层库存对象完整权限
所以系统设计是:
前台业务对象由业务用户发起,底层履约对象由系统安全地代为创建。
这也是为什么不少链路问题看起来像“销售动作居然创建了库存对象”。
procure_method 才是很多人漏看的第二个分叉口
很多人只看 action,但真正影响后续行为的还有 procure_method。
Odoo 常见三种:
make_to_stockmake_to_ordermts_else_mto
可以把它理解成:
make_to_stock
先把需求当成“从当前来源库存满足”。
它更偏库存池思维。
make_to_order
不优先吃现货,而是继续向上游再触发一层规则。
它更偏需求驱动思维。
mts_else_mto
能吃库存先吃库存,不够的那部分再继续触发上游规则。
它最贴近很多企业真实需求,所以也最容易让调试复杂化。
因为这意味着:
- 一部分需求被现货满足
- 另一部分需求继续派生补货链
如果你只盯最终单据,很容易看不出它是“同一条 rule 下的混合行为”。
buy:为什么有些需求最后会生成采购单
当命中的 rule action 是 buy 时,逻辑会转到 purchase_stock 对 stock.rule 的扩展。
这一步不是简单“立刻新建一张 PO”,而是要继续做几件事:
- 找匹配供应商
- 计算采购分组 domain
- 查已有采购单是否可复用
- 合并 procurement
- 决定更新已有 PO line 还是新建 line
源码里还有个很值得注意的点:
当 procurement 带有 buy 路线时,系统会把仓库 reception route 也并入上下文。
这说明一件事:
- buy 不是凭空生成采购单
- 它仍然要和仓库的收货路线、后续入库动作协同
所以采购不是 route / rule 系统之外的另一套宇宙,而是它的延伸。
manufacture:为什么同样是需求,有时会长成 MO
当命中 manufacture action 时,逻辑会转入 mrp 对 stock.rule 的扩展。
这一层最核心的事情有三件:
- 找可用 BOM
- 决定是复用已有草稿 / 确认中的 MO,还是新建
- 准备并创建制造单,再按条件自动 confirm
源码里还有两个很关键的细节:
1. phantom kit 会先 explode
如果当前产品命中 phantom BOM,run() 会先把 kit 展开成组件 procurement,再走后续规则。
这意味着:
有些你以为应该生成 MO 的场景,实际上根本不会建制造单,而是直接把组件需求放进库存链路。
2. manufacture 也不是“只建一张 MO 就完”
MO 的创建、确认、原料 move、成品 move,后面仍然是被规则系统和 move confirm 继续串起来的。
所以 manufacture 只是某一跳的动作类型,不是独立于库存链路的孤岛。
push 为什么更容易被新人理解反
pull 的逻辑是:
- 某地有需求
- 系统想办法从上游补过去
push 的逻辑则是:
- 某地一旦来了货
- 系统再继续把货推去下一个位置
所以最简明的区别是:
- pull 是需求驱动
- push 是到货驱动
新人常把两者都理解成“调拨规则”,结果一调就乱。
真正要分清的是:
- 是因为某处缺货,才触发补货?
- 还是因为某处收到了货,才继续往后推?
这个边界一搞清楚,很多多步仓内流转配置就会清楚很多。
用一个业务例子把整套机制串起来
假设你卖一个可库存产品,客户下单 10 件。
场景 A:仓内现货足够
可能发生的是:
- 销售行发起 procurement
- 命中面向客户位置的 pull rule
- rule 的
procure_method = make_to_stock - 系统生成从仓内源位置到客户方向的 move / picking
这时你看到的是正常发货链。
场景 B:仓内没货,但路线允许采购
可能发生的是:
- 销售行发起 procurement
- 命中面向客户的 pull rule
- 这个 rule 或其上游 move 使用
make_to_order - 新一轮 procurement 在上游位置继续找 rule
- 命中
buyrule - 最终生成 RFQ / PO,并带起入库链
场景 C:仓内没货,但路线允许制造
链路就会变成:
- 销售需求进入 rule.run
- 上游位置命中
manufacture - 系统找 BOM
- 建 MO
- 原料准备与成品入库再继续由 move / rule 串起来
所以你会发现:
同一张销售单不是直接决定采购还是制造;真正决定分流的是 route + rule + procure_method + warehouse/location 上下文。
这套设计为什么强
因为它把“业务入口”和“履约实现”拆开了。
销售、补货、制造、采购,都可以共享同一套需求分发思想:
- 先提出需求
- 再按上下文找可用 rule
- 再由 rule 决定下一步动作
这样做的好处是:
1. 可扩展
你可以扩采购、扩制造、扩仓内流转,而不用重写所有业务入口。
2. 可组合
同一个需求可以穿过多跳规则,而不是死绑在一条硬编码流程里。
3. 可调试
只要你知道“需求在哪出现、命中哪条 rule、这个 rule 怎么继续传播”,链路就能拆开看。
实战里最容易踩的 8 个坑
1. 只看 route,不看最终命中的 rule
会停留在概念层,查不到真正动作来源。
2. 只看 source location,不看 destination location
而 _get_rule() 对 pull 的起点恰恰更偏 destination。
3. 忽略 procure_method
你会解释不了为什么同一条路线有时吃库存、有时继续向上游传。
4. 不看 warehouse 上下文
同样的产品在不同仓库命中不同规则,非常常见。
5. 不看 route / rule sequence
规则不是“谁都能命中”,是有优先级的。
6. 把 push 和 pull 当成同一类调拨
最后多步库位流转一定会配乱。
7. 以为 buy / manufacture 是 route 系统外的附加模块
其实它们是 stock.rule action 的扩展分支。
8. 只看创建动作,不看 confirm 后传播
很多异常不是出在 create,而是出在后续 _action_confirm() 的继续派生。
真正高效的调试顺序
如果你要排查“为什么这次走了这条库存链”,我建议按这个顺序看:
-
需求从哪里来 - 销售行、补货规则、手工 move,还是制造需求
-
procurement values 里带了什么上下文 -
route_ids、warehouse_id、location_final_id、date_planned -
_get_rule()最终命中了哪条 rule - 重点看 destination、warehouse、sequence -
这条 rule 的
action是什么 - pull、buy、manufacture、push -
这条 rule 的
procure_method是什么 - MTS、MTO、MTS else MTO -
生成了什么对象 - move、picking、PO、MO
-
这些对象 confirm 后有没有继续派生下一跳
只要按这条线查,绝大多数“路线和规则看不懂”的问题都会变得可解释。
一句话记忆法
把这套机制记成一句话:
route 决定“允许走哪些履约方向”,rule 决定“当前这一步到底执行什么动作”,而真正让链路不断长出来的,是 rule 命中后的 move / procurement confirm 传播。
理解这一句之后,你再看 Odoo 的库存路线、补货、采购、制造,就不再像四套分裂系统,而会更像一台统一的供应链决策引擎。
DISCUSSION
评论区