先说结论
Odoo 的采购确认,也不是“按钮一按就什么都干完了”。
它其实分成两件不同的事:
- 采购业务状态推进:RFQ 变成 PO,或者进入待审批状态
- 库存履约对象生成:创建收货单、库存 move,并继续确认下游链路
这就是为什么采购模块看起来是在“确认订单”,库存模块却已经开始“准备收货”。
第一层:purchase.order.button_confirm()
在 purchase/models/purchase_order.py 里,确认逻辑本身并不负责直接创建收货单。
它主要做这些事:
- 检查订单能不能确认
- 校验 analytic distribution
- 把供应商写回产品
- 根据审批规则决定是进入
purchase还是to approve
这说明一个关键事实:
采购确认首先是业务状态,不是库存对象。
也就是说,纯 purchase 模块只是把采购关系说清楚;真正把它接到库存链路上的是 purchase_stock。
第二层:purchase_stock.PurchaseOrder.button_approve()
purchase_stock 继承了 button_approve(),并在父类完成状态推进后补了一句:
self._create_picking()
这句话就是收货链路的起点。
所以对有库存收货需求的采购单来说,审批不是终点,而是履约开始。
_create_picking() 做了什么
这段代码的核心思想是:
- 如果已经有未完成收货单,就复用它
- 如果还没有,就新建一个 incoming picking
- 然后把采购行转换成 stock move
- 再 confirm、assign,并继续确认被 push 规则影响到的后续 picking
代码结构非常清楚:
pickings = order.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
if not pickings:
res = order._prepare_picking()
picking = StockPicking.with_user(SUPERUSER_ID).create(res)
moves = order.order_line._create_stock_moves(picking)
moves._action_confirm()
moves._action_assign()
forward_pickings = self.env['stock.picking']._get_impacted_pickings(moves)
(pickings | forward_pickings).action_confirm()
对新手来说,这里最容易误解的一点是:
采购单不是直接“变成”收货单,而是采购单驱动了收货单。
这两个对象仍然是分开的,只是 traceability 把它们连起来了。
_run_buy() 负责的不是“收货”,而是“补货决策”
采购链路还有另一半:stock.rule._run_buy()。
这段逻辑的工作重点不是收货,而是:
- 找到匹配的供应商
- 决定是否创建新的 PO
- 或者把 procurement 合并进已有的 PO 行
- 再根据供应商交期回写
date_planned
如果找不到供应商,它还会走一条降级逻辑:
- 把下游 move 切回 make-to-stock
- 取消可传播的链路
- 通知责任人去补供应商资料
这表示 _run_buy() 做的是“采购决策”,不是“仓库收货动作”。
为什么采购确认和收货单不能混为一谈
这个边界非常重要。
采购确认回答的是:
这笔采购业务是否成立?
收货单回答的是:
这批货是否真的到仓了?
所以你会看到:
button_confirm()管业务状态_create_picking()管收货对象move._action_assign()管预留picking.action_confirm()管下游链路
它们是一条链,但不是同一个动作。
实战里怎么判断问题出在哪
如果采购单确认了,但你没看到收货单,优先查:
- 采购行产品是不是需要库存履约
purchase_stock是否真的装上了- 订单状态是不是已经到
purchase picking_ids是否已有未完成单据- 供应商、路线和仓库设置是否让规则走了别的路径
如果你调试的是补货问题,别只盯着采购订单本身,最好一路追到 stock.rule._run_buy()。
一句话总结
采购确认不是“结束”,而是“开始把采购关系落到仓库链路上”。
button_confirm() 决定业务状态,_create_picking() 生成收货单,_run_buy() 负责补货决策,三者拼起来才是完整的采购履约链。
DISCUSSION
评论区