拣货算法

Odoo 为什么有时会优先清掉整包库存:least_packages、A* 搜索与拣货目标函数讲透

很多人以为 Odoo 的出库策略无非就是 FIFO、LIFO、FEFO 排个序。但 stock.quant 里 actually 单独实现了 least_packages,并且用 A* 搜索包裹组合。这说明它优化的不是时间顺序,而是“尽量少拆包”。本文把这套目标函数讲透。

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

先说结论

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:

  1. 先看产品分类上的策略;
  2. 沿着库位向上找 location 上的策略;
  3. 都没有就回到 fifo

这说明 least_packages 和 FIFO 一样,是正式的库存移除策略,不是某个临时优化选项。

也就是说,只要上下文命中它,后面整套 quant 收集逻辑都会带着这个目标走。


核心抓手:Odoo 先按 package 聚合可用量

进入 _run_least_packages_removal_strategy_astar() 后,第一步不是逐条 quant 排序,而是:

  • 先按 package_id group by;
  • 汇总 SUM(quantity - reserved_quantity)
  • 只保留可用量大于 0 的包。

这一步非常关键。

因为它说明系统此刻关注的基本单位不是 quant,而是:

  • 一个包作为可供组合的容器单元。

这正是 least_packages 与常规先进先出策略的本质区别。


为什么无包裹库存会被拆成一个个单件元素

源码里对 package_id is None 的情况做了特殊处理:

  • 把无包裹库存按整数数量拆成 (None, 1) 的单件元素。

这说明在 least_packages 的世界里:

  • 整包库存是高价值组合单元;
  • 无包裹散件则被视为一个个独立小块。

业务含义非常直白:

  • 如果能用少数几个整包满足需求,系统更愿意这样做;
  • 散件通常意味着更多拣选动作、更多拆包、更多操作成本。

为什么这里用了 A* 搜索

因为这是个典型“从若干包裹组合中找一个尽量优”的问题。

源码定义了:

  • PriorityQueue
  • Node(count_remaining, taken_packages, next_index)
  • heuristic(node)

当前节点记录:

  • 还差多少数量;
  • 已经选了哪些包;
  • 下一步从哪开始继续试。

启发函数则用:

  • 已选包数
  • 加上剩余数量除以下一个包可用量的粗略估计

来给搜索排序。

这正是 A* 的味道:

  • 不是暴力穷举所有组合;
  • 而是优先探索更可能“包更少、又更接近满足数量”的路径。

best_leaf 暴露了真实目标:先少包,再少浪费

源码维护了一个 best_leaf,专门处理这些情况:

  • 精确命中不到;
  • 只能超选;
  • 或根本包不够。

当出现超选时,比较规则是:

  1. 优先更少的包数;
  2. 如果包数相同,再优先“超得更少”。

这句话非常重要。

它说明 least_packages 的第一目标不是最少超量,而是:

最少包裹数。

超量控制只是第二层。

这和很多用户的直觉相反。

所以你看到系统宁可选 2 个大包多出一点,也不一定会选 5 个小包刚好凑满,这其实可能是算法有意为之。


为什么 _get_removal_strategy_order() 仍然返回 in_date ASC

源码里 least_packagesfifo 共用:

  • 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

评论区

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