采购收货链路

采购单确认后,Odoo 如何生成收货单和库存移动?

从 purchase.order.button_confirm 和 purchase_stock._create_picking 看 Odoo 如何把采购确认变成收货单、stock.move 和后续预留。

Odoo 开发 库存 采购
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

采购单一旦从 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

这段逻辑的重点有三个:

  1. 先检查订单是否还处于 draft/sent
  2. 校验订单行是否缺产品、分析分布是否合法
  3. 把供应商补进产品主数据,避免下次再下单时还是“陌生供应商”

如果订单满足审批条件,状态会直接进入 purchase;否则先进 to approve

新手最容易误解的一点是: 他们以为确认采购单只会改状态。实际上,采购确认常常同时在“修正主数据”和“准备后续物流动作”。

第二段:收货单不是在 purchase 模块里凭空长出来的

真正创建收货单的是 purchase_stockpurchase.order 的扩展。 相关代码在:

  • addons/purchase_stock/models/purchase_order.py:_create_picking
  • addons/purchase_stock/models/purchase_order.py:_prepare_picking

核心思路很直接:

  1. 先决定收货单的类型和上下游地点
  2. 再创建 stock.picking
  3. 根据采购行生成库存移动
  4. 把 move 确认、排序、预留
  5. 继续把受影响的后续 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 管执行细节

这样分层的好处是:

  • 采购可以只负责商务逻辑
  • 仓库可以独立处理收货
  • 预留、拆单、批次跟踪都能在库存层继续演化

实战开发时最常见的坑

  1. 只改 purchase,不看 purchase_stock 采购确认后真正的收货动作通常来自扩展模块,不在基础 purchase 里。

  2. 把 picking 当成“凭证头” picking 不是空壳,它连接着 location、move、package、lot 和后续流程。

  3. 忽略 _action_assign() 只看到创建 move,不代表库存已经锁定。

  4. 忘记确认后还有后续推式规则 采购到货并不一定停在一个 picking,上游/下游规则可能继续生成更多单据。

一句话总结

Odoo 的采购确认不是单纯的状态切换,而是把业务订单推进到物流执行链。 purchase.order.button_confirm() 先完成业务层准备,purchase_stock._create_picking() 再把它变成收货单和 stock.move,最后通过确认与预留把流程真正落到库存上。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。