库存搬移

Odoo 库存搬移为什么不是“把 quant 改个库位”:stock.quant.relocate、partial package 和搬移边界讲透

很多人以为库存搬移就是把 quant 的 location 改一下,但官方向导真正处理的是更麻烦的现实:整包还是拆包、目标是库位还是包裹、盘点数量要不要先清、跨多个来源库位时界面如何约束。本文把这套逻辑讲透。

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

先说结论

Odoo 的库存搬移,不是把 stock.quantlocation_id 直接改掉

更准确地说,它在处理的是:

现有库存状态要如何在不破坏包裹结构、盘点语义和目标约束的前提下,被安全地搬去另一个位置。

所以搬移动作关注的不只是“从哪到哪”,还包括:

  • 这批 quant 是不是属于完整包裹
  • 目标是新库位、已有包裹,还是两者都有
  • 是否存在盘点中的数量状态需要先清理
  • 多来源库位时,目标该如何限制

这也是为什么官方会单独做一个 stock.quant.relocate wizard,而不是开放一个可随便编辑 quant 位置的表单。


为什么“直接改库位”听起来简单,实际却很危险

因为 quant 从来不只是一个“库存数量格子”。

它还可能同时背着这些维度:

  • 产品
  • lot / serial
  • owner
  • package
  • company
  • 当前盘点数量状态

如果你粗暴把 quant 的位置改掉,很容易出现两类问题:

1)破坏包裹一致性

一个包裹里的 quants 被你搬走一部分,但系统却还把它当成一个整体包裹。

2)破坏盘点语义

有些 quant 可能带着库存盘点上下文,如果你没先清干净,就会把“待确认的盘点事实”和“已发生的搬移动作”混在一起。

所以 Odoo 在搬移时,重点不是“能不能改位置”,而是:

如何让搬移后的库存状态仍然自洽。


源码抓手:addons/stock/wizard/stock_quant_relocate.py

这个 wizard 的关键点主要在:

  • _compute_is_partial_package
  • _compute_is_multi_location
  • _compute_dest_package_id_domain
  • _compute_dest_package_id
  • action_relocate_quants

从这些方法就能看出,官方把库存搬移当成一个“带结构约束的库存调整动作”。


第一步:系统先判断你是不是在搬“半包”

_compute_is_partial_package 会看当前 quant_ids 里涉及到的 package_id,并检查:

  • 某个包裹里的所有 quant,是不是都包含在本次搬移集合里

如果不是,就把它标记为:

  • is_partial_package = True

这非常关键。

因为在仓库现实里,“搬一个完整包裹”和“从包裹里拆一部分出来搬走”完全不是同一件事。

前者的语义是:

  • 包裹整体转移

后者的语义是:

  • 先拆包
  • 再把其中一部分库存迁移出去

如果系统不先识别这个区别,就很容易把整包结构弄乱。


第二步:多来源库位时,wizard 不会假装目标包裹还能随便选

_compute_is_multi_location 会判断当前 quants 是否来自多个 location_id

如果是,而且你还没指定 dest_location_id,就会标记 is_multi_location = True

这背后的意思很实用:

当来源不止一个库位时,系统不能再假装“目标包裹域”天然明确。

因为包裹往往带着位置上下文。

如果你连目标库位都没定,系统就很难知道:

  • 哪些目标包裹是合法候选
  • 它们该属于哪个位置语义

所以 Odoo 宁可先把这层复杂性暴露出来,也不愿意给你一个看似方便、实则模糊的选择器。


第三步:目标包裹不是随便列,而是按公司 + 位置域收紧

_compute_dest_package_id_domain 很值得看。

它给目标包裹构造了一个 domain,大致包含:

  • 公司要匹配当前 quant 所属公司,或者是无公司共享包裹;
  • 如果选了 dest_location_id,目标包裹要么无固定位置,要么就在该位置;
  • 如果没选目标库位,但来源 quant 只来自单一位置,则目标包裹也要么无位置、要么就在该来源位置。

这说明 Odoo 对“包裹目的地”的理解不是纯粹容器选择,而是:

