包裹历史

Odoo 包裹历史为什么不是“当前在哪个库位”:stock.package.history、父子容器与轨迹重建讲透

很多人看 Odoo 包裹时,只关心 package 现在在哪个库位。但官方源码专门建了 stock.package.history,说明系统更在乎的是这只包怎么被装、被套、被搬以及跟哪些调拨关联。本文把这条包裹追溯链讲透。

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

先说结论

在 Odoo 里,package 的当前库位并不足以表达 package 的业务历史

这就是为什么源码里单独存在 stock.package.history

它要解决的问题不是:

  • 包裹现在在哪。

而是:

  • 这个包从哪来;
  • 它中途进过哪些父容器;
  • 它这次搬运关联了哪些 move line;
  • 它属于哪张调拨;
  • 它的目标外层容器链是什么。

所以这套模型真正表达的是:

包裹不是一个静态位置对象,而是一条可追溯的容器演化链。


为什么“当前库位”不足以描述 package

如果仓库只做最简单的装箱发货,当前库位好像已经够看了。

但一旦出现这些场景:

  • 小箱装进大箱;
  • 大箱放到托盘;
  • 托盘整包转移;
  • 部分包裹被重新拆出再装;
  • 一次调拨中多层容器共同移动。

你就会发现:

  • 当前库位只能告诉你“它现在停在哪”;
  • 却无法告诉你“它是怎么变成现在这样的”。

而在追溯、售后、异常复盘里,后者往往比前者更重要。


stock.package.history 这个模型到底记什么

addons/stock/models/stock_package_history.py 看,这个模型核心字段包括:

  • location_id / location_dest_id
  • move_line_ids
  • package_id
  • package_name
  • package_type_id
  • parent_orig_id / parent_orig_name
  • parent_dest_id / parent_dest_name
  • outermost_dest_id
  • picking_ids

请注意这组字段的味道:

  • 它不是在复制 stock.package 的全部状态;
  • 它在保存一次包裹形态和流转关系的快照

尤其是:

  • 来源父容器;
  • 目标父容器;
  • 最外层目标容器;
  • 关联调拨;
  • 参与的 move lines。

这些都说明它关心的是 lineage,而不是单一时点属性。


历史是谁写进去的:_prepare_package_history_vals()

关键抓手在 stock_move_line.py_prepare_package_history_vals()

这个方法会:

  1. result_package_id 出发;
  2. 取出 _get_all_package_dest_ids() 涉及的整条目标包裹链;
  3. 对链上的每一个 package 生成 history vals;
  4. 把与该 package 对应的 move lines、pickings、父包裹、外层包裹一并写进去。

这有两个非常重要的含义。

含义一:history 不是从“当前 package 单体”推导,而是从“本次结果容器链”生成

也就是说,Odoo 认为包裹历史的形成时刻,是:

  • 一次 move line 处理把货放进某个结果包裹时。

不是你回头打开 package 详情页时现算。

含义二:它天然支持“包中包”的层级记录

因为它不是只记 result_package_id 这一个包。 它会把这条目的容器链一起展开。

这就是为什么 outermost_dest_id 会出现。


result_package_id 为什么在这里这么关键

很多人把 result_package_id 只理解为“这行货最后进了哪个包”。

这话没错,但还不够。

在 package history 语义里,它更像是:

触发容器轨迹落账的锚点。

因为有了 result_package_id,系统才知道:

  • 这批货现在属于哪个结果容器;
  • 该容器有没有父包;
  • 父包上面还有没有更外层包;
  • 哪些 move line 应该被归到这个历史片段里。

所以 package history 的成立,和 result_package_id 关系非常深。


为什么 history 里同时存“来源父容器”和“目标父容器”

字段里既有:

  • parent_orig_id
  • parent_dest_id

这不是重复。

它在表达两件不同的事情:

  • 原来这个包属于哪个上层容器;
  • 处理之后它被塞进了哪个上层容器。

如果没有这对字段,你只能知道“这个包现在在托盘 B 里”,却不知道:

  • 它是不是原来在托盘 A;
  • 它是不是刚从散包状态被装进 B;
  • 它是不是经历了一次重新套箱。

所以这对字段在售后排查、错装复盘、包裹重组分析里特别关键。


_get_complete_dest_name_except_outermost() 暗示了显示层的真实需求

stock.package_history.py 里有个方法:

  • _get_complete_dest_name_except_outermost()

逻辑很简单,但信息量很大:

  • 如果没有目标父容器,返回空;
  • 如果目标父容器就是最外层包,返回当前 package 名;
  • 否则返回 complete_name 去掉最外层之后的内部路径。

这说明 Odoo 在展示包裹链时,不只是想告诉你“属于哪个最外层包”,还想告诉你:

  • 在最外层包之内,这个包的内部嵌套路径是什么。

也就是说,UI 关心的不是单点,而是层级路径。

这和普通 location 树显示完全不一样。


为什么 history 里要存 picking_ids

按直觉,你可能会说:

  • 既然已经有 move line 了,调拨不是能反查出来吗?

理论上是,但直接把 picking_ids 落到 history 里,能让这张历史表更像一份“追溯索引”。

它意味着你可以很快回答:

  • 这只包跟哪些 transfer 有关;
  • 某张 picking 下改动过哪些包;
  • 这次异常包裹关系应该回看哪几张单。

这和纯粹依赖关系链回溯相比,查询和理解都会轻很多。


action_show_package() 的设计也说明:history 不是替代 package

action_show_package() 最后还是把你带回 stock.package 的 form view。

这说明 Odoo 的设计边界很清楚:

  • stock.package 是对象本体;
  • stock.package.history 是对象在流转中的历史片段。

history 不负责替代当前包对象。 它负责把“当前对象是怎么走到这一步的”补全。


实施里最容易踩的几个误区

1. 只看 package 当前库位做追溯

这会漏掉:

  • 父子容器变化;
  • 同一包关联多次调拨;
  • 中途重组或换外箱。

2. 把 package history 理解成日志表

它不是无脑流水日志。 它更像是围绕结果包裹链构造出来的业务快照

3. 认为 package 追溯只和 package 自己有关

不对。 它和:

  • result_package_id
  • move_line_ids
  • picking_ids
  • 父包裹链

是一起工作的。


调试包裹轨迹时建议怎么查

如果你遇到“这个包怎么会跑到这里”之类问题,建议顺序是:

  1. 先看相关 move line 的 result_package_id
  2. 再看 _prepare_package_history_vals() 逻辑能不能解释当前层级;
  3. 再看 history 里的 parent_orig_id / parent_dest_id 是否发生了换套;
  4. 最后再结合 picking_ids 回到具体调拨单。

这样排查会比只盯 package 当前页高效很多。


一句话总结

Odoo 的 stock.package.history 不是为了告诉你“包裹现在放在哪里”,而是为了保存:

  • 这只包与哪些 move line / picking 相连;
  • 它从哪个父容器来;
  • 又进入了哪个父容器;
  • 最终落在怎样的外层包裹链里。

最准确的理解是:

package 是当前对象,package history 是对象的容器谱系。

只有把这两层分开,你才真的读得懂 Odoo 的包裹追溯模型。

DISCUSSION

评论区

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