路线与规则

Odoo 库存路线和规则到底谁在做决定:stock.route 与 stock.rule 决策引擎深度解读

很多人知道 Odoo 有 route 和 rule,却总分不清谁负责“选方向”,谁负责“落动作”。本文从销售触发、规则匹配、动作执行到采购/制造分流,把 stock.route 与 stock.rule 的协作关系讲透。

Odoo 开发 库存
进阶 开发者 4 分钟阅读
0 评论 0 点赞 0 收藏 28 阅读

先说结论

如果你总觉得 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 里最关键的,不是名字,而是这些字段组合:

  • action
  • procure_method
  • location_src_id
  • location_dest_id
  • picking_type_id
  • warehouse_id
  • route_id
  • delay

它们一起定义了:

  • 这条需求是从哪拉货
  • 拉到哪
  • 用什么操作类型
  • 是吃库存还是继续触发下一层规则
  • 是否走采购或制造扩展

所以 rule 才是 Odoo 供应链“真正执行”的最小单元


一条销售需求是怎么走进路线 / 规则引擎的

如果从销售单往下看,入口通常在:

  • sale.order.line._action_launch_stock_rule()

这一步不会直接决定“建出库单还是采购单”,而是先把销售行包装成 procurement。

源码里准备的上下文很关键,典型包括:

  • route_ids
  • warehouse_id
  • date_planned
  • date_deadline
  • partner_id
  • location_final_id
  • reference_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 上下文不对,就会觉得系统“乱选规则”。

其实不是乱,而是你理解的匹配入口错了。

规则优先级大致怎么排

从源码搜索逻辑看,优先级大致是:

  1. procurement 显式带进来的 route_ids
  2. 包装关联路线
  3. 产品 / 产品分类路线
  4. 仓库路线

而同一批候选规则里,又会按:

  • route_sequence
  • sequence

来取更靠前的那一条。

所以当你发现“同一个产品为什么在两个仓表现不一样”,往往不是产品字段变了,而是:

  • warehouse route 不同
  • destination location 不同
  • sequence 排序不同
  • 销售行显式 route 抢了优先级

route 决定“可走哪些路”,rule 决定“这一步到底干嘛”

当 Odoo 找到匹配 rule 后,接下来不是统一动作,而是看 action

最常见的分支是这些:

  • pull
  • pull_push
  • buy
  • manufacture
  • push

这几个分支,决定了同一个需求最后会变成完全不同的业务对象。


_run_pull():最常见、也最容易被低估的路径

对很多库存链路来说,真正的主干不是采购也不是制造,而是 pull。

_run_pull() 的核心动作可以概括成三步:

  1. 校验 rule 上是否有 location_src_id
  2. 根据 procurement + rule 生成 stock.move values
  3. 按公司批量 sudo().create() move,然后执行 _action_confirm()

这里有几个细节非常重要。

1. move 是用 rule 反推出来的

_get_stock_move_values() 会把 rule 里的这些东西灌进 move:

  • location_idlocation_src_id
  • location_dest_id / location_final_id
  • picking_type_id
  • rule_id
  • warehouse_id
  • procure_method
  • route_ids
  • origin
  • reference_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_stock
  • make_to_order
  • mts_else_mto

可以把它理解成:

make_to_stock

先把需求当成“从当前来源库存满足”。

它更偏库存池思维。

make_to_order

不优先吃现货,而是继续向上游再触发一层规则。

它更偏需求驱动思维。

mts_else_mto

能吃库存先吃库存,不够的那部分再继续触发上游规则。

它最贴近很多企业真实需求,所以也最容易让调试复杂化。

因为这意味着:

  • 一部分需求被现货满足
  • 另一部分需求继续派生补货链

如果你只盯最终单据,很容易看不出它是“同一条 rule 下的混合行为”。


buy:为什么有些需求最后会生成采购单

当命中的 rule action 是 buy 时,逻辑会转到 purchase_stockstock.rule 的扩展。

这一步不是简单“立刻新建一张 PO”,而是要继续做几件事:

  • 找匹配供应商
  • 计算采购分组 domain
  • 查已有采购单是否可复用
  • 合并 procurement
  • 决定更新已有 PO line 还是新建 line

源码里还有个很值得注意的点:

当 procurement 带有 buy 路线时,系统会把仓库 reception route 也并入上下文。

这说明一件事:

  • buy 不是凭空生成采购单
  • 它仍然要和仓库的收货路线、后续入库动作协同

所以采购不是 route / rule 系统之外的另一套宇宙,而是它的延伸。


manufacture:为什么同样是需求,有时会长成 MO

当命中 manufacture action 时,逻辑会转入 mrpstock.rule 的扩展。

这一层最核心的事情有三件:

  1. 找可用 BOM
  2. 决定是复用已有草稿 / 确认中的 MO,还是新建
  3. 准备并创建制造单,再按条件自动 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
  • 命中 buy rule
  • 最终生成 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() 的继续派生。


真正高效的调试顺序

如果你要排查“为什么这次走了这条库存链”,我建议按这个顺序看:

  1. 需求从哪里来 - 销售行、补货规则、手工 move,还是制造需求

  2. procurement values 里带了什么上下文 - route_idswarehouse_idlocation_final_iddate_planned

  3. _get_rule() 最终命中了哪条 rule - 重点看 destination、warehouse、sequence

  4. 这条 rule 的 action 是什么 - pull、buy、manufacture、push

  5. 这条 rule 的 procure_method 是什么 - MTS、MTO、MTS else MTO

  6. 生成了什么对象 - move、picking、PO、MO

  7. 这些对象 confirm 后有没有继续派生下一跳

只要按这条线查,绝大多数“路线和规则看不懂”的问题都会变得可解释。


一句话记忆法

把这套机制记成一句话:

route 决定“允许走哪些履约方向”,rule 决定“当前这一步到底执行什么动作”,而真正让链路不断长出来的,是 rule 命中后的 move / procurement confirm 传播。

理解这一句之后,你再看 Odoo 的库存路线、补货、采购、制造,就不再像四套分裂系统,而会更像一台统一的供应链决策引擎。

DISCUSSION

评论区

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