销售履约调度链

销售确认后为什么不是直接建发货单:procurement values、stock.rule.run 和 picking 再确认到底怎么串?

很多人知道销售单确认后会触发库存,但容易把这件事理解成“sale 直接生成 picking”。结合 sale_stock 与 stock 源码看,真实主线更像是:订单行先组织 procurement values,再交给 stock.rule.run 分流,最后由 move/picking 链路继续推进。

Odoo 开发 库存 销售
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

很多人第一次看 Odoo 销售履约,脑子里都会自动补成一句话:

销售单确认后,系统生成发货单。

这句话作为业务层口语没有问题,但如果你在做开发、排错或二开,它就太粗了。

因为从 /home/ubuntu/odoo-temp/addons/sale_stock/models/sale_order.pysale_order_line.pyaddons/stock/models/stock_rule.py 的真实链路看,Odoo 的思路并不是:

  • 销售模块自己直接把 picking 一把建完;

而是:

  1. 销售单确认后,订单行先判断还有多少数量需要履约;
  2. 订单行准备一包 procurement values
  3. 把需求交给 stock.rule.run()
  4. 再由规则系统决定是 pull、buy 还是 manufacture;
  5. 如果走 pull,就创建 stock move,再由 move / picking 确认链继续推进。

换句话说,销售模块更像“发起需求”,库存规则系统才是“决定如何履约的人”。

一、入口其实非常短:sale.order._action_confirm()

sale_order.py 里的关键入口只有一行:

self.order_line._action_launch_stock_rule()

这行代码短得几乎容易被忽略,但它其实非常说明设计哲学。

它告诉你:

  • 销售单头部不是最终执行者;
  • 真正被送进履约系统的是订单行

为什么是订单行,而不是订单头?

因为决定履约路径的关键上下文都在行上:

  • 产品
  • 数量
  • 单位
  • 路线
  • 仓库
  • 收货地址
  • 这条行已经被履约了多少

如果把这些决策都塞到订单头,扩展能力会很差。

二、真正的需求发射器:sale.order.line._action_launch_stock_rule()

这一步才是整条链真正启动的地方。

源码里它先做几件很务实的事:

1)跳过不该处理的行

比如:

  • 不是 sale 状态
  • 订单已锁定
  • 产品类型不参与这条库存履约逻辑

这说明系统不是“确认单据就全量推库存”,而是有明确过滤条件。

2)先算“还有多少没被履约”

_get_qty_procurement() 会先看已有的 outgoing / incoming moves,算出这条销售行已经被推进了多少。

然后真正待发射的数量是:

product_uom_qty - qty

这一步非常关键,因为它解释了为什么 Odoo 在修改销售数量、部分履约、退货后,后续动作不会简单粗暴地整单重建。

系统先看现实,再发新需求。

三、procurement values 才是后续规则判断的“上下文包”

很多人知道 _action_launch_stock_rule() 会调库存规则,但没认真看 _prepare_procurement_values()

其实这一步非常重要,因为它准备的不是装饰性数据,而是后续规则系统做判断时真正依赖的上下文。

源码里典型会塞进去这些值:

  • origin
  • sale_line_id
  • date_planned
  • date_deadline
  • route_ids
  • warehouse_id
  • partner_id
  • location_final_id
  • company_id
  • sequence
  • packaging_uom_id

你可以把这包 values 理解成一句完整的业务指令:

这条销售需求来自谁、要什么时候交、往哪交、按什么路线走、属于哪个公司、对应哪条销售行,请你根据规则接着处理。

这也是 Odoo 可扩展性的关键原因之一。

如果将来你要做:

  • 特殊路由
  • 多公司差异
  • 特定仓库策略
  • 某些自定义分单/分仓逻辑

很多扩展点都不是去改销售确认入口,而是去补这包 values 或影响 rule 选择逻辑。

四、为什么销售模块不直接“自己建 picking”

因为 Odoo 要解决的不是“生成一张发货单”这么单一的问题,而是“如何满足这条需求”。

到了 stock.rule.run(),源码会做两件事:

1)给 procurement 找 rule

系统会根据:

  • 产品路线
  • 仓库路线
  • 包装路线
  • 目标位置
  • 公司上下文

去找最合适的 rule。

2)按 action 分流

如果找到了 rule,Odoo 并不是统一走一条固定逻辑,而是根据 rule 的 action 决定后续路径:

  • pull
  • buy
  • manufacture
  • 以及 pull_push 映射后的 pull 处理

