先说结论
Odoo 里的 Replenish 按钮,不是“临时建一条补货规则”。
它真正更像是:
面向某个产品、某个仓库、某个时间点,手动发起一次即时 procurement 请求。
也就是说,它不是在长期配置层定义“以后低于多少就补”;而是在当前上下文里说:
- 我现在就要补这件货
- 用这个仓库视角
- 走这条 route
- 计划日期按这条路线的延迟来推
这就是为什么手动补货和 orderpoint 虽然都能触发补货,但源码层语义完全不同。
为什么很多人会把它误解成 orderpoint
因为业务结果看起来有点像:
- 都可能生成采购 / 调拨 / 制造动作;
- 都和 route、warehouse、预测库存有关;
- 都在解决“库存不够怎么办”。
但 orderpoint 在回答的是:
- 长期补货策略是什么
- 何时自动触发
- 低于阈值时要补到多少
而手动补货在回答的是:
- 我现在决定补这件货
- 请系统立刻把这次需求跑进补货引擎
一句话区分:
orderpoint 是规则,replenish wizard 是请求。
源码抓手:addons/stock/wizard/product_replenish.py
这篇文章的关键入口主要在:
default_get_compute_forecasted_quantity_get_date_plannedlaunch_replenishment_prepare_run_values_get_route_domain
只要把这几个方法串起来,手动补货的真实定位就很清楚了。
第一步:向导默认值就已经在表达“即时请求”,不是“长期规则”
default_get 里会根据上下文补齐很多东西:
product_id/product_tmpl_idcompany_idwarehouse_idproduct_uom_idroute_id
这里最值得注意的是两点。
1)仓库默认取当前公司下的仓库
这说明手动补货不是抽象全局动作,而是明确绑定仓库视角。
2)route 会按允许域优先选一个可用路线
这说明 wizard 打开的那一刻,系统就在尝试回答:
- 这次补货准备走哪条供给路径?
这很像人工下达一条供应请求,而不是建制度。
第二步:默认数量来自预测库存,不来自 min/max 阈值
_compute_forecasted_quantity 里,向导会用:
product_id.with_context(warehouse_id=rec.warehouse_id.id).virtual_available
也就是站在该仓库视角看 virtual_available。
随后 _onchange_product_id 会做一个很实用的默认逻辑:
- 如果预测库存小于 0,默认补货量取其绝对值;
- 否则默认给 1。
这段逻辑很有代表性。
因为它表达的不是:
- 安全库存低于多少
- 最小补货量是多少
- 最大补货量是多少
而是:
先看当前预测缺口,如果已经欠货,就先把坑补平;如果没欠货,那就至少给一个最小人工请求量。
这明显是“即时操作”思维,不是“长期补货参数”思维。
第三步:计划日期不是随便填的,而是按 route 的 rule delay 倒推出来
_get_date_planned 的逻辑非常直接:
- 取当前时间
now - 如果有 route,就把 route 下所有
rule.delay累加 - 最后得到
date_planned
这说明手动补货虽然是人工触发,但并不是“拍脑袋立刻到货”。
Odoo 仍然在问:
- 这条 supply 路要经过哪些规则层?
- 每层规则会吃掉多少天?
- 最终这笔补货应该以什么时间进入执行?
所以更准确的说法不是“手动补货跳过规则”,而是:
手动补货跳过的是自动触发机制,不是补货规则本身。
这一点特别容易被误解。
第四步:真正的核心在 launch_replenishment ——它直接跑 stock.rule.run
这就是全文最关键的地方。
launch_replenishment 里没有去创建 orderpoint,也没有把这次请求先存成一条长期规则。
它做的是:
- 调
self.env['stock.rule'].with_context(clean_context(...)).run(...) - 传进去一个
Procurement(...)
这个 Procurement 里包含:
- 产品
- 数量
- 单位
- 仓库主库存位
warehouse_id.lot_stock_id - 名称 / 来源:
Manual Replenishment - 公司
- 以及
_prepare_run_values()里的运行参数
这已经很说明问题了:
Replenish 按钮本质上是手动把一条 procurement 请求直接塞进规则引擎。
不是先定义规则再等 scheduler 发现它,而是此刻就执行。
_prepare_run_values() 说明:它不是低配版补货,而是完整走 route / warehouse / date 语义
这个方法会带上:
warehouse_idroute_idsdate_plannedforce_uom = True
也就是说,手动补货并不是“临时 shortcut”。
它并没有绕开:
- 仓库语义
- 路线语义
- 时间语义
- 单位语义
它跳过的只是“由系统自己监控并自动触发”的那一层。
这也是为什么手动补货和自动补货能复用同一套 supply engine。
为什么补货后还能弹出新单据提示
launch_replenishment 里会记录一个时间点 now,然后用 _get_record_to_notify(now) 去找刚刚生成的 stock.move,最后拼一个通知。
如果 move 有对应 picking,就给你一个跳转链接。
这说明 Odoo 对手动补货的交互预期是:
- 你刚刚手动下达了一次 supply 请求
- 系统已经把它转成了真实执行对象
- 你应该能立刻跳过去看结果
这和 orderpoint 的体验也很不一样。
orderpoint 更像后台机制; manual replenish 更像前台操作入口。
为什么它不是“创建 orderpoint 后再跑一次”
源码里虽然还留了 _prepare_orderpoint_values(),但注释已经写明:
TODO: to remove in master
而且真正执行补货的路径并不依赖它。
这其实已经给了我们一个很明确的信号:
官方设计正在更明确地区分“手动即时补货”与“长期 orderpoint 配置”。
所以项目里如果有人打算把 Replenish 按钮魔改成“自动落一条 orderpoint”,要非常小心。
因为这会把一次性操作,硬生生改成长期策略,常常会留下后患:
- 下次 scheduler 又自动补一遍;
- 用户只是临时补货,结果系统以后一直按这条规则跑;
- 补货语义从 request 变成 policy,责任边界变得很混乱。
实战里最容易误解的 5 个点
1)手动补货不是绕开 route
它不是不走 route,而是人工指定 / 触发一次 route 驱动的 procurement。
2)手动补货不是自动补货的弱化版
两者共用很多底层规则,但一个是人工请求,一个是系统监测触发。
3)默认数量不是“建议采购量”
默认数量只是基于当前负预测库存给你的一个操作起点,不等于完整补货策略。
4)计划日期不是 UI 装饰
它反映 route 规则延迟,影响后续链路节奏。
5)不要把一次性操作偷偷变成永久规则
这是最常见、也最伤系统可解释性的定制错误。
什么时候该用手动补货,什么时候该用 orderpoint
更适合手动补货的场景
- 临时缺货,需要立刻发起一次补货
- 运营人员明确知道这次要走哪条 route
- 你想先人工确认,而不是让系统后台自动跑
- 某产品不是稳定消耗品,不适合长期阈值配置
更适合 orderpoint 的场景
- 周期性消耗稳定
- 希望系统自动监控库存并补货
- 需要 min/max 或规则化补货机制
- 不想每次人工点一次 Replenish
小结
看完源码后,最实用的记法其实很简单:
- orderpoint = 持久化补货政策
- product.replenish = 一次性补货请求入口
所以,Odoo 的 Replenish 按钮之所以不能简单理解成“临时建一条补货规则”,是因为它真正做的是:
直接把当前产品、仓库、路线和计划日期打包成一条 procurement,请规则引擎立刻执行。
这也正是它在实战里非常好用的原因:
- 有规则的严谨
- 但保留人工操作的灵活
它不是 orderpoint 的替身,而是另一种完全不同的补货入口。
DISCUSSION
评论区