先说结论
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() 的优先级是:
- 先看上下文里的
global_horizon_days; - 再看记录所在公司的
horizon_days; - 最后才回到当前用户公司的设置。
这说明 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_forecastqty_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 触发时机,是规划参数,不只是显示参数。
调试时建议怎么查
碰到“为什么没补 / 为什么现在才补”时,建议顺序是:
- 看当前
qty_on_hand、qty_forecast与 min/max; - 看
deadline_date是否已落到近期; - 看
get_horizon_days()最终拿到的值; - 看
_get_orderpoint_procurement_date()是否把动作推到更早或更晚; - 看
unwanted_replenish是否在提醒“补完会超”。
这样排查比只盯一个 qty_to_order 要准确得多。
一句话总结
Odoo 的 orderpoint 不只是“低于最小库存就补货”的简单阈值器。
它同时在做四件事:
- 在 horizon 内判断何时真正跌穿安全线;
- 计算应该提前多久启动补货;
- 把补货动作倒推到 procurement 时间点;
- 防止补完以后反而变成多余库存。
最准确的理解是:
它是一个带时间视野和过量保护的补货规划器,而不是纯粹的 min/max 开关。
DISCUSSION
评论区