先说结论
stock.move 是 Odoo 库存链路里最核心的“执行单元”。
很多人只看到界面上的状态变化:
- Waiting
- Available
- Assigned
- Done
但源码里真正发生的事情更像一条流水线:
- 先判断这张 move 要不要预留
- 再把可用库存写成 move line
- 如果数量不够,就标成部分可用
- 到了过账时,再把 move line 逐条落地
- 如果只做了一部分,就拆 backorder
- 最后把下游 move 继续推下去
所以,stock.move 不是“一个状态字段”,而是 库存执行过程本身。
_action_assign() 做的不是“改状态”,而是“算预留”
在 addons/stock/models/stock_move.py 里,_action_assign() 的注释已经说明了核心目标:
Reserve stock moves by creating their stock move lines.
也就是说,它不是简单把 move 改成 assigned,而是:
- 根据需求数量找可用 quants
- 创建或更新
stock.move.line - 让预留数量和需求数量对齐
源码里先会区分:
- 已经满足的 move
- 需要部分预留的 move
- 需要直接跳过预留的 move
这一步很关键,因为后面的所有状态,其实都来自 move line 的预留结果,不是反过来。
为什么会出现 partially_available
如果可用库存不够,Odoo 不会硬把它当成失败,而是进入 partially_available。
这正是库存链路很实用的地方:
- 有多少,就先占住多少
- 剩下的差额继续留在需求里
- 等后续补货、到货或别的链路补齐
你可以把它理解为:
系统不是在问“能不能一次做完”,而是在问“现在能先推进多少”。
这也是为什么 Odoo 的库存状态看起来不像传统 ERP 那样二选一,而是很细腻。
_update_reserved_quantity() 负责把“可用量”写进 move line
真正创建预留行的动作,落在 _update_reserved_quantity() 和 _update_reserved_quantity_vals()。
这两个方法会做几件事:
- 先从
stock.quant里查可预留数量 - 按批次、包裹、所有人、库位分组
- 尽量复用已有 move line
- 不够时再新建 move line
这里有一个很重要的设计点:
Odoo 预留的不是抽象数字,而是具体到 location / lot / package / owner 的库存片段。
所以你在界面里看到的“已预留 5”,背后往往不是一个值,而是几条有明确来源的 move line。
_action_done() 才是真正的“落地完成”
_action_assign() 只是预留,真正把库存变化写死的是 _action_done()。
这一步会先处理几个问题:
- 草稿 move 会被先确认
- 没有勾选或数量为 0 的 line 会被清理
- 必要时先创建 backorder
- 然后再对 move line 调
_action_done()
接着它会:
- 把 move 标记成
done - 写入完成时间
- 推动下游 move 重新
_action_assign() - 必要时继续生成新的 backorder
所以“点了验证”不是一个动作,而是 完成、推送、重算、拆单 的组合。
新手最容易误解的 3 件事
1)Assigned 不等于已经出库
Assigned 只表示预留好了,不代表库存已经真的减少。
2)Done 不是结束,而是链路的中间点
对于有下游 move 的场景,当前 move done 后,后面的 move 才有机会继续被推进。
3)backorder 不是异常,而是正常拆分机制
只做了一部分时,Odoo 倾向于把未完成部分保留下来,而不是直接报错中断。
实战开发时的建议
如果你要扩展库存逻辑,优先考虑这几个原则:
- 尽量用 hook,而不是整段重写
_action_assign()/_action_done() - 不要假设数量字段就代表真实预留结果
- 处理多公司、多批次、包装和追溯时,一定要考虑 move line 维度
- 需要调试时,先看 move state,再看 move line,再看 quant
理解了这条链路,你就能看懂很多库存“为什么没按我想的来”的问题。
DISCUSSION
评论区