很多人第一次看 Odoo 库存,会把 assigned 误解成“已经完成”。其实它只是说明:系统已经把某些可用库存锁给了这张单据,但真正的出库验证还没发生。
这篇就顺着 /home/ubuntu/odoo-temp/addons/stock/models/stock_move.py 和 stock_quant.py 看一遍:Odoo 到底是怎么从“可用库存”走到“预留库存”的。
先记住一句话:_action_assign() 做的是“分配”,不是“完成”
stock.move._action_assign() 的职责很明确:给符合条件的 move 创建或更新 move line,让这张单的需求和某些 quant 绑定起来。
所以它解决的是:
- 这张单还缺多少数量;
- 哪些库存行可以拿来用;
- 需要把多少数量标成
reserved_quantity; - 最后这张 move 是
assigned还是partially_available。
它不做的是:
- 不把货真的发走;
- 不把 picking 直接变成
done; - 不替你跳过后续验证。
_action_assign() 先算“还差多少”,再决定怎么补
源码里最值得注意的第一步,是它先读出每个 move 当前的 reserved_availability,再计算缺口。
你可以把它理解成:
- 先看现在已经占了多少;
- 再看订单目标是多少;
- 差额才是这轮真正要处理的数量。
这就是 Odoo 很少重复造 move 的原因之一。它不是“确认一次就全量重来”,而是持续追踪缺口。
_gather() 负责找“哪些 quant 可以被拿来用”
真正决定“拿哪几行库存”的,不是 move 自己拍脑袋,而是 stock.quant._gather()。
这个方法先构造一个 domain,再按 removal strategy 排序:
- FIFO
- LIFO
- closest
- least_packages
_get_gather_domain() 里最关键的差异,是 strict:
strict=False时,location 会用child_of,lot/package/owner 也会放宽;strict=True时,位置和特征都要精确匹配。
所以你看到“同样是找库存,有时能找到,有时找不到”,很多时候不是系统坏了,而是 strict 和 domain 条件变了。
为什么会出现 partially_available
_action_assign() 不是只会成功或失败,它还会标记“部分可用”。
逻辑很简单:
- 找到的可用量 >= 需求量:
assigned - 找到的可用量 < 需求量:
partially_available
这在实务里非常重要,因为它让 Odoo 能继续工作,而不是因为少一部分货就整单卡死。
例如:
- 你手上只有 8 件;
- 销售要 10 件;
- 系统会先锁 8 件,并把状态标成部分可用;
- 剩下 2 件继续等补货或后续规则处理。
你容易误解的两个点
1)“有库存”不等于“能分配”
quantity 只是总量,真正可分配的是:
quantity - reserved_quantity
如果库存行已经被别的 move 占了,当前单也许看得到数量,但拿不到。
2)“找得到 quant”不等于“已经能出库”
_gather() 只是找候选库存;
_action_assign() 只是把候选库存锁住;
真正出库还得等 move / picking 后续确认。
开发和排错时该看什么
如果你在调 reservation 问题,建议按这个顺序看:
- move 的状态是不是
confirmed/waiting/partially_available; - 当前 location、lot、package、owner 是否和 quant 匹配;
- removal strategy 是不是你以为的 FIFO;
strict有没有被切换;- 这个 move 有没有走 bypass reservation 的特殊链路;
reserved_quantity是否已经被别的单据占掉。
一句话总结
Odoo 的库存分配不是“先抓货再说”,而是:
先算缺口,再用
_gather()找可用 quants,最后通过_action_assign()把这部分库存正式预留给当前 move。
理解了这条链,你就能把“库存明明在,但单据就是分配不上”的问题拆开看了。
DISCUSSION
评论区