一个包裹是否是合法目标,要看它和位置语义是否兼容。

很多项目里之所以会出现“库存进了包,但包和库位关系奇怪”的脏数据,就是因为这层限制没守住。


第四步:如果原来选过的目标包裹不再合法,系统会自动清掉

_compute_dest_package_id 会检查当前选中的 dest_package_id 是否还满足最新 domain。

如果不满足,就清空。

这看似是小细节,其实非常重要。

因为搬移向导里经常会发生这种情况:

  • 你先选了一个目标包裹
  • 后来又改了目标库位
  • 或者 quant 集合发生变化
  • 于是原包裹其实已经不合法了

如果系统不自动清掉,就会把一个旧选择带进新语境,最后产生很难解释的数据。


第五步:真正执行搬移前,先 action_clear_inventory_quantity()

这是整篇文章里最值得注意的步骤之一。

action_relocate_quants 一上来就先做:

  • self.quant_ids.action_clear_inventory_quantity()

这说明 Odoo 非常在意:

搬移的是当前库存状态,而不是尚未清理干净的盘点中间态。

换句话说,如果 quant 上还带着 inventory quantity 相关状态,系统会先把这层盘点痕迹清掉,再去做位置迁移。

这避免了两种语义混淆:

  • “我在盘点差异”
  • “我在搬库存”

项目里如果有人定制 quant 搬移,完全跳过这一步,后面盘点异常通常很难查。


partial package 时为什么可能要先 unpack

如果本次是 is_partial_package,而且你又没有指定 dest_package_id,源码会先找出那些“包裹未完整入选”的 quants,然后:

  • move_quants(..., unpack=True)

这一步的业务语义很明确:

  • 如果你不是把它们搬进另一个包裹
  • 又只拿走原包裹中的一部分
  • 那系统就必须先承认“原包裹结构被拆开了”

所以先 unpack,再搬。

这就是为什么 partial package 不是小问题,而是结构性问题。

它不只是数量少一部分,而是:

包裹作为操作单元的完整性已经不成立。


最后才是真正的 move_quants

把前面的限制、清理、拆包都做完后,wizard 才会调用:

  • move_quants(location_dest_id=..., package_dest_id=..., message=...)

这里的 message 其实也很实用。

它意味着搬移不是静默改数据,而是允许把“为什么搬”留下痕迹。

对库存治理来说,这很重要。因为很多仓库问题最后不是不知道搬了,而是不知道为什么搬


为什么搬完后返回页面还会区分 lot / 单产品

源码最后会根据上下文决定跳去:

  • lot 的 quant 视图
  • 单产品 quant 视图
  • 或通用 quant 列表

这说明 Odoo 认为库存搬移不是纯后台动作,而是一个需要继续复核的操作。

也就是说,搬移完成之后,系统默认你还要看:

  • lot 视角是否合理
  • 单产品库存切片是否合理
  • 全局 quant 结果是否合理

这其实也是在提醒我们:

quant relocation 更像“受控库存重定位”,不是一笔无痕字段编辑。


实战里最容易踩的 5 个坑

1)把 quant relocation 当成位置字段编辑

这样很容易破坏 package、lot、owner 等边界。

2)忽视 partial package

半包搬移如果不先拆结构,后续 package 语义会乱。

3)目标包裹不校验位置合法性

最终经常会得到“包裹在 A 位,内容却像在 B 位”的脏状态。

4)跳过 inventory quantity 清理

盘点中的中间状态和搬移动作混在一起,很难收拾。

5)只关心数量搬过去了,没有留下原因

库存治理不只是结果对,还要可追溯。


小结

看完官方源码后,最实用的一句话总结其实是:

Odoo 的 quant relocation,不是在改 quant 的位置字段,而是在维护库存状态、包裹结构和盘点语义的一致性。

所以你如果在项目里做库存搬移相关开发,千万别把它想轻了。

一旦忽略:

  • package 完整性
  • 目标包裹合法性
  • inventory quantity 清理
  • 多来源库位边界

最后问题通常不会马上爆,而是会在盘点、追溯、整包操作时一起爆出来。

DISCUSSION

评论区

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