先说结论
很多人排查库存时,一看到 move 还没 assign,就立刻问:
- 为什么有货却不分配?
但有相当一类问题,根本不是“分配失败”,而是:
系统在当前时点还没打算开始抢这批库存。
最实用的理解是:
free_qty决定“理论上现在还有多少自由可用量”reservation_method+reservation_date决定“这张 move 什么时候该进入库存竞争”- operation type 决定这套时机规则挂在哪个作业制度上
should_bypass_reservation()决定这张 move 是否根本不走普通 reservation 路径
所以有些 move 停在 confirmed,并不是因为系统没看到库存,而是因为:
- 它还没到该预留的时间
- 或者 它根本不该走普通预留逻辑
为什么这类问题特别容易被误判
因为用户的观察顺序通常是:
- 产品还有库存
- 这张单还没 ready
- 那一定是系统没分到货
但源码真正的判断顺序不是这样。
系统更像是在问:
- 这张 move 现在应不应该开始 assign?
- 如果应该,当前 location 有没有 free_qty?
- 如果有,能不能按 reservation 规则真正 reserve 住?
所以“明明有库存却还没开始预留”这类问题,很多时候卡在第 1 步,而不是第 2 步或第 3 步。
free_qty 不是“你看到的库存总量”
很多人会把 free_qty 想成:
- 仓里剩多少就等于 free_qty
其实不是。
在产品数量计算里,free_qty 更接近:
当前在手量扣掉已预留量之后,理论上还能自由使用的量。
所以它本来就比 on hand 更贴近 reservation 逻辑。
这意味着:
- on hand 很多,不代表 free_qty 很多
- free_qty 看起来够,也不代表这张 move 现在就会立刻 reserve
换句话说:
free_qty 只是“如果现在开始竞争库存,大概还有多少可抢”;它不是“系统已经决定要抢你这张单”的承诺。
真正决定“现在要不要开始抢”的,是 reservation timing
这层很多人容易忽略。
在 stock.move._should_assign_at_confirm() 里,源码逻辑大意是:
- 如果 bypass reservation → 可以按特殊逻辑走
- 或者 picking type 的
reservation_method == at_confirm - 或者
reservation_date <= 今天
否则,哪怕 move 已 confirmed,也不代表它会马上进入 _action_assign()。
这非常关键。
因为它说明:
“有没有库存” 和 “系统此刻会不会动手预留” 是两个问题。
reservation_method 为什么其实是在决定库存竞争的入场时机
在 operation type 上,Odoo 提供常见几种 reservation method:
at_confirmmanualby_date
它们真正决定的不是“预留功能开不开”,而是:
- 这类 move 在什么时点允许进入 reservation 竞争
at_confirm
一确认就尽快尝试。
manual
默认不自动抢,等人工或后续动作触发。
by_date
等 reservation_date 到了再说。
这就是为什么两张看起来一模一样的 move,会有完全不同的库存体感:
- 一张一确认就锁货
- 另一张可能还在 confirmed 静静待着
根因不是库存本身,而是 reservation method 不同。
reservation_date 是把“策略”落成“具体日期”的关键字段
如果 reservation method 是 by_date,系统会根据 move 的计划日期倒推出 reservation_date。
源码里 stock.move._compute_reservation_date() 就在干这个事情:
- 取 move.date
- 减去 operation type 上的提前天数
- 得到真正的 reservation_date
所以这张 move 即使已经存在、库存也看起来有,系统仍可能选择:
- 今天先不抢
因为在它眼里:
- 这张 move 还没到该进场的时间窗口
这和很多用户的直觉冲突很大,因为用户更容易按“单据已经确认了”思考,而 Odoo 更像按“执行窗口到了没”思考。
operation type 为什么是这个问题的制度层开关
很多人排查 reservation 时,只盯当前 move。
其实 reservation timing 很多时候不是 move 自己决定的,而是它所属的 operation type 决定的。
也就是说:
- 这张 move 是不是 at_confirm
- 是不是 by_date
- 提前几天进场
这些很多是 picking type / operation type 级别的制度配置。
所以如果你发现一批 move 都有类似表现:
- 都 confirmed 很久
- 都没开始 assign
- 但库存看起来没问题
优先看 operation type,往往比只盯单张 move 更快。
should_bypass_reservation() 为什么能让问题彻底换个维度
源码里还有一个很关键的判断:
location.should_bypass_reservation()- 或者产品不是 storable
这时 move 可能根本不走普通 reservation 路径。
翻成人话就是:
有些 move 本来就不是按“先锁 quant,再进入 assigned”这套常规剧本来跑的。
所以如果你在某些库位 / 产品上硬套普通 reservation 逻辑,就会看得越来越怪。
这也是为什么排查时第一步应该先确认:
- 当前 location / product 是否属于 bypass reservation 场景
不然你会一直在问一个系统根本没打算回答的问题。
为什么“还没开始预留”和“预留失败”必须分开看
这两个问题很像,但不是一回事。
还没开始预留
重点问的是:
- 为什么它还没进
_action_assign() - reservation_date 到没到
- reservation method 是什么
- operation type 是怎么规定的
预留失败
重点问的是:
- 进了 assign 之后
- 为什么 free_qty / quant / lot / owner / package / strict 边界不满足
所以这篇更关注的是前者:
- 为什么系统当前时点还没动手
而不是:
- 动手以后为什么失败
把这两层分开,你排查会快很多。
一个很常见的误判场景
假设:
- 产品页面还有 free_qty
- 出库 move 处于
confirmed - 用户说“这不是应该马上 ready 吗?”
这时很多人直接去查 quant。
但更高效的顺序其实是:
- 先看 move 有没有上游依赖,排除
waiting逻辑 - 再看
reservation_method - 再看
reservation_date - 再看 operation type 的制度配置
- 最后才去看 quant / free_qty / reserve 细节
因为这类问题里,最常见的真相往往是:
- 它不是 reserve 失败,而是压根还没轮到 reserve
最实用的排查顺序
1. 先确认当前 move 是否应该走普通 reservation
看:
should_bypass_reservation()- 产品是不是 storable
2. 再确认它当前是否到了 assign 时机
看:
reservation_methodreservation_date_should_assign_at_confirm()
3. 再确认当前 operation type 的制度配置
看:
- 是 at_confirm 还是 by_date / manual
- 提前天数怎么配
4. 最后再看 free_qty
如果前面都没问题,再问:
- 当前 location 真的还有可自由竞争的量吗
5. 如果 free_qty 也够,再下钻 reservation 失败边界
比如:
- lot / owner / package / strict / quant 等细节
这样排查会比“上来就看产品数量”高效得多。
最容易踩的 6 个坑
1. 把 on hand 当成“现在就该预留”
不是。
2. 把 free_qty 当成“已经为这张单保留的量”
也不是。它只是理论自由量。
3. 忽略 reservation timing
这会把时机问题误判成库存问题。
4. 只看 move,不看 operation type
很多制度规则根本挂在 picking type 上。
5. 看到 confirmed 就默认系统应该已经开始 assign
不一定,可能还没到 reservation_date。
6. 忘了 bypass reservation 场景
这样会把本来不走普通预留的 move 也硬往 reservation 逻辑里塞。
一句话记忆法
把这个问题记成一句话:
有库存却还没开始预留,很多时候不是库存不够,而是这张 move 还没到该进库存竞争的时间,或者它根本不走普通 reservation 路径。
理解这一句之后,你以后再看到一张 move 停在 confirmed,就不会只盯着库存数字问“为什么还没分配”,而会先去问:
- 它现在到底该不该开始分配?
DISCUSSION
评论区