先说结论
Odoo 的 least_packages 出库策略,优化目标不是“最早入库先出”,也不是“最接近到期先出”。
它更像是在回答:
为了满足这次出库量,怎样选包裹组合,才能尽量少拆包、少动散件?
所以一旦仓库启用了 least_packages,你看到系统优先拿整包、优先清掉某几个大包,不一定是 bug。
它可能正是在执行一套和 FIFO 完全不同的目标函数。
为什么 least_packages 不是“再套一个排序”
很多人第一次听到 removal strategy,会自然联想到:
- FIFO:按最早入库排序;
- LIFO:按最后入库排序;
- FEFO:按到期日排序;
least_packages:那大概就是按包裹数量排序?
不对。
如果只是排序,系统面对这些情况会很难处理:
- 两个大包刚好够;
- 三个中包也够;
- 一个大包加几个散件也够;
- 没有精确匹配,只能超选或不足。
这不是简单“按谁排前面”能解决的问题,而是一个组合选择问题。
这也是为什么 Odoo 源码专门写了 _run_least_packages_removal_strategy_astar()。
先看策略入口:_get_removal_strategy()
在 stock_quant.py 里,Odoo 会先找 removal strategy:
- 先看产品分类上的策略;
- 沿着库位向上找 location 上的策略;
- 都没有就回到
fifo。
这说明 least_packages 和 FIFO 一样,是正式的库存移除策略,不是某个临时优化选项。
也就是说,只要上下文命中它,后面整套 quant 收集逻辑都会带着这个目标走。
核心抓手:Odoo 先按 package 聚合可用量
进入 _run_least_packages_removal_strategy_astar() 后,第一步不是逐条 quant 排序,而是:
- 先按
package_idgroup by; - 汇总
SUM(quantity - reserved_quantity); - 只保留可用量大于 0 的包。
这一步非常关键。
因为它说明系统此刻关注的基本单位不是 quant,而是:
- 一个包作为可供组合的容器单元。
这正是 least_packages 与常规先进先出策略的本质区别。
为什么无包裹库存会被拆成一个个单件元素
源码里对 package_id is None 的情况做了特殊处理:
- 把无包裹库存按整数数量拆成
(None, 1)的单件元素。
这说明在 least_packages 的世界里:
- 整包库存是高价值组合单元;
- 无包裹散件则被视为一个个独立小块。
业务含义非常直白:
- 如果能用少数几个整包满足需求,系统更愿意这样做;
- 散件通常意味着更多拣选动作、更多拆包、更多操作成本。
为什么这里用了 A* 搜索
因为这是个典型“从若干包裹组合中找一个尽量优”的问题。
源码定义了:
PriorityQueueNode(count_remaining, taken_packages, next_index)heuristic(node)
当前节点记录:
- 还差多少数量;
- 已经选了哪些包;
- 下一步从哪开始继续试。
启发函数则用:
- 已选包数
- 加上剩余数量除以下一个包可用量的粗略估计
来给搜索排序。
这正是 A* 的味道:
- 不是暴力穷举所有组合;
- 而是优先探索更可能“包更少、又更接近满足数量”的路径。
best_leaf 暴露了真实目标:先少包,再少浪费
源码维护了一个 best_leaf,专门处理这些情况:
- 精确命中不到;
- 只能超选;
- 或根本包不够。
当出现超选时,比较规则是:
- 优先更少的包数;
- 如果包数相同,再优先“超得更少”。
这句话非常重要。
它说明 least_packages 的第一目标不是最少超量,而是:
最少包裹数。
超量控制只是第二层。
这和很多用户的直觉相反。
所以你看到系统宁可选 2 个大包多出一点,也不一定会选 5 个小包刚好凑满,这其实可能是算法有意为之。
为什么 _get_removal_strategy_order() 仍然返回 in_date ASC
源码里 least_packages 和 fifo 共用:
in_date ASC, id
这并不意味着 least_packages 退化成 FIFO。
更准确地说:
- 当需要在包内或候选量上进一步稳定排序时,它仍然借用 FIFO 风格的时间顺序;
- 但真正决定选哪几个包的,是前面的 A* 组合搜索。
所以这里不要被返回值误导。
in_date 是辅助顺序,不是首要目标函数。
MemoryError 保护也很值得注意
源码显式捕获了 MemoryError:
- 如果 A* 搜索耗尽内存,就记录日志并回退到原 domain。
这说明 Odoo 官方自己也清楚:
- least_packages 在理论上更复杂;
- 当候选包裹过多时,搜索成本会上升。
因此它不是一套“免费优化”。
如果你的仓库里:
- 候选包很多;
- 同类商品散布很碎;
- 包裹粒度极细;
那么它的算法收益和成本就需要一起评估。
实施里最常见的误读
1. 以为它违反了 FIFO
其实不是违反,而是目标不同。 它要优化的是包裹数量,不是纯时间顺序。
2. 以为系统只会选“刚好够”的组合
也不是。 如果刚好够需要很多包,而略微超量只要很少包,算法可能会选后者。
3. 看到散件没被优先出就觉得异常
在 least_packages 模式下,散件本来就可能被压后,因为它们会增加操作复杂度。
什么场景特别适合 least_packages
通常是这些场景:
- 整箱、整托优先出库;
- 仓库更怕拆包而不是怕少量超拣;
- 人工作业成本高于细粒度最优配比;
- 想尽量减少 package 层级被打散。
反过来说,如果你的业务更重视:
- 精确数量;
- 严格时序;
- 到期优先;
那 least_packages 就未必是最适合的策略。
一句话总结
Odoo 的 least_packages 不是 FIFO 的一个别名,也不是简单排序。
它是一个明确的组合优化策略:
- 先按 package 聚合;
- 用 A* 搜索候选组合;
- 优先最少包数;
- 在无法精确满足时,再尽量减少浪费。
最准确的理解是:
它优化的是仓库操作成本,不是理论上的数量排序优雅度。
DISCUSSION
评论区