预留失败

Odoo 明明有货为什么还是预留失败:reservation 边界、strict 匹配和负库存修正讲透

库存界面看着有 on hand,但单据就是 assign 不上,往往不是“系统坏了”,而是 reservation 正在按更严格的边界判断。本文把这类失败的真正原因拆开讲清。

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

先说结论

在 Odoo 里,“有货”“这张单据现在能 reserve 到货” 不是一回事。

很多人看到产品页还有 on hand,就会默认这张拣货单理应 assign 成功。但源码真正问的问题更像是:

在当前 location、lot、package、owner、tracking、UoM 与 reservation 规则下,这个 move 还能不能合法拿到足够数量。

所以“明明有货却预留失败”,大多数时候不是 bug,而是你在看总量,系统在看当前动作可合法占用的那一小块量


入口看起来简单,真正难点在后面

库存预留主入口还是熟悉的:

  • stock.picking.action_assign()
  • stock.move._action_assign()
  • stock.quant._get_reserve_quantity()

但一旦进入 stock.quant,逻辑就不再是“仓里有多少就拿多少”,而是先回答几个更细的问题:

  1. 这次要不要 bypass reservation
  2. 要按宽松匹配还是 strict=True
  3. lot / package / owner 有没有卡边界
  4. serial 产品能不能按整数保留
  5. 当前 quant 里是不是混着负数 / 负预留残留

很多“看着有货却分不到”的真相,都藏在这些细节里。


第一类误判:你看的 on hand,不是这次 move 能用的 available

最常见的误区是把下面两句话当成同义词:

  • 仓里还有货
  • 这张单据能拿到货

其实不是。

_get_available_quantity()_get_reserve_quantity() 都支持:

  • lot_id
  • package_id
  • owner_id
  • strict

也就是说,Odoo 看的不是“总库存”,而是带条件的库存

举个最典型的例子:

  • 总库存 100
  • 其中 60 属于某个 owner
  • 20 在特定 package 里
  • 20 已被别的 move reserve 了

你在产品页看到“还有货”,并不代表当前这张 move 在自己的边界里还有货。


第二类误判:strict 语义和人脑直觉不一样

stock.quant._get_gather_domain() 很值得看。

strict=False 时,Odoo 会更宽松地找 quant,例如:

  • location_id child_of 当前库位
  • lot 可能允许 [指定 lot, False]
  • owner / package 只在有值时参与约束

但当 strict=True 时,条件会明显收紧:

  • location 必须完全相等
  • package / owner 必须完全相等
  • lot 也会按精确边界来算

这就是为什么很多链路里你会遇到一种反直觉情况:

同样是“查库存”,宽松时看起来够,真正落到具体 move line 或链式 move 时却又不够。

因为系统已经从“广义上有没有”切换到了“这一条具体明细能不能严丝合缝地匹配”。


第三类误判:serial 产品不是“有零头也能先分一点”

源码里 _get_reserve_quantity() 有一个很关键的约束:

  • 如果产品 tracking 是 serial
  • 而可保留数量不是整数
  • 那么数量会直接归零

这背后的意思很简单:

序列号产品的 reservation 不是“先凑一点”,而是必须按一件一号的离散单位来成立。

所以你可能看到:

  • 界面上还有 0.6、0.8 这类数量
  • 甚至 UoM 换算后感觉“差不多够了”

但对 serial 产品,差一点就是不行。


第四类误判:UoM 和 rounding 会把“看起来够”变成“系统不敢分”

源码里在 reserve 前会做 UoM 换算,并且尽量避免“超分配”。

这一步会考虑:

  • move 的 UoM
  • product 的基础 UoM
  • rounding precision
  • DOWN / HALF-UP 之类的换算方式

所以某些场景下,人脑算的是:

  • 反正就差一点点,分了也没事

系统算的是:

  • 如果按 rounding 规则一换算,会超出允许边界,那我宁可不分

这类问题在包装单位、采购单位和库存单位不完全一致时特别常见。


第五类误判:负 quant / 负预留会让“可用量”比你想的更小

_get_reserve_quantity() 里还有一段非常实战的逻辑:

  • 它会先识别 quantity - reserved_quantity < 0 的 quant
  • 把这些负的残留先冲掉
  • 再决定真正还能分多少

这说明 Odoo 不是单纯把所有正数加总就完事,而是会先处理历史上已经存在的异常形态。

这类异常可能来自:

  • 之前的强制操作
  • 特殊链路下的库存修正
  • inventory adjustment 把链式 move 的供给打断
  • 某些 done / unreserve / revert 操作后的临时不平衡

所以你看到“表面数量够”,系统却 reserve 不满,有时不是当前单据的问题,而是历史 quant 状态先把一部分可用量抵掉了。


第六类误判:上游 move 带来的“可用量”也不一定真的还在 quant 里

_action_assign() 对有 move_orig_ids 的 move,会先看父 move 带来了多少可分配的 move line,再去 double-check quant 自身是否还可用。

源码里甚至直接写了一个很重要的注释:

  • 这可能是 inventory adjustment 把原来那部分量全部或部分拿掉了

意思是:

上游链路说“理论上这批货应该流到你这里”,不代表 quant 层现在还真的保有这批货。

所以链式补货 / MTO / 多步物流里,预留失败不能只看 move 关系,还要落回 quant 事实层确认。


最值得先排的 6 个点

如果你遇到“明明有货却 assign 不上”,最省时间的排查顺序是:

1. 看产品是不是 storable,库位是不是 bypass reservation

有些动作本来就不走普通 reservation 语义。

2. 看 move 需要的边界

重点确认:

  • location
  • lot
  • package
  • owner
  • tracking

3. 看可用量是不是已经被别的 move reserve 掉了

不是 on hand,而是 quantity - reserved_quantity

4. 看 serial / UoM / rounding 有没有把数量打成 0

尤其是序列号产品与包装换算。

5. 看 quant 里有没有负残留

这会先吞掉可保留量。

6. 看是不是链式 move,被 inventory adjustment 或别的动作中途改了底层事实

“上游说有”不等于“量现在还在”。


新手最容易犯的判断错误

1. 把产品页 on hand 当成 reservation 成功保证

不是。

2. 以为 assign 失败一定是 location 没货

也可能是 lot / owner / package 不匹配。

3. 以为 serial 产品能像普通产品那样先拆点零头

不行。

4. 以为链式 move 的可用量来自上游关系,而不是来自 quant 事实

关系链只是线索,不是最终库存事实。

5. 看到数量够就忽略 rounding

系统不会因为“差一点点”就网开一面。


一句话记忆法

Odoo 的 reservation 失败,很多时候不是“没货”,而是“在当前 strict 边界、tracking 规则、UoM 精度和 quant 真实状态下,没有足够可合法占用的货”。

理解这句话,你以后看到“明明有货却预留失败”,就不会再只盯着 on hand 发呆。

DISCUSSION

评论区

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