很多人第一次读 Odoo 包裹相关字段,都会陷入一个看似合理但非常致命的误解:
package_id是包裹;result_package_id也是包裹;- 那它俩大概只是“包裹名称前后不一致”吧。
不是。
这两个字段最关键的区别,不在于“它们都指向 stock.package”,而在于:
它们站在库存流转的不同时间面。
如果你没先把“来源容器”和“结果容器”分开,后面看整包作业、二次装箱、包中包、package level、put in pack 向导,基本都会越看越乱。
一句话先讲透
package_id:货现在从哪个包里出来;result_package_id:这次处理后,货要进入哪个包里去。
前者是来源视角,后者是结果视角。
所以这不是“同一个字段重复建模”,而是 Odoo 明确把一次库存动作拆成:
- 货从哪里来;
- 货处理完以后落到哪里。
这正是装箱、拆箱、整包转移能成立的基础。
为什么这两个字段不能合并
想一个最简单的仓库动作:
- 从散货区拿出 10 件;
- 装进一个新建纸箱;
- 纸箱再跟着发运。
在这个动作里:
- 开始时货可能没有
package_id; - 结束后却会有
result_package_id。
再看另一个动作:
- 从托盘 A 里拣出一部分;
- 放进纸箱 B;
这时:
package_id = 托盘 Aresult_package_id = 纸箱 B
如果你只有一个 package 字段,根本没法同时表达“来源容器”和“结果容器”。
所以 Odoo 这两个字段不是啰嗦,而是为了避免把一条 move line 的前后状态糊成一团。
action_put_in_pack() 真正在做什么
很多人把 Put in Pack 按钮理解成“给几行明细贴个箱号”。 源码告诉你,事情没这么浅。
action_put_in_pack() 的核心顺序大致是:
- 先决定哪些 move line 这次要参与装箱;
- 必要时弹向导,让你先解决目标库位 / 包类型 / 包名等问题;
- 创建包裹或复用已有包裹;
- 把相关 move line 的
result_package_id指向这个包; - 如有必要,重新应用 putaway 策略。
注意重点:
- 它改的是
result_package_id,不是简单写package_id; - 它表达的是“本次操作的产出包裹”。
这也是为什么看装箱逻辑时,如果你老从“源包裹”角度读,就会觉得源码很绕。
单行装箱时,目的库位甚至可能跟着包一起变
源码里有个很值得注意的细节:
当只有一条 move line 被装进包裹时,系统会基于这个 package 再跑一次 _get_putaway_strategy()。
这背后的业务含义是:
- 不是只有“产品”决定目的地;
- 包裹本身也可能影响后续放到哪里。
这对理解以下场景很有帮助:
- 某些包装类型必须进指定区;
- 整包上架和散货上架走不同路径;
- 目的库位不是在行创建时一次性写死的。
也就是说,装箱不是文档美化,而是可能改变真实仓内流向的动作。
package level 解决的不是“又一层包装字段”
不少人第一次看到 package level 会想:
“不是已经有 result_package_id 了吗,怎么又多一层?”
原因在于:
result_package_id解决的是单条 move line 最终落在哪个包;package level解决的是这一组明细是否应被视为一个整体包裹作业单元。
也就是说,package level 不是字段堆叠,而是把“多行一起组成整包语义”提到更高层。
这层抽象一旦没有,你就很难优雅表达:
- 整包移动;
- 整包校验;
- 一组行共同代表一个运输 / 拣货包;
- 包中包和整包追加装箱的连续动作。
为什么 Odoo 要区分“行装箱”和“整包装箱”
因为现实仓库里,这两类动作并不一样。
行装箱
强调的是:
- 我从若干明细中挑出一些数量;
- 把它们塞进某个结果包;
- 结果包是这次动作的输出。
整包装箱 / 包中包
强调的是:
- 我手里已经有一个包;
- 我要把整个包再放进更大的包;
- 关注点不再是明细拆分,而是包树结构。
源码里 stock.package.action_put_in_pack() 正是在处理后者这类语义。它不是再去逐条定义商品,而是在处理 package 到 package 的目标容器关系。
为什么 package 相关 bug 总显得“诡异”
因为很多 bug 不是库存数量错,而是你把包裹字段读反了。
例如:
- 你以为
package_id应该表示“这次装进去的包”,于是自定义写错字段; - 你以为
result_package_id只是展示用途,结果跳过它,整包逻辑全断; - 你没区分来源包和结果包,导致 traceability 看起来像货“瞬移”。
这类问题的共同点是:
数量可能还对, 但容器时序已经错了。
而容器时序一错,后面的:
- 包裹追踪
- 整包作业
- putaway 决策
- 包中包关系
都会一起变得难解释。
推荐的源码阅读顺序
建议按这个顺序看:
stock.move.line里package_id/result_package_id字段定义;_put_in_pack():看结果包是如何创建与挂接的;action_put_in_pack():看 UI 入口如何组织待装箱行与已有包;stock.package.action_put_in_pack():看包中包是怎么建立目标容器树的;- package level / entire pack 相关逻辑:理解“多行 = 一个整包动作单元”的抽象。
这样读,层次会清楚很多。
实战排错:先问的是“从哪来”,还是“要去哪”
只要是 package 问题,我建议先把字段按时间面拆开问:
- 这批货现在是从哪个包里来的?对应
package_id。 - 这次操作结束后,它应该落进哪个包?对应
result_package_id。 - 这是在处理单行装箱,还是把整个现成包再装进别的包?
- 这组行要不要被视为一个 package level / entire pack 操作?
很多看起来复杂的问题,到这里就已经能拆开了。
最后的结论
Odoo 的 package 建模看似字段多,实则是在努力保留库存动作的前后关系:
- 来源包不是结果包;
- 行级容器结果不是整包操作语义;
- 装箱不只是贴标签,而会影响后续 putaway 与包树结构。
所以如果你想真正看懂 Odoo 的包裹链路,第一步不是背 API,而是把这句话刻进脑子里:
package_id讲的是“货从哪个包出来”,result_package_id讲的是“货最后进哪个包”。
一旦这条时间线不再混,后面 package level、整包搬运、包中包逻辑都会顺很多。
DISCUSSION
评论区