很多人看 Odoo 销售流时,会下意识把它想成一条直线:
- 销售单确认;
- 系统生成出库;
- 然后顺手生成发票。
但源码视角下,这个理解不够准确。
更真实的说法是:
销售确认后,Odoo 会把业务拆成两条彼此相关、但不完全同步的链。
- 履约链:去解决“货怎么交付”
- 开票链:去解决“现在能不能开票、该开多少”
这也是为什么你经常会看到这些“看起来矛盾,实际上正常”的状态:
- 订单已确认,但还不能开票
- 已有 picking,但 invoice_status 还是
no - 已经开票了,但货还没出完
不是系统乱,而是 Odoo 故意把“物流事实”和“财务确认”拆开管理。
一、action_confirm() 真正做的是把订单推进到可执行状态
在 addons/sale/models/sale_order.py 里,action_confirm() 先做三件核心事:
- 校验订单是否允许确认
write()把订单状态推进到sale- 调
_action_confirm(),让后续扩展模块去生成下游单据
这一步很关键。
它并不是“直接创建所有后续业务单”,而是:
先把销售订单从报价,升级为一个已经承诺要履行的业务对象。
所以确认之后,订单本身进入执行态;但执行态之后到底要不要建库存单、怎么建、何时能开票,要交给后续链路分别决定。
二、库存履约链真正从 _action_launch_stock_rule() 开始发力
很多人以为 sale order 一确认,系统直接“建一张出库单”。
源码里其实更抽象。
在 sale_stock/models/sale_order_line.py 里,真正重要的是:
- 每条销售行先判断自己是否需要走库存履约
- 再通过
_action_launch_stock_rule()生成 procurement - 最后交给
stock.rule.run()决定后面走 pull / buy / manufacture 哪条分支
也就是说,销售行并不是直接说“给我一张 picking”。它更像是在说:
这里有一笔需求,请库存规则系统决定如何满足。
这也是 Odoo 设计上很漂亮的一点:
- 销售负责提出需求
- 库存规则负责决定 fulfilment strategy
所以销售确认后,下游可能不只是普通出库,还可能是:
- 仓内调拨
- 采购补货
- 制造单
- dropship
你如果把这条链看成“确认销售 = 创建出库单”,就会在复杂路线下很快看不懂。
三、为什么确认后会看到 picking,但不代表已经能开票
在 _action_launch_stock_rule() 之后,stock.rule.run() 会驱动 move / picking 逐步落地。
于是前台最容易看到的现象是:
- 销售单上出现了交货 smart button
- 下游有 picking 了
- 甚至 picking 已经 assigned 了
这时很多新手会自然推断:
既然都开始发货了,那应该也能开票了吧?
不一定。
因为库存履约链解决的是“货有没有被安排和交付”,而开票链盯的是另一组字段,尤其是:
qty_deliveredqty_invoicedqty_to_invoiceinvoice_status
如果产品开票策略是按已交付数量,那么 delivery 的推进当然会影响能否开票;
但如果还没真正 done,或者交付数量还没进入可开票条件,哪怕你已经看到 picking,也可能依然不能开票。
四、开票链不是从 picking 出发,而是从“哪些行现在可开”出发
在 sale/models/sale_order.py 里,_create_invoices() 的核心思路不是“只要订单确认就开票”,而是:
- 先找出 invoiceable lines
- 再把这些销售行转换成
account.move.line的 vals - 最后创建
account.move
这一步的重心是“可开票性”,而不是“有没有库存单”。
而销售行如何变成发票行,关键又落到 sale_order_line._prepare_invoice_line()。
这意味着:
- 库存链关注的是 move / picking / rule
- 开票链关注的是 sale order line 当前的 invoiceable 数量
两条链会互相影响,但不会互相替代。
五、为什么 Odoo 要故意拆成两条链
因为业务世界里,“交付”和“开票”本来就不总是同一步。
最常见的几种情况:
1)先确认,后发货,再开票
这是很多实物商品场景。
2)先确认就能开票
如果产品按订购数量开票,系统并不要求先等交货完成。
3)已经有交货动作,但暂时还不能开票
比如部分交货、异常退货、数量还没汇总到可开票条件。
4)已经开了票,但下游物流还在继续
在预付款、阶段性交付、按订购数量开票等场景里都很常见。
所以从架构上讲,Odoo 不是偷懒没把它们写成一条线,而是:
它承认销售履约和财务确认不是同一个事实。
六、排错时应该先问“卡在哪条链上”
实际排错时,很多人一上来就在销售单里乱点,效率很低。
更稳的顺序是先分链。
情况 A:问题像是“为什么没有出库 / 出库不对”
优先看履约链:
action_confirm()后订单状态是否进入sale- 相关销售行是不是 storable / consu,是否满足走库存规则
_action_launch_stock_rule()有没有真正跑起来- route / rule 是否把需求导向了正确分支
- picking / move 当前是什么状态
情况 B:问题像是“为什么不能开票 / 开票数量不对”
优先看开票链:
- 该行产品的 invoicing policy 是什么
qty_delivered、qty_invoiced、qty_to_invoice分别是多少_get_invoiceable_lines()有没有把这行纳入_prepare_invoice_line()生成的 line vals 是否符合预期
情况 C:用户说“明明发货了还不能开票”
先不要怀疑按钮坏了。
先判断:
- 是不是按 delivered quantities 开票
- picking 是否真的
done qty_delivered是否正确回写- 是否只是部分交货
很多“开票失效”问题,根子其实在交付事实还没成立。
七、最容易误解的点:看到 picking,不代表履约已经完成
这是实施现场特别常见的错觉。
很多人把这些状态混成一个词:
- 有 picking
- picking assigned
- picking done
- delivered qty 已更新
- qty_to_invoice 已变成正数
实际上,这五件事不是一个层级。
最粗暴但很实用的理解是:
- 有 picking:系统已经开始安排履约
- picking assigned:库存资源已尝试分配
- picking done:物流事实被确认
- qty_delivered 更新:销售层开始承认已交付数量
- qty_to_invoice > 0:财务层开始承认可以开票
每一层都比前一层更接近“能收钱”。
八、实战开发时该注意什么
1)不要在自定义里把库存链和开票链硬绑死
比如一确认销售就强制建发票,往往会破坏按已交付数量开票的语义。
2)扩展确认逻辑时,优先挂在 _action_confirm() 周边
因为 action_confirm() 是入口,真正给模块扩展后续行为的通常是 _action_confirm() 语义。
3)排查数量问题时,不要只看订单头
头部状态大多只是聚合结果,根因常在行上:
- sale.order.line
- stock.move
- picking state
- qty_delivered / qty_to_invoice
4)别把“生成 picking”误当成“路线已经正确”
生成了 picking 只是说明有下游动作,不代表路线、仓库、来源位置、供应策略全都对。
最后总结
理解 Odoo 销售确认后的行为,最重要的不是记住多少个方法名,而是建立一个正确心智模型:
确认销售后,系统会同时启动“履约链”和“开票链”;它们互相关联,但不是一条线。
所以以后再看到“已确认但未开票”“已有出库但状态别扭”“开票和交付不同步”时,不要急着说系统有 bug。
先问自己一句:
这次卡住的,到底是库存履约链,还是销售开票链?
这一步分清了,后面排错会快很多。
DISCUSSION
评论区