这就是为什么“销售确认后会生成什么”这个问题,不能脱离路线和规则配置单独回答。

对某些产品,结果可能是:

  • 直接生成库存 move
  • 触发采购
  • 触发制造
  • 串出多段 move 链

销售模块如果直接硬编码“创建 picking”,就会把这些灵活性全部抹掉。

五、如果命中 pull,真正创建的是 stock.move

stock_rule.py 里的 _run_pull() 很直接:

  1. 先检查 rule 的来源位置是否完整;
  2. _get_stock_move_values() 组装 move values;
  3. 按公司批量创建 stock.move
  4. 对新建 move 调 moves._action_confirm()

这一步很能说明层次关系:

pull 规则首先关心的是“生成库存移动”,不是先关心 UI 上那张 picking 单据长什么样。

对 Odoo 来说,move 才是库存执行语义里的基础对象;picking 更像对一组 move 的业务承载和操作容器。

六、_get_stock_move_values() 解释了 move 是怎么“带着业务上下文出生”的

这一步不是简单拷几个字段。

从源码看,它会综合考虑:

  • date_planned / date_deadline
  • partner
  • warehouse_id
  • procure_method
  • move_dest_ids
  • 以及从 procurement values 往下传的各种附加信息

所以库存 move 从一开始就不是孤零零的一条物料动作,而是:

  • 知道自己从哪来
  • 知道要去哪里
  • 知道属于哪个业务来源
  • 知道后面是否还挂着链式目的 move

这也解释了为什么很多履约问题最终要追到 move,而不是只停留在 sale order 或 picking 界面。

七、为什么创建 move 后还要再确认 picking

sale_order_line.py 最后还有一段经常被忽略的代码:

  • 收集订单相关 pickings
  • 过滤掉 cancel / done
  • 再执行 pickings_to_confirm.action_confirm()

源码注释也写得很坦白:

当前调度器触发点还依赖 picking confirmation,而不只是 stock.move confirmation。

这句话非常关键。

它说明现实系统里,链路不是“move 出来就万事大吉”,而是:

  • 需求先进入规则系统
  • 规则产出 move
  • 然后某些后续调度还要靠 picking 的确认链来继续接力

所以如果你排查问题时只盯着“move 已经创建了”,但没看 picking 是否进入正确状态,很容易误判为“库存没继续跑”。

八、开发和实施时最容易踩的坑

坑 1:把问题全怪到 sale 模块

很多“销售确认后没出单”的问题,根因其实在:

  • route 配置
  • warehouse 配置
  • rule 没命中
  • procurement values 缺上下文

而不是 sale 本身没执行。

坑 2:以为 procurement 是最终产物

procurement 只是需求表达,不是最终单据。

真正的后续对象要看 rule 决定。

坑 3:忽略“已履约数量”的扣减逻辑

如果已有 move、部分交付、退货、补单混在一起,不先看 _get_qty_procurement(),你很容易误判系统为什么没有再建新 move。

坑 4:只盯 picking,不追 move 和 rule

picking 是结果展示层之一,rule 和 move 才是这条链的骨架。

九、排查销售履约问题的推荐顺序

遇到“确认销售单后库存动作不对”,我建议按这个顺序查:

  1. 订单行是不是已经进入 sale 状态
  2. _get_qty_procurement() 结果是不是说明其实没有新增需求
  3. _prepare_procurement_values() 是否带上了正确 route / warehouse / partner / deadline
  4. stock.rule.run() 命中了哪条 rule
  5. 对应 action 是 pull、buy 还是 manufacture
  6. 如果走 pull,stock.move 是否成功创建并确认
  7. 相关 picking 是否又被正确 confirm,后续调度是否接上。

照这个顺序看,你会比“为什么没发货单”这种问法更接近真实问题。

总结

Odoo 销售确认后的库存链路,真正的重点不是“sale 直接建 picking”,而是:

  • 订单行先计算还需要履约多少
  • 再构造 procurement values 作为规则系统的上下文输入
  • stock.rule.run() 决定到底走 pull、buy 还是 manufacture
  • 如果走 pull,就先生成并确认 stock move
  • 最后再通过 picking confirmation 把后续调度接起来。

理解这条主线后,你会发现很多销售履约问题其实不是“销售没工作”,而是需求已经被发射出去了,只是后面的规则链没有按你以为的方式落地。

DISCUSSION

评论区

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