很多人第一次理解 Odoo route 时,会把 Push Rule 和 Pull Rule 都看成“库存移动规则”,然后觉得它们只是触发时机不同:
- Push:货到了就往后推;
- Pull:缺货了就从前面拉。
这个理解不算错,但还不够。
如果你真正看 /home/ubuntu/odoo-temp/addons/stock/models/stock_rule.py,会发现 Pull Rule 最核心的思维不是“货现在在哪”,而是:
哪个目标库位出现了需求,系统就围绕这个目标库位去寻找一条能把货拉过来的补货规则。
这也是为什么它总给人一种强烈的“倒着长单据”感:
- 先看到下游要货;
- 然后上游 move、采购、制造动作才被补出来。
一、Pull Rule 不是在“扫描现有单据”,而是在处理 procurement
run() 的入口非常重要。
它处理的不是某张现成调拨单,而是一组 procurement,也就是:
- 某产品;
- 某数量;
- 某目标 location;
- 某计划日期;
- 某上下文 values。
也就是说,系统先拿到的是一句抽象需求:
- “这个产品,需要在这个库位、这个时间点可用。”
然后才决定:
- 是从库存拿;
- 还是触发上游规则;
- 还是根本找不到规则直接报错。
所以 Pull Rule 的视角天然是需求驱动,不是“已有物流单据驱动”。
二、为什么 _get_rule() 是按目标库位往上找,不是按来源库位往下找
很多人脑中的查找方式是:
- 货应该从哪里来?
- 那我去那个来源库位找规则。
但 _get_rule(product_id, location_id, values) 做的恰恰相反。
它拿到的是当前需求所在的 location_id,然后:
- 从这个目标库位开始;
- 沿 location 父层级一路往上;
- 结合 route、packaging、product routes、warehouse routes;
- 在这些候选里选最合适的一条 rule。
这非常关键。
因为 Pull Rule 表达的是:
“如果某个目标位置需要货,应该用哪条制度把货补过来?”
所以优先锚定的是目标位置,不是来源位置。
这也是很多调试误判的根源:
- 你一直盯来源库位配没配规则;
- 但系统实际是在目标库位这一侧找“谁负责满足这里的需求”。
三、规则选择并不是“找到一条就算”,而是 route、warehouse、location 三层一起决定
_search_rule_for_warehouses() 和 _search_rule() 展示了 Odoo 选 rule 的真实复杂度。
候选来源通常包括:
- procurement 自己带进来的
route_ids - 包装类型带的 route
- 产品和品类上的 route
- warehouse 上的 route
同时还会按:
location_dest_idwarehouse_idroute_id
做分组和排序,顺序上又受:
route_sequencerule.sequence
影响。
翻成人话就是:
Pull Rule 不是“产品打了哪个 route 就永远按那个走”,而是在“目标库位 + 仓库 + route 来源 + 排序优先级”这几个维度上共同决策。
所以现场出现“同产品在不同仓、不同位置、不同补货上下文下走法不同”,其实很正常。
四、_run_pull() 为什么总让人觉得“上一张 move 是系统补出来的”
_run_pull() 的动作很直接:
- 先校验 rule 是否有
location_src_id; - 再按 procurement + rule 生成 move values;
- 以 sudo 创建
stock.move; - 最后调用
moves._action_confirm()。
这套动作看起来平平无奇,但恰恰是 Pull Rule 那种“倒着长链”的来源。
因为下游只是在说:
- 我要在目标位置有货。
然后 _run_pull() 就根据 rule 反推出:
- 那应该从哪个来源位置建一张 move,把货往这里拉。
于是用户在界面上会感受到:
- 明明只是销售单确认了;
- 或者补货建议点了确认;
- 怎么突然多了一张仓间调拨,甚至再往上又多了采购 / 制造。
其实不是“凭空生成”,而是需求经由 rule 被逐层物化成 move。
五、_get_stock_move_values() 才是 Pull Rule 把抽象需求落地成物流动作的地方
这个方法里最值得抓的几个字段是:
location_id = self.location_src_id.idlocation_final_id = location_dest_id.idmove_dest_idsprocure_methodpicking_type_iddate/date_deadlineprocurement_values
这说明 Pull Rule 不是只说“从 A 到 B”,它还在同时确定:
- 这段 move 属于哪种 operation type;
- 它和下游 move 怎么串(
move_dest_ids); - 计划时间怎么倒推出去;
- 后续还需要哪些上下文继续传递。
特别是 move_dest_ids 很关键。
它让上游 move 和下游需求形成明确连接。
所以 Pull Rule 的核心不只是建一张上游单,而是:
把“我为什么要建这张上游单”也一起编码进链路里。
这就是为什么很多 traceability、smart button、依赖取消传播能顺着需求链继续工作。
六、为什么 date 会往前倒推
_get_stock_move_values() 里会把 values['date_planned'] 再减去 rule 的 delay,得到 move 的计划日期。
这说明 Pull Rule 不只是决定“从哪拉”,还决定“要提前多久开始拉”。
所以实施里很常见的一种错觉是:
- 我明明要 3 月 30 日交货,怎么仓库动作被排到更早?
因为 Pull Rule 的视角是:
- 目标位置在 3 月 30 日必须有货;
- 那来源位置的动作就得提前若干天发生。
这不是日期错了,而是 supply chain 倒排本来就该这样。
七、procure_method 为什么会影响 Pull Rule 是“继续往上找”还是“先吃库存”
虽然 _run_pull() 默认会把 mts_else_mto 先转成 make_to_stock 进入 move,但整体补货语义仍由 procure_method 控制:
make_to_stock:先从来源库存满足;make_to_order:忽略现有可用量,继续向上触发规则;mts_else_mto:能吃库存就吃,不够再往上补。
这就是为什么两条看起来很像的 route,最终补货体感完全不同:
- 一条会优先消耗源库现货;
- 另一条会继续长出采购或制造;
- 还有一条是“不够的部分才继续往上追”。
所以 Pull Rule 不是单独运作的,它和 procure_method 一起决定了“需求回拉的深度”。
八、为什么创建 move 时要 sudo()
_run_pull() 里创建 move 明确用了 sudo().with_company(company_id).create(...)。
注释已经点明:
- 当前触发采购链的人,不一定有库存对象创建权限;
- 比如销售动作触发了 MTO 补货。
这意味着 Pull Rule 其实承担的是一种系统级补货职责,而不是单个业务用户手工发起的普通库存操作。
所以很多时候你不能用“当前用户在库存里有没有这个权限”来理解它为什么还能继续长单。
它表达的是:
- 这个业务动作有权触发补货制度;
- 具体补货文档由系统以更高权限生成。
九、最容易踩坑的几个调试顺序
1)报“找不到规则”时,先看目标库位和 route,不要先看来源库位
因为 _get_rule() 是围绕目标位置找“谁来满足这里的需求”。
2)出现“上一张 move 怎么自己长出来了”时,先看 move_dest_ids 和 procurement 来源
多数情况下不是凭空长,而是下游需求把上游动作拉出来了。
3)日期看起来提前了,不一定是 bug,先看 rule.delay
Pull Rule 天然会为目标可用时间做倒排。
4)为什么有时继续触发采购,有时只从源库拿货,先看 procure_method
不要把所有 Pull Rule 都当成纯 MTO。
5)为什么不同仓同产品表现不一致,先看 warehouse routes 与 route sequence
规则选择不是只看产品。
总结
Pull Rule 最值得记住的一句话是:
它不是“货从哪里发出”的说明书,而是“某个目标位置一旦缺货,系统该怎样沿着规则把上游补货动作倒着长出来”的制度。
所以它看起来总像“上一张单据凭空出现”,本质上是因为你在界面上先看到了需求端,而 Odoo 在后台又把满足这个需求所需的上游动作补齐了。
把这个逻辑想明白之后,很多库存疑难都会顺:
- 为什么仓间调拨自己冒出来;
- 为什么采购 / 制造会被继续触发;
- 为什么计划日期总往前;
- 为什么 route 明明配了却没命中;
- 为什么调试时应该从目标库位往回查,而不是从来源库位往前猜。
这才是 Pull Rule 真正强大的地方:
它让 Odoo 不只是“记录物流动作”,而是能把需求、规则、时间和上游补货链串成一条可执行的制度链。
DISCUSSION
评论区