结论先行
出库时,stock.move.line 不是先天就有,而是 Odoo 在给 stock.move 做库存预留(reserve) 时,根据当前可用的 stock.quant 自动拆出来的。
可以把它理解成:
stock.move:计划层,表示“应该搬多少货”stock.move.line:执行层,表示“实际从哪个库位 / 哪个批次 / 哪个包裹 / 哪个货主拿多少货”
所以,stock.move.line 最终如何确定,主要由下面几件事共同决定:
- 当前
stock.move还缺多少数量没有预留 - 源库位里有哪些可用
stock.quant - 仓库或库位配置的移除策略(FIFO / LIFO / closest / least_packages)
- 产品是否启用了批次或序列号跟踪
- 这个 move 是否受上游 move 限制
整体流程:从出库单到 move line
普通出库时,常见链路是这样的:
- 销售单或其他业务单据生成
stock.move - 出库单点击“检查可用量”或在确认时自动触发预留
stock.picking.action_assign()调用各条 move 的_action_assign()- Odoo 去
stock.quant里找“从哪拿货” - 根据 quant 的来源特征自动创建一个或多个
stock.move.line - 用户可在详细作业里微调
- 验证出库时,系统按这些 move line 真正扣减库存
最核心的方法集中在 stock 模块里:
stock.picking.action_assign()stock.move._action_assign()stock.move._update_reserved_quantity_vals()stock.move._prepare_move_line_vals()stock.quant._get_reserve_quantity()stock.quant._gather()
第一层:什么时候开始生成 move line
生成 stock.move.line 最常见的时机,就是 Odoo 给 move 做 库存预留 的时候。
在 stock.picking.action_assign() 里,会取到当前 picking 的 move,然后执行:
moves._action_assign()
接着,在 stock.move._action_assign() 里,系统会判断:
- 当前还缺多少没预留
- 这个 move 是否绕过 reservation
- 是普通 MTS,还是 MTO
- 是否存在上游 move
对于最常见的普通出库,会走到:
move._update_reserved_quantity(need, move.location_id, strict=False)
这句话可以翻译成人话:
“我要从源库位里拿出
need这么多货,帮我找到可预留的库存,并顺便生成对应的stock.move.line。”
第二层:系统按什么规则找货
真正决定“从哪拿货”的,是 stock.quant 相关逻辑。
1)先算还差多少
在 _action_assign() 里,系统会先计算当前 move 还缺多少:
missing_reserved_uom_quantity = move.product_uom_qty - reserved_availability[move]
例如:
- move 计划出库 10
- 已经预留了 4
- 本次还需要再找 6
2)再去 quant 里找库存
接着进入:
stock.quant._get_reserve_quantity(...)
而 _get_reserve_quantity() 又会先调用:
stock.quant._gather(...)
_gather() 会根据这些条件筛选 quant:
product_idlocation_idlot_idpackage_idowner_idstrict是否精确匹配
所以本质上,系统不是先想 move line,而是先想:
“当前这批货,在库存记录里有哪些实际可拿的 quant?”
第三层:quant 的顺序由移除策略决定
这一步非常关键。
在 _gather() 里,Odoo 会根据仓库 / 库位的 Removal Strategy(移除策略) 决定 quant 的取货顺序:
- FIFO:先进先出,按
in_date ASC - LIFO:后进先出,按
in_date DESC - closest:优先最近库位
- least_packages:尽量少拆包
所以你如果问:
为什么系统这次优先从这个 lot / 这个库位出,而不是另一个?
通常先看 移除策略。
第四层:为什么一个 move 会拆成多条 move line
当 quant 选出来后,stock.move._update_reserved_quantity_vals() 会按下面这个组合进行分组:
location_idlot_idpackage_idowner_id
也就是说,只要这些维度不同,就会拆成不同的 stock.move.line。
典型情况:
- 同一产品,但来自两个库位 → 两条 line
- 同一库位,但来自两个批次 → 两条 line
- 同一批次,但来自两个包裹 → 两条 line
这就是为什么一个 stock.move 明明只是一条需求,却可能在详细作业里看到多条操作行。
第五层:move line 上的字段从哪里来
_prepare_move_line_vals() 里能看到 move line 的基础字段来源。
来自 stock.move 的主干字段
move_idproduct_idproduct_uom_idlocation_idlocation_dest_idpicking_idcompany_id
来自 reserved_quant 的执行细节
location_id = reserved_quant.location_idlot_id = reserved_quant.lot_idpackage_id = reserved_quant.package_idowner_id = reserved_quant.owner_id
可以这么记:
- move 决定“这票货要去哪”
- quant 决定“这票货具体从哪里拿”
第六层:普通产品、批次产品、序列号产品的差异
1)不跟踪(tracking = none)
最简单。系统只关心哪里有可用货,能合并就尽量合并。
例如:
- A 库位 6 个
- B 库位 4 个
- 要出 10 个
可能生成:
- line1:A 库位,6
- line2:B 库位,4
2)批次跟踪(tracking = lot)
除了库位,lot 也会进入拆分维度。
例如:
- LOT-A:6 个
- LOT-B:4 个
则会生成:
- line1:LOT-A,6
- line2:LOT-B,4
3)序列号跟踪(tracking = serial)
这是最特殊的一种。
Odoo 会按“一件一条 move line”的方式拆。
比如要出 5 台序列号产品,通常就会拆出 5 条 line,每条数量 1。
第七层:有上游 move 时,会更像“分摊”而不是“随便找货”
如果当前 move 有 move_orig_ids,说明它不是单纯从库存里自由取货,而是受上游流转结果约束。
此时 _action_assign() 会先看:
- 上游已完成 move 带来了哪些
move_line - 同级兄弟 move 已经拿走了多少
然后再算当前 move 还可以分配哪些:
- 库位
- 批次
- 包裹
- 货主
这时候逻辑更像:
“上游送来了什么,我就只能在这些来源里分。”
而不是:
“仓里有什么,我就任意拿什么。”
一个最小例子
假设有一个出库 move:
- 产品:P
- 需求数量:10
- 源库位:
WH/Stock - 目标库位:
Customers - 跟踪:lot
- 移除策略:FIFO
当前 quant:
- quant1:
WH/Stock,LOT-A,6 个 - quant2:
WH/Stock,LOT-B,8 个
系统执行 _action_assign() 后,大致会这样:
- 发现还需要预留 10
_gather()按 FIFO 顺序先取 LOT-A,再取 LOT-B_get_reserve_quantity()决定: - 从 quant1 拿 6 - 从 quant2 拿 4_update_reserved_quantity_vals()按(location, lot, package, owner)拆 line- 最终生成: - line1:LOT-A,6 - line2:LOT-B,4
这就是一个标准的“由 quant 决定 move line”的例子。
开发时最值得盯住的 3 个扩展点
1)想改“从哪挑货”
重点看:
stock.quant._gather()stock.quant._get_reserve_quantity()
适合做:
- 优先某个库位
- 优先某个 lot
- 特殊包裹优先规则
2)想改“拆成几条 line”
重点看:
stock.move._update_reserved_quantity_vals()
适合做:
- 强制更细或更粗的拆分粒度
- 防止某些场景下自动合并
- 引入自定义分组维度
3)想改“line 上带哪些业务字段”
重点看:
stock.move._prepare_move_line_vals()
适合做:
- 自动填充自定义字段
- 记录业务标签
- 根据 quant / move 衍生额外属性
最后一句话总结
出库时 stock.move.line 的确定,本质上是:Odoo 在 _action_assign() 里根据 stock.quant 的可用情况和移除策略做库存预留,再按库位、批次、包裹、货主等维度把结果拆成一条或多条执行明细。
DISCUSSION
评论区