很多人第一次读销售单源码时,都会默认以为:
“销售单确认 = 出库 + 开票 + 所有后续动作一起做完。”
Odoo 不是这么设计的。
从源码看,销售确认更像是把订单推进到一个“可履约”的状态,然后把后续工作拆成两条不同的链:
- 补货 / 出库链:负责把商品真正送出去;
- 开票链:负责根据开票政策把可开票数量变成发票。
这两条链经常同时存在,但它们并不是同一条逻辑。
一、action_confirm() 只负责把订单推进到“已销售”阶段
在 addons/sale/models/sale_order.py 里,action_confirm() 的职责很明确:
- 先检查订单是否允许确认;
- 再写入确认值,把 state 推进到
sale; - 然后调用
_action_confirm()这个扩展钩子; - 最后按条件锁单、发确认邮件。
也就是说,核心销售模块本身并不在这里把所有下游文档一次性做完。 它只是把订单状态推进到“可以继续处理”的阶段。
二、真正的履约动作,很多都挂在 sale_stock
在 sale_stock 里,_action_confirm() 会继续调用订单行的 _action_launch_stock_rule()。
这一段非常关键,因为它说明了一个事实:
销售确认后,并不是“订单本身”去创建库存动作, 而是订单行根据自己的补货需求,去启动 stock rule。
订单行在 _prepare_procurement_values() 里会把一堆关键上下文准备好:
origin:来源单据;date_planned/date_deadline:计划和截止时间;warehouse_id、route_ids:走哪条路线;partner_id:送货对象;location_final_id:最终目的地;sequence等追踪信息。
这些值随后会被包装成 procurement,再交给 stock.rule。
三、stock.rule._run_pull() 才是真正创建库存移动的地方
_run_pull() 会把 procurement 转成 move values,然后用 sudo 创建 stock.move,最后 _action_confirm()。
这个设计说明了两件事:
- 库存补货链是一个独立的规则系统;
- 它不依赖“销售单本身会不会直接写库存表”,而是依赖 route / rule 的配置。
所以有时候你看到销售确认后没出库,不是销售模块没跑,而是:
- 路线没配;
- 规则没匹配;
- 补货条件还没满足;
- 或者这个产品根本不走这条补货链。
四、开票链是另一套判断:看的是“可开票数量”
开票不看库存规则,它看的是另外一组变量:
invoice_status;qty_to_invoice;invoice_policy;- 以及订单行是否是 down payment、note、section 这类特殊行。
在 sale.order._get_invoiceable_lines() 里,Odoo 会先筛掉不该开票的行,再把要开的行按 section / subsection / down payment 重新组织。
然后 _create_invoices() 负责真正创建 account.move。
这意味着:
一个订单可以已经确认、已经生成出库单, 但仍然暂时没有可开票行; 反过来,一个订单也可能先有预付款发票,后面才真正出库。
五、最容易混淆的点:确认、交付、开票不是同一时刻
新手最常误解的是把这三件事混成一个按钮:
- 确认:订单状态推进;
- 交付:库存链路执行;
- 开票:会计链路执行。
它们可以在业务上连续发生,但在源码里是不同层次、不同模块、不同判断条件。
这也是 Odoo 好用的原因:
- 纯数字服务不需要库存链;
- 先收款后交付的场景可以单独处理;
- 部分交付、部分开票也能表达;
- 复杂销售流程不会被塞进一个巨大的“确认函数”里。
结论
销售确认后之所以会分成“补货链”和“开票链”,是因为 Odoo 把履约拆成了两个独立维度:
- 货怎么走,由 stock rule 负责;
- 钱怎么开,由 invoice policy 和 invoiceable lines 负责。
理解这一点之后,你再看销售单源码,就不会再期待“确认按钮”顺手替你做完所有业务动作了。
DISCUSSION
评论区