补货边界

Odoo 补货规则为什么有时明明缺货却不建议补:horizon days、deadline date 与 unwanted_replenish 讲透

很多人看到 Odoo 某个 orderpoint 已经接近缺货,却没收到自己预期的补货建议,就以为系统算错了。其实 stock.warehouse.orderpoint 里有 horizon days、deadline date、procurement date 和 unwanted_replenish 几层边界。本文把这套“为什么系统暂时不建议补”的机制讲透。

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

先说结论

Odoo 的 orderpoint 不是“只要快低于最小库存,就立刻建议补货”。

它更准确地说是在判断:

在当前 horizon 视野内,这个产品会不会真的落到需要现在就触发补货的程度;以及补完以后会不会反而多余。

所以你看到“看起来快缺了,但系统没像你想的那样补”,很多时候不是 bug,而是:

  • horizon days 还没把风险纳入当前视野;
  • deadline date 还没到关键点;
  • procurement date 正在往前倒推;
  • 或者 unwanted_replenish 认为再补就会超到不合理。

为什么“快缺货”不等于“现在就该补”

业务直觉通常是:

  • 低于 min 就补;
  • 高于 min 就先不补。

但 Odoo 的 orderpoint 没这么扁平。

它不仅看当前数量,还会看:

  • 未来 horizon 内的入库和出库;
  • lead time 倒推后的采购 / 调拨时点;
  • 补完后是否会超过合理上限;
  • 当前是手动触发还是自动触发。

所以它回答的不是“现在缺不缺”,而是:

现在是否到了必须启动补货链的时刻。


deadline_date 说明它在看“什么时候跌穿”

_compute_deadline_date() 的逻辑很值得看。

源码先判断:

  • 如果 qty_on_hand < product_min_qty,那 deadline_date 就是今天。

如果当前还没跌破,它不会立刻放弃,而是继续:

  • 按公司维度取 horizon_days
  • 取 horizon date;
  • 聚合 horizon 内相关 incoming / outgoing moves;
  • 找出库存第一次跌破 product_min_qty 的日期。

这说明 orderpoint 的视角不是一刀切阈值,而是:

  • 在可见的未来窗口内,什么时候会真正变危险。

这也是为什么“今天没跌穿、明天会跌穿”和“未来很久以后才跌穿”在系统眼里不是一回事。


get_horizon_days() 决定了系统看多远

get_horizon_days() 的优先级是:

  1. 先看上下文里的 global_horizon_days
  2. 再看记录所在公司的 horizon_days
  3. 最后才回到当前用户公司的设置。

这说明 horizon 不是一个硬编码常量,而是补货视野参数

它直接决定:

  • 系统是只看眼前几天;
  • 还是提前更久开始担心未来短缺。

因此很多“为什么它没提醒”的问题,本质上不是 min/max 算错,而是 horizon 太短。


_get_orderpoint_procurement_date() 解释了“为什么要提前动”

到了真正准备 procurement 的时候,源码会调用:

  • _get_orderpoint_procurement_date()

它把 lead_horizon_date 结合公司时区,转成一个具体 UTC datetime。

然后在调度逻辑里,又会结合 global_horizon_days 往前减天数。

这意味着 Odoo 不只是判断“会不会缺”,还在判断:

  • 如果会缺,现在是不是该为那一天提前启动采购 / 调拨 / 生产

所以 orderpoint 不是纯库存规则,它同时也是一个时间规划器。


unwanted_replenish 为什么很容易被忽视

很多人查补货问题时,只盯:

  • qty_forecast
  • qty_to_order
  • min/max

_compute_unwanted_replenish() 还有一道很关键的闸门。

它会判断:

  • 如果补完后的 virtual_available + qty_to_order 超过 product_max_qty
  • 那这次补货是否会变成“多余补货”。

换句话说,它在问:

这次补货建议虽然算得出来,但从库存控制角度是不是已经过头了。

这就是很多人觉得“系统明明缺货却不建议补”的典型误判来源。

因为系统看的不是现在这一瞬间,而是补后全局结果。


为什么 horizon 和 unwanted_replenish 要一起看

这两者分别限制不同层面:

  • horizon_days 管的是:系统看多远;
  • unwanted_replenish 管的是:即便决定补,也别补得太过。

所以一个典型现象是:

  • horizon 太短时,系统根本还没把未来风险纳入眼前;
  • horizon 看到了风险后,系统又可能因为补完会超 max,而给出谨慎判断。

这就是 Odoo orderpoint 的“既前瞻,又克制”。


为什么调度器里还要再减一次 horizon

_procure_orderpoint_confirm() 那段逻辑里,准备 procurement 时会:

  • 先拿 _get_orderpoint_procurement_date()
  • 再减去 global_horizon_days

这一步很容易把人看晕。

其实它表达的是:

  • orderpoint 看到的风险点在未来;
  • 但调度器要在更早的时间提前触发,才能让上游动作来得及完成。

也就是说:

  • horizon 不是仅仅用来显示风险;
  • 它还会影响何时真正把需求抛给 stock.rule。

实施里最常见的误区

1. 认为低于 min 才会触发,一切都只看当前库存

不对。 系统会看 horizon 内的动态变化。

2. 看到 qty_to_order > 0 就以为一定会建议执行

也不一定。 还要结合 unwanted_replenish、trigger、调度时点等边界。

3. 把 horizon 当成“报表过滤参数”

也不对。 它会影响 deadline 判断和 procurement 触发时机,是规划参数,不只是显示参数。


调试时建议怎么查

碰到“为什么没补 / 为什么现在才补”时,建议顺序是:

  1. 看当前 qty_on_handqty_forecast 与 min/max;
  2. deadline_date 是否已落到近期;
  3. get_horizon_days() 最终拿到的值;
  4. _get_orderpoint_procurement_date() 是否把动作推到更早或更晚;
  5. unwanted_replenish 是否在提醒“补完会超”。

这样排查比只盯一个 qty_to_order 要准确得多。


一句话总结

Odoo 的 orderpoint 不只是“低于最小库存就补货”的简单阈值器。

它同时在做四件事:

  • 在 horizon 内判断何时真正跌穿安全线;
  • 计算应该提前多久启动补货;
  • 把补货动作倒推到 procurement 时间点;
  • 防止补完以后反而变成多余库存。

最准确的理解是:

它是一个带时间视野和过量保护的补货规划器,而不是纯粹的 min/max 开关。

DISCUSSION

评论区

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