先说结论
Odoo 的 batch transfer / wave,不是“把几张 picking 放进一个列表里统一看”这么简单。
它更准确的语义是:
把一组符合约束的拣货单,组织成一个可共同分配、共同执行、共同校验,但仍保留各自库存事实的作业容器。
所以 batch 的重点,不只是“聚合展示”,而是:
- 哪些 picking 可以被纳入同一容器
- 这个容器何时进入执行态
- 校验时哪些空单要被摘出
- batch 与 wave 为什么不能乱混
- 批次自身的状态为什么会跟着子 picking 自动演化
这也是为什么官方专门做了 stock.picking.batch,而不是让用户靠过滤器把几张单勾在一起就算完。
为什么“只是打包几张单”这个理解会误导人
因为从界面观感上,batch 确实像一个装 pickings 的壳。
但如果它真只是个壳,很多问题就答不出来:
- 为什么 operation type 不同的单不能进同一批
- 为什么 done / cancel 的批次不能继续 merge
- 为什么有的 picking 会在验证时被自动从批次里移除
- 为什么批次也有自己的
state - 为什么 wave 和 batch 在源码里还要区分
is_wave
这些现象都在说明:
batch 不是静态集合,而是带执行规则的作业对象。
源码抓手:addons/stock_picking_batch/models/stock_picking_batch.py
配合向导文件一起看更清楚:
addons/stock_picking_batch/models/stock_picking_batch.pyaddons/stock_picking_batch/wizard/stock_picking_to_batch.py
最关键的方法包括:
_compute_allowed_picking_ids_compute_stateaction_confirmaction_done_sanity_checkaction_mergeStockPickingToBatch.attach_pickings
第一步:不是任何 picking 都能进 batch
_compute_allowed_picking_ids 会按这些条件筛:
- 同公司
- state 必须在允许集合里
- 如果已有
picking_type_id,还得同 operation type - draft picking 只有在 batch 本身还是 draft 时才允许加入
这意味着 batch 的边界不是“用户想怎么拼就怎么拼”,而是:
只有执行语义兼容的 picking,才允许进入同一个作业容器。
这很合理。因为不同 operation type 的单据,往往代表:
- 不同流程节点
- 不同仓位逻辑
- 不同人员和设备
- 不同校验习惯
把它们强塞进一个批次,执行上通常只会更乱。
第二步:batch 自己也不是手填状态,而是跟着 picking 演化
_compute_state 会根据子 picking 状态来计算批次状态:
- 全部取消 → batch 取消
- 所有未取消 picking 都 done → batch done
- 否则保留进行中 / 草稿等语义
这说明 batch 的状态不是独立业务事实,而是:
对子 picking 执行结果的作业层总结。
换句话说,batch 不是替代 picking 的状态机,而是叠加在 picking 之上的“执行容器状态机”。
第三步:action_confirm 的重点不是换状态,而是确认“这批单真的能一起干”
action_confirm 会做几件事:
- 没有 picking 不让确认
- 对 picking 执行
action_confirm() - 做公司检查
- 把 batch 状态设为
in_progress
这说明 batch confirm 不是 UI 上“我准备好了”的按钮,而是:
把一组 pickings 正式推进到同一批作业执行语境。
这就是为什么它会连带推动 picking 自己进入确认流程。
第四步:批量校验时,空单不是一刀切报错,而是按语义拆开处理
action_done 这段源码很值得读。
它会先区分几类 picking:
- 正常待处理的 pickings
waiting/confirmed且没有实际处理数量的空单assigned但本质为空的单- 至少做了一部分的单
随后它会:
- 对一些“空但没必要继续校验”的 picking 从 batch 里摘掉
- 对真正要执行的 picking 统一做
sanity_check - 再调用这些 picking 的
button_validate()
这说明 Odoo 对 batch validate 的理解,不是“所有子单全有或全无”。
而是:
尽可能让有实际作业内容的 picking 继续完成,同时把纯空壳 picking 安全地剥离。
这比简单粗暴地整批报错要成熟得多。
为什么 batch 里还要算重量、体积、lots 文本等
源码里 batch 会计算:
estimated_shipping_weightestimated_shipping_volumeshow_lots_textmove_idsmove_line_ids
这说明 batch 不是只拿来“批量点按钮”,它还承担了一个实际执行视角:
- 这批货大概多重
- 体积多大
- 是否涉及 lot 展示
- 聚合后的 move / move line 长什么样
这在仓库现场非常重要,因为批量作业通常首先是一个组织拣货与搬运资源的问题。
所以 batch 是执行单元,不只是数据集合。
第五步:为什么 batch 和 wave 不能混着 merge
action_merge 里有几个特别关键的约束:
- picking type 不同不能 merge
is_wave不同不能 merge- state 不同不能 merge
- done / cancel 的 batch/wave 不能 merge
这说明 Odoo 认为 batch 和 wave 虽然很像,但仍然不是同一种语义容器。
最朴素的理解可以这么记:
- batch 更像把一组 transfer 绑成一个执行批次
- wave 更强调波次组织与作业编排语义
源码没有允许你把这两种东西混着合并,本质上是在保护操作含义不被搅乱。
attach_pickings 也说明:加入 batch 不是简单 many2one 赋值
向导 stock.picking.to.batch 在新增 batch 时,会先检查:
- 选中的 pickings 是否属于同一公司
然后创建 batch,并根据选项决定:
- 仅创建 draft
- 还是直接
action_confirm()
这说明“把 picking 放进 batch”在 Odoo 里并不是静态归档动作,而是带执行后果的动作。
尤其是非草稿模式下,加入 batch 之后就可能直接进入作业态。
为什么 _sanity_check 如此重要
_sanity_check 会检查当前 batch 下的 picking 是否都仍然属于 allowed_picking_ids。
如果有不兼容的单据混进来,就直接报错,并列出 incompatible transfers。
这很像一个作业前的最后保险丝。
因为 batch 允许你聚合很多单,但聚合越多,越需要防止:
- 状态漂移
- 类型不一致
- 后续加单导致容器语义被污染
所以 sanity check 其实是在守住一句话:
批量执行可以提高效率,但前提是这批东西真的适合一起执行。
实战里最容易误解的 5 件事
1)batch 不是报表分组
它是可执行容器,不是单纯分组视图。
2)把 picking 放进 batch,不等于业务事实合并
库存事实仍然发生在各自 picking / move / move line 上,batch 只是组织层。
3)空单在 batch validate 时不是“异常边角料”
官方专门设计了摘出逻辑,说明这是常见现场情况。
4)wave 和 batch 不是完全同义词
既然源码要用 is_wave 区分,还禁止混并,就说明两者操作语义不同。
5)不要为了“方便”把不兼容 operation type 硬合批
短期省点点选,长期会增加现场混乱与解释成本。
小结
看完官方源码后,最实用的理解方式是:
Odoo 的 batch transfer / wave,本质上是仓库作业容器,而不是若干 picking 的可视化文件夹。
它负责的是:
- 把兼容的单据组织起来
- 让它们能一起分配、一起执行、一起校验
- 但又不抹掉每张 picking 原本独立的库存事实
所以你如果要做批量拣货相关定制,最先该守住的,不是界面好不好看,而是:
- 批次准入边界
- 批次执行边界
- 批次与 picking 的责任边界
这三条一旦守住,batch 才会真正提升仓库效率,而不是把复杂度藏起来。
DISCUSSION
评论区