先说结论
Odoo 的销售确认,并不是“点一下按钮就直接生成发货单”。
更准确地说,它先把销售需求交给 procurement,再由库存规则决定下一步是:
- 直接拉出库动作
- 生成采购需求
- 触发制造需求
这就是为什么同样是“确认销售单”,不同产品、不同路线、不同仓库下,结果会完全不一样。
入口:sale.order._action_confirm()
在 sale_stock/models/sale_order.py 里,确认逻辑最醒目的代码只有一行:
self.order_line._action_launch_stock_rule()
这句话的意思很清楚:
- 订单头负责把业务状态推进到“已确认”
- 订单行负责把“这份需求怎么履行”传给库存规则
也就是说,真正决定后续路径的不是销售单头,而是每一条明细行的产品、路线、仓库、交货位置和数量。
真正的起点:sale.order.line._action_launch_stock_rule()
这一步是整条链路的核心。
代码会先过滤掉不该进入库存链路的行,比如:
- 状态还不是
sale - 订单被锁定
- 已经处理过同样数量,不需要重复跑
- 还有上下文里显式要求跳过 procurement 的情况
然后它会计算“还差多少没被履约”:
qty = line._get_qty_procurement(previous_product_uom_qty)
if float_compare(qty, line.product_uom_qty, precision_digits=precision) == 0:
continue
这段逻辑很重要,因为 Odoo 不是每次都重新造一套单据,而是先看:
这条销售行,是不是已经有足够的下游动作覆盖了?
如果已经覆盖,就不再重复创建。
procurement 里装了什么
接下来,销售行会准备 procurement values,再组装成 stock.rule.Procurement:
procurements += line._create_procurements(product_qty, procurement_uom, values)
你可以把 procurement 理解成一个“需求包”。
它不直接告诉系统“去建哪个对象”,而是带着这些信息:
- 产品是什么
- 要多少
- 用什么计量单位
- 送到哪里
- 来源单据是谁
- 关联哪些参考对象
这就是 Odoo 设计得很聪明的地方:
销售只描述需求,不决定实现方式。
谁来决定“发货、采购还是制造”
最后是 stock.rule.run(procurements)。
这一步才真正把需求分流成不同执行路径:
_run_pull:走拉式库存链_run_buy:走采购链_run_manufacture:走制造链
所以你看到的“出库单”“采购单”“生产单”,不是销售模块自己硬编码出来的,而是规则引擎根据 route、warehouse、product 和 company 决定的。
最容易误解的三个点
1)确认销售单,不等于创建发货单
对某些产品,它可能只是在系统里留下一个需求痕迹,后面再由库存规则生成真正的执行对象。
2)订单头不是唯一入口
真正参与链路判断的,是订单行级别的数据。只改订单头,往往不够。
3)销售确认后的结果不是单一的
同一套代码,可能产出:
- delivery picking
- RFQ / PO
- manufacturing order
这取决于路线,而不是“销售模块默认总是发货”。
实战里怎么改最稳
如果你要扩展这条链路,优先考虑这几个点:
- 先改 procurement values,再让规则引擎接手
- 需要自定义履约逻辑时,优先看 route / stock.rule
- 调试时记得看
stock.reference,它会帮你把销售和下游动作串起来 - 不要轻易在
sale.order._action_confirm()里硬塞创建库存单据的代码
真正稳的做法,是让销售继续负责需求表达,让库存规则负责执行决策。
一句话总结
Odoo 的销售确认不是“造单”,而是“发起需求”。
sale.order 把业务确认下来,sale.order.line 把需求包装成 procurement,stock.rule 再把需求分流成发货、采购或制造。
DISCUSSION
评论区