先说结论
在 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_idsskip_backorder=Truepicking_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
评论区