先说结论
采购单在 Odoo 里不是“点一下审批就结束”。
它会把业务拆成几层:
- 采购单头部确认承诺
- 收货单负责真正的实物流转
- 已收数量负责回写采购明细
- 发票和退货再基于这些数量继续往下走
所以采购的核心不是单据本身,而是 采购单、收货单、库存 move、已收数量、发票状态 之间的联动。
审批按钮后,button_approve() 立即接上收货链
在 addons/purchase_stock/models/purchase_order.py 里,button_approve() 做完父类逻辑后,会接:
self._create_picking()
这说明审批不是终点,而是采购履约链路的开关。
一旦订单进入 purchase 状态,系统就开始检查:
- 有没有现成的 picking 可以继续用
- 有没有需要创建的新收货单
- 采购行该怎么对应到 stock.move
从这个角度看,采购审批更像是在说:
这张采购单正式成立,下面请把它接入库存执行链。
_create_or_update_picking() 负责把采购行落到收货单上
在 purchase.order.line 里,真正把收货单组织起来的是 _create_or_update_picking()。
这个方法会做几件特别实用的事情:
- 如果采购行是 consumable,会检查数量和发票之间的关系
- 尝试把已有的 stock.move 重新挂到当前采购行上
- 找到合适的 picking,没有就新建
- 再创建或更新对应的 stock moves
- 最后对这些 moves 依次
_action_confirm()._action_assign()
这里很重要的一点是:
采购行不是直接“生成收货单”,而是把自己绑定到一组库存 move 上。
收货单只是承载这些 move 的容器。
为什么 qty_received 不是简单数 done 数量
很多人以为 qty_received 就是“收货单 done 了多少”。
实际上,Odoo 在 _prepare_qty_received() 里算得更细。
它会:
- 只看和当前采购行匹配的 move
- 把 done 的入库数量加进去
- 把退货、供应商返货、dropship 回流等边界情况单独处理
- 最后再写回
qty_received
这个设计的意义是:
已收数量不是界面上看见的简单累计,而是采购链路真实结果的归纳值。
所以你会看到,Odoo 对退货场景特别小心,避免重复计算。
qty_received_method = 'stock_moves' 是关键开关
在 purchase.order.line 里,某些产品会把 qty_received_method 切到 stock_moves。
这意味着:
- 已收数量不再靠人工填
- 而是直接从库存 move 反推
- 对于 consu 或需要库存链的行尤其重要
这就是 Odoo 采购和库存联动的核心。
采购行并不自己“记住收了多少”,而是从库存执行结果里推导。
这样做的好处是,采购、收货、退货三条线不会各算各的。
receipt_status 其实是 picking 状态的摘要
在 purchase.order 里,_compute_receipt_status() 会根据 picking_ids 的状态给订单打摘要:
- 没有收货单,或者全 cancel:空
- 全 done/cancel:
full - 有 done,但不是全部 done:
partial - 还没 done:
pending
这就解释了为什么采购单列表里你看到的是一个“收货状态”,但它本质上来自 pickings。
同样,effective_date 也不是自己算出来的,而是从 done 的 receipt 里取最早日期。
新手最容易混淆的 3 个概念
1)审批不等于收货
审批只是让系统把采购单接入库存链路。
2)收货单不等于已收数量
收货单是单据;qty_received 是结果。
3)发票状态不完全独立
采购行的收货和退货结果,会影响是否该开票、是否要退款、以及是否出现欠收或多收的边界判断。
实战开发建议
如果你在改采购逻辑,建议按这条顺序检查:
button_approve()有没有进入收货链picking_ids有没有被创建或更新stock.move有没有正确连到purchase_line_idqty_received_method是不是走stock_moves- 退货和 dropship 的边界有没有被误算
采购的难点,不是“有没有单”,而是 单据、库存、发票三套状态如何保持一致。
DISCUSSION
评论区