库存校验

Odoo 库存点了 Validate 为什么不一定立刻 Done:button_validate、补单向导和真正入账动作是分层的

很多人以为点击库存单上的 Validate 就是“直接完成”。但在 Odoo 源码里,button_validate 更像总闸门:它会先做检查、再跑向导、最后才可能进入 _action_done。本文把这条链路讲透。

Odoo 开发 库存
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 9 阅读

先说结论

在 Odoo 里,库存单上那个 Validate 按钮,语义并不是:

  • “直接把单据改成 done”

更准确地说,它的含义是:

开始一条‘准备完成库存动作’的总流程。

这个总流程中间可能会穿过:

  • 草稿确认
  • 已完成数量补值
  • sanity check
  • backorder 判断
  • backorder 向导
  • 最终 _action_done()

所以当你看到:

  • 点了 Validate 却弹窗
  • 点了 Validate 但状态没立刻 done
  • 不同 picking type 表现不一样

先别急着怀疑“按钮坏了”。

大概率只是因为:

button_validate() 根本就不是“直接 done”的同义词。


源码入口:stock.picking.button_validate()

addons/stock/models/stock_picking.py 里,这个方法的前半段做了几件很关键的事。

1. 先过滤掉已经 done 的 picking

也就是说,这个按钮只处理还没完成的单据。

2. 草稿单会先 action_confirm()

如果 picking 还是 draft,Odoo 不会跳过业务前置,而是先把它推进到正式库存链路里。

3. 对草稿 move,必要时把 quantity 补成需求数量

源码里有个很容易忽略的细节:

  • 如果 move.quantity == 0
  • move.product_uom_qty != 0

Odoo 会把 quantity 补成需求数量。

这其实是在帮“直接完成整单”的常见操作兜底。

你可以把它理解成:

  • 用户想一次性确认整个转移
  • 系统帮你把 done qty 补到合理默认值

第二层:sanity check 不是摆设

接着 button_validate() 会调用:

  • _sanity_check()

除非上下文里显式传了 skip_sanity_check=True

这个阶段会挡住很多不合法状态,比如:

  • 没数量却想完成
  • 序列号 / 批次缺失
  • 明显不满足完成条件的转移

所以业务上经常有人说:

  • “我就是想强制点过去”

但源码并不是这么设计的。

Odoo 的态度更像:

在真正 done 之前,先把数据边界守住。


第三层:真正关键的是 _pre_action_done_hook()

很多人只盯着 _action_done(),但 button_validate() 里最有意思的一层,其实是:

  • _pre_action_done_hook()

这说明 Odoo 把“完成前的分流逻辑”和“真正完成动作”故意拆开了。

这个 hook 先做什么

它会检查:

  • 是否已经填了 quantity
  • 是否存在 picked 标记

如果有完成数量但还没 picked,它会帮你把 move 标记成 picked。

这是一种“让实际执行语义更一致”的预处理。

更关键的是:它会决定要不要走补单判断

如果上下文里没有 skip_backorder,它会调用:

  • _check_backorder()

只要发现当前转移不是“全部需求都完整完成”,就可能不会直接 done,而是返回:

  • _action_generate_backorder_wizard(...)

这就是为什么很多时候点了 Validate,出来的是一个向导,而不是直接完成。


Backorder 不是异常,而是 Odoo 对“部分完成”的正式建模

很多新手会把 backorder 理解成:

  • 单据出了岔子
  • 系统又自动长了一张单

其实不是。

backorder 的本质是:

原单这次只完成了一部分,剩余未完成部分需要被正式保留下来,交给后续执行。

这在现实仓储里很正常:

  • 先发一部分货
  • 先收一部分货
  • 先内部转一部分物料

如果系统没有 backorder,就只能在“部分完成”与“完整完成”之间硬选一个,反而不真实。


Backorder 向导到底在做什么

addons/stock/wizard/stock_backorder_confirmation.py 里,逻辑非常直白。

default_get()

它会根据 pick_ids 自动生成一批 line:

  • 哪些 picking 需要处理
  • 默认 to_backorder=True

也就是说,向导默认站在“保留剩余量”为正式补单的立场上。

process()

它会把用户在向导里的选择分成两类:

  • 要 backorder 的 picking
  • 不要 backorder 的 picking

然后借助上下文里的:

  • button_validate_picking_ids
  • skip_backorder=True
  • picking_ids_not_to_backorder=...

再重新调用一次:

  • pickings_to_validate.button_validate()

这个设计很妙,因为它没有自己在向导里硬做所有 done 逻辑,而是:

让向导只负责“表达用户对剩余量的决策”,真正完成动作还是回到标准 Validate 主链。

这就是 Odoo 常见的“向导改上下文,主流程继续跑”的设计风格。


真正进入完成的是 _action_done(),不是按钮本身

stock_picking.py 里,button_validate() 通过了前置逻辑后,才会分两拨调用 _action_done()

  • 不该 backorder 的:with_context(cancel_backorder=True)._action_done()
  • 需要 backorder 的:with_context(cancel_backorder=False)._action_done()

这里你会看到一个特别关键的点:

同样叫 Validate,不同上下文会让最终 _action_done() 走出不同结果。

也就是说,完成不是一个单一动作,而是一个受上下文控制的落地策略。


为什么开发时最容易把这条链路改坏

因为很多人改库存逻辑时,会犯两个典型错误:

错误 1:把 button_validate() 当成“最终完成点”

于是把很多业务写死在这里。

结果是:

  • 向导场景不一致
  • backorder 场景失真
  • 批量处理和单个处理行为不一致

错误 2:只在 _action_done() 里想问题

结果漏掉:

  • sanity check
  • picked 预处理
  • backorder 分流
  • 上下文传参

于是就会出现:

  • 手工点按钮正常
  • 程序调用 _action_done() 却行为不同

这不是 Odoo 不稳定,而是你绕开了前半段分流流程。


一个很实用的调试顺序

以后只要遇到库存 Validate 行为异常,建议按这个顺序查:

1. picking / move 当前 state 是什么

是不是还在 draft、confirmed、assigned 之类的前置状态。

2. quantity / picked 是否合理

有没有 done qty,picked 标记是否被补上。

3. _sanity_check() 有没有拦住

很多异常其实在这里就结束了。

4. _pre_action_done_hook() 有没有返回向导 action

这一步经常决定“为什么没直接 done”。

5. 最后才看 _action_done()

别一开始就冲进去看库存落账。


一句话总结

button_validate() 在 Odoo 库存里真正扮演的角色,不是“立即完成按钮”,而是:

一个把确认、检查、补单判断和最终 done 串起来的总入口。

理解了这点,你就会明白:

  • 为什么点 Validate 有时会弹 backorder
  • 为什么有些自动化要补上下文才能和手工操作一致
  • 为什么库存完成逻辑应该按“前置分流 + 最终 done”两层来设计

这才是 Odoo 库存 Validate 的真实心智模型。

DISCUSSION

评论区

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