先说结论
一张销售单点“确认”之后,Odoo 不是简单地“生成一个出库单”就完事了。
更准确地说,它会做三层事情:
- 销售层确认业务承诺
- 库存规则层决定该怎么 fulfil 这份需求
- 执行层生成 move / picking / 采购单 / 制造需求
所以你看到的“发货单出来了”“系统去采购了”“系统去拉下游库位了”,本质上都不是销售模块自己硬编码完成的,而是通过 procurement + stock.rule 这套调度机制分发出去的。
入口 1:sale.order._action_confirm()
在 sale_stock/models/sale_order.py 里,销售单确认时最关键的一句是:
self.order_line._action_launch_stock_rule()
也就是说:
- 销售单确认
- 不直接一股脑创建所有库存对象
- 而是交给每条销售行自己去“发起库存规则”
这个设计很重要,因为真正决定后续动作的,不是订单头,而是每一条产品行的产品类型、路线、仓库、数量、公司、交货位置。
入口 2:sale.order.line._action_launch_stock_rule()
这一步是整条链路的真正起点。
源码里的注释写得很直白:它会根据销售行的规则去触发:
_run_pull_run_buy_run_manufacture
也就是拉式补货、采购补货、制造补货。
这一步先做了哪些过滤
不是所有销售行都会触发库存逻辑。源码里先排掉这些情况:
- 不是
sale状态 - 订单已锁定
- 产品类型不需要库存链路
- 已经做过相同数量的 procurement,不必重复跑
这意味着销售模块并不会盲目重复造 move。
它会先判断:
这条销售行还有没有“未被 fulfil 的差额数量”?
只有差额存在,才继续往下。
销售行在这一步准备了什么上下文
在真正调用 stock.rule 之前,销售行会准备一份 procurement values。
里面通常包含:
- 计划日期
- 目的地位置
- 仓库
- 路线
- 客户 / 收货地址相关信息
- 原始单据引用
- 下游 move 关联线索
你可以把它理解成一张“任务说明书”:
我现在有一个来自销售的需求,请系统根据规则决定怎么满足它。
这里有个很容易忽略但很关键的点:
- 销售行并不关心“最后一定是发货单还是采购单”
- 它只负责把需求包装成 procurement
- 真正的动作决策交给
stock.rule.run()
这就是 Odoo 这条链路可扩展的根源。
stock.rule.run():分发器,而不是执行器本体
当销售行把 procurements 收集完之后,会调用:
self.env['stock.rule'].run(procurements)
这一层最像一个调度中心。
它会根据命中的 route / rule 决定:
- 是直接从上游库位拉货?
- 还是需要采购?
- 还是应该制造?
- 还是应该做 push / pull 链条中的某一步?
所以很多人调试销售出库问题时,盯着 sale 模块看半天,其实只看到了 30%。
另外 70% 往往在:
stock.rulestock.movepurchase_stockmrp
_run_pull():最常见的“生成库存 move”路径
如果命中的规则是 pull,那么 Odoo 会走到 stock.rule._run_pull()。
这一步的核心动作非常清晰:
- 校验 source location 是否存在
- 为每个 procurement 计算 move values
- 按公司分组批量创建
stock.move - 对新 move 执行
_action_confirm()
源码里还有一个非常值得注意的细节:
moves = self.env['stock.move'].sudo().with_company(company_id).create(moves_values)
也就是说,Odoo 在这里是 sudo 创建 move 的。
为什么?
因为业务触发人可能是销售员,但销售员不一定具备完整库存对象创建权限。如果不提权,很多“销售触发库存”场景会因为权限而断链。
这就是 Odoo 的典型思路:
- 前台用户在业务对象上有权限
- 底层履约对象由系统在安全边界内代为推进
move 创建后为什么还要 _action_confirm()
很多人以为创建 stock.move 之后事情就结束了,其实远没有。
stock.move._action_confirm() 才是下一层真正把链路继续向前推进的地方。
这一步会做几件关键事:
- 把 move 从 draft 变成 confirmed / waiting
- 对
make_to_ordermove 再次生成 procurement 请求 - 需要时继续触发下游规则
- 按 picking 维度分组归并
- 对符合条件的 move 尝试自动 assign
换句话说:
销售行触发的是第一跳 procurement,move confirm 则负责把这条供应链继续往前传。
这也是为什么复杂路线看起来像“自己会长出来”——因为每一步 confirm 都可能继续派生下一步。
_run_buy():什么时候会走采购
如果产品路线命中 Buy,或者仓库补货规则最终落到采购,stock.rule.run() 会转去采购扩展模块的逻辑。
理解上不用把它想复杂:
- 销售先提出需求
- 规则发现本仓不该自己供货
- 那就把需求转成采购需求
- 采购模块再负责合并 / 选供应商 / 生成 PO
所以销售并不是“直接建采购单”,而是:
销售需求 → procurement → buy rule → purchase.order line / RFQ
这个分层非常利于扩展:
- 你可以换供应商选择逻辑
- 可以加 MOQ、采购提前期
- 可以按公司、仓库、路线、供应商策略做定制
而无需改销售确认入口。
销售确认后为什么还会去 picking.action_confirm()
在 sale.order.line._action_launch_stock_rule() 末尾,Odoo 还会把订单相关的未完成 picking 拿出来,执行:
pickings_to_confirm.action_confirm()
源码注释已经说明原因:
当前 scheduler trigger 是在 picking confirm 上触发的,而不是 stock.move confirm。
这句话很值钱。
它告诉你一件事实:
- 销售跑完 procurement 后,系统还会补一脚 picking confirm
- 目的是让调度、可用量检查、后续 reservation 流程接上
所以如果你看到“销售确认了但 picking 状态不对”,不要只查 sale order line,也要查 picking confirm 有没有被正常跑到。
用业务语言重新翻译整条链
如果不用源码术语,这条链可以翻译成:
- 客户下单,销售确认承诺
- 每条产品行问系统:“这件货该怎么 fulfil?”
- 规则引擎查看仓库路线
- 能内部拉货就生成库存 move
- 需要外采就转成采购需求
- 需要生产就转成制造需求
- move / picking 在确认后继续触发下一跳
- 最终形成可执行的发货、收货、调拨、采购或制造动作
这也是 Odoo 强的地方:
它不是写死“销售 -> 出库”,而是让路线系统决定“销售 -> 什么动作组合”。
实战调试时最该看的 6 个点
如果你遇到“销售单确认后没出库”“没采购”“路线怪怪的”,优先看这几个点:
1. 产品类型和发票/库存属性
产品本身是否应该进入库存履约链。
2. 销售行路线 / 仓库 / 公司
很多异常不是代码 bug,而是上下文命中了错误规则。
3. sale.order.line._action_launch_stock_rule() 是否真正执行
比如是否被 skip_procurement、状态过滤或数量差额判断提前跳过。
4. stock.rule.run() 命中了哪条 rule
这是分叉口。
5. stock.move._action_confirm() 后 move 进了什么状态
是 confirmed、waiting,还是继续生成了新的 procurement。
6. picking confirm / assign 有没有继续推进
很多“单生成了但走不动”的问题卡在这里。
一句话记忆法
把整条链记成一句话:
销售模块提出需求,库存规则决定路径,move / picking / purchase / mrp 负责把路径执行出来。
理解这一句之后,你再看 Odoo 销售、库存、采购、制造之间的关系,脑子会清楚很多。
DISCUSSION
评论区