收货回写

Odoo 采购收货完成时,库存、收货数量和采购单状态是怎么回写的?

很多人以为采购单收货“点一下 Validate 就结束了”,其实背后是一整条从采购单、收货单、stock move 到 qty_received 回写的联动链。本文把这条链拆开讲清。

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

先说结论

采购收货完成,不是把一张收货单状态改成 done 就结束。

真正发生的是一条连续动作:

  1. 采购单确认后生成或复用入库 picking
  2. 采购行生成 stock.move,并把 purchase_line_id 绑到 move 上
  3. 仓库在收货单里录入实收数量并点击 Validate
  4. stock.picking.button_validate() 进入标准完成链路
  5. purchase_stockstock.picking._action_done() 做扩展,先对采购单执行 action_acknowledge()
  6. 采购行的 qty_received 再根据已完成 move 重新计算

所以你在界面里看到的“已收货”“部分收货”“已完成”,本质上都不是独立字段各玩各的,而是围绕 采购行 ↔ 库存 move ↔ picking 完成 这条主线回写出来的结果。


第一段:采购单确认后,为什么会冒出收货单

purchase/models/purchase_order.py 里,button_confirm() 主要负责把采购单从草稿推进到确认流程。真正把库存单据建出来的关键扩展,在 purchase_stock/models/purchase_order.py

  • button_approve() 里会调用 _create_picking()
  • _create_picking() 会为采购单准备入库 picking
  • 然后让 order_line._create_stock_moves(picking) 生成 move

也就是说,采购确认和库存执行并不是两套平行系统:

采购单一旦进入已确认采购,库存入库载体就要被建出来。

这也是为什么你常会看到采购单和收货单天然关联在一起。


第二段:采购行到底把什么信息交给了库存 move

purchase_stock/models/purchase_order_line.py 里,_prepare_stock_moves() / _prepare_stock_move_vals() 很关键。

这里准备的不是“界面展示数据”,而是之后所有回写的锚点:

  • purchase_line_id
  • picking_id
  • product_uom_qty
  • location_id / location_dest_id
  • move_dest_ids
  • propagate_cancel
  • date_deadline

最重要的是 purchase_line_id

它意味着这条库存 move 不是一条孤立的入库动作,而是明确地说:

我是这条采购行的履约结果之一。

后面采购行要计算 qty_received,本质上就是沿着这根线去回看自己关联的 move。


第三段:Validate 按钮真正进入的是标准库存完成链

很多人会误以为“采购收货”有一套特殊入口。

其实收货单校验走的仍然是 stock/models/stock_picking.py 里的标准 button_validate()

  • 先补齐 draft picking 的确认
  • 做 sanity check
  • _pre_action_done_hook()
  • 处理 backorder 分支
  • 最后进入 _action_done()

这段设计很重要,因为它说明:

  • 采购收货不是旁路逻辑
  • 它是 标准库存完成链 上的一个业务场景
  • 采购模块只是把“采购上下文”挂接到这条库存主链上

所以排查问题时,别只盯采购模块。 很多“采购收货异常”,根子其实在 stock 的 backorder、done 数量、return、move 状态切换里。


第四段:为什么采购单会被标记为已确认收到

purchase_stock/models/stock.py 里对 stock.picking._action_done() 做了一个很轻但很关键的扩展:

self.purchase_id.sudo().action_acknowledge()
return super()._action_done()

意思很直白:

  • 这个 picking 若能反查到 purchase_id
  • 那么在真正完成库存动作前,先把采购单做 acknowledge

这一步不是数量计算本身,但它会影响采购单上的业务状态感知:

  • 供应商已交付的确认感
  • 采购协同流程上的“已被仓库承认收到”
  • 后续统计口径里的确认时间/回执语义

所以收货单 done,并不只是库存层的动作;它也在顺手推进采购单的业务语义。


第五段:qty_received 为什么会自动变动

真正让采购行“已收数量”变化的关键,在 purchase_stock/models/purchase_order_line.py_prepare_qty_received()

这段逻辑不是读一张 picking 的状态,而是逐条看本采购行关联的 move:

  • 只统计 state == 'done' 的 move
  • 普通入库,数量累加
  • 采购退货,按条件扣减
  • 某些 dropship / return 边界场景要跳过,避免重复计算

这说明一个经常被忽略的事实:

采购行的已收数量,本质上是根据“完成的库存 move”重算出来的,不是人工同步写进去的。

因此你会看到这些现象:

  • 只收一部分,qty_received 就部分增加
  • 做退货,qty_received 可能回退
  • move 没真正 done,只是 reserved / assigned,不会计入已收

这也是很多“为什么明明有收货单但采购单没显示已收”的答案:

因为决定口径的是 done move,不是“有单据存在”本身。


第六段:为什么有时采购单还是显示部分收货

因为 picking done 和采购 fully received 不是同一个判断层级。

一个 picking 完成,只代表:

  • 这一次收货动作结束了

但采购单是否“全部收完”,还要回到采购行层面看:

  • 每条采购行的 product_qty
  • 每条采购行的 qty_received
  • 是否存在 backorder / 后续补收

也就是说:

  • picking 是执行单据
  • purchase order line 是履约口径

执行动作已经 done,不等于履约义务已经全部完成。


实战里最容易误解的 4 件事

1. 以为 Validate 只改 picking 状态

错。它还会触发 move done、回写采购行已收数量,并推进采购单确认语义。

2. 以为 qty_received 是采购模块自己维护的

不准确。它在 stock_moves 模式下,本质上是由库存 move 结果倒推出的。

3. 以为看到收货单就一定算已收

不对。只有进入 done 的 move 才进入统计。

4. 以为部分收货异常一定在采购单上修

很多时候真正该查的是:

  • move 数量是否录对
  • 是否走了 backorder
  • 是否发生 return
  • 关联的 purchase_line_id 是否正确

你排错时该按什么顺序看

如果现场出现“采购单数量没回写”“收货后状态不对”,我通常按这个顺序排:

  1. 采购行是否真的生成了 purchase_line_id 关联 move
  2. move 是否 done
  3. done 数量是不是你以为的数量
  4. 是否有 return / backorder 抵消了结果
  5. picking 是否能正确反查到 purchase_id
  6. 再去看采购单页面上的状态展示

顺序不要反过来。 先盯页面状态,往往越看越乱;先盯 move 链路,通常很快就能定位。


最后一句话

Odoo 采购收货的设计并不是“采购单改一次、收货单再改一次”。

它更像是:

  • 采购单负责提出履约需求
  • picking / move 负责记录真实执行
  • 采购行状态再根据真实执行结果回写出来

所以想真正看懂“收货完成后系统为什么这样显示”,关键不是背页面字段,而是抓住这条主线:

采购行有没有被正确绑定到库存 move,而这些 move 又是否真的完成。

DISCUSSION

评论区

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