先说结论
采购单一旦从 RFQ 变成正式订单,Odoo 并不是“只改个状态”这么简单。
对有库存影响的采购,系统还会继续把它推进到收货链路:创建收货单、生成 stock.move,然后确认并预留库存。
你可以把这条链路理解成三层:
- 采购层:
purchase.order负责订单状态、供应商信息、价格与交期 - 物流层:
stock.picking负责收货单 - 执行层:
stock.move负责真正的库存移动
所以,采购确认不是终点,而是物流链路的起点。
第一段:button_confirm() 先把采购单变成可执行状态
在 addons/purchase/models/purchase_order.py 里,核心入口是:
def button_confirm(self):
for order in self:
if order.state not in ['draft', 'sent']:
continue
error_msg = order._confirmation_error_message()
if error_msg:
raise UserError(error_msg)
order.order_line._validate_analytic_distribution()
order._add_supplier_to_product()
if order._approval_allowed():
order.button_approve()
else:
order.write({'state': 'to approve'})
return True
这段逻辑的重点有三个:
- 先检查订单是否还处于 draft/sent
- 校验订单行是否缺产品、分析分布是否合法
- 把供应商补进产品主数据,避免下次再下单时还是“陌生供应商”
如果订单满足审批条件,状态会直接进入 purchase;否则先进 to approve。
新手最容易误解的一点是: 他们以为确认采购单只会改状态。实际上,采购确认常常同时在“修正主数据”和“准备后续物流动作”。
第二段:收货单不是在 purchase 模块里凭空长出来的
真正创建收货单的是 purchase_stock 对 purchase.order 的扩展。
相关代码在:
addons/purchase_stock/models/purchase_order.py:_create_pickingaddons/purchase_stock/models/purchase_order.py:_prepare_picking
核心思路很直接:
- 先决定收货单的类型和上下游地点
- 再创建
stock.picking - 根据采购行生成库存移动
- 把 move 确认、排序、预留
- 继续把受影响的后续 picking 一并确认
其中 _prepare_picking() 负责组装收货单值:
return {
'picking_type_id': self.picking_type_id.id,
'partner_id': self.partner_id.id,
'origin': self.name,
'location_dest_id': self._get_destination_location(),
'location_id': self.partner_id.property_stock_supplier.id,
'company_id': self.company_id.id,
'state': 'draft',
}
这说明收货单的来源地点通常是供应商库位,目的地点则是仓库默认收货区或伙伴配置的目的地。
第三段:_create_picking() 负责把采购行变成库存移动
_create_picking() 的结构可以概括为:
moves = order.order_line._create_stock_moves(picking)
moves = moves.filtered(lambda x: x.state not in ('done', 'cancel'))._action_confirm()
for move in sorted(moves, key=lambda move: move.date):
move.sequence = ...
moves._action_assign()
forward_pickings = self.env['stock.picking']._get_impacted_pickings(moves)
(pickings | forward_pickings).action_confirm()
这里最值得注意的不是“创建了什么”,而是“接下来做了什么”:
_action_confirm():让 move 进入已确认状态_action_assign():尝试预留库存_get_impacted_pickings():如果有推式规则,还要把后续单据一起推进
也就是说,采购确认后的结果并不是单一对象,而是一个小型链式反应。
这条链路的核心心智模型
你可以把它记成一句话:
采购单负责“我要买什么”,收货单负责“货从哪里来”,库存移动负责“系统准备怎么收”。
三者职责不同:
purchase.order管业务意图stock.picking管物流单据stock.move管执行细节
这样分层的好处是:
- 采购可以只负责商务逻辑
- 仓库可以独立处理收货
- 预留、拆单、批次跟踪都能在库存层继续演化
实战开发时最常见的坑
-
只改 purchase,不看 purchase_stock 采购确认后真正的收货动作通常来自扩展模块,不在基础 purchase 里。
-
把 picking 当成“凭证头” picking 不是空壳,它连接着 location、move、package、lot 和后续流程。
-
忽略
_action_assign()只看到创建 move,不代表库存已经锁定。 -
忘记确认后还有后续推式规则 采购到货并不一定停在一个 picking,上游/下游规则可能继续生成更多单据。
一句话总结
Odoo 的采购确认不是单纯的状态切换,而是把业务订单推进到物流执行链。
purchase.order.button_confirm() 先完成业务层准备,purchase_stock._create_picking() 再把它变成收货单和 stock.move,最后通过确认与预留把流程真正落到库存上。
DISCUSSION
评论区