库存出库机制

出库时的 stock.move.line 是如何确定的

用通俗但不失源码细节的方式,讲清楚 Odoo 出库时 stock.move.line 的生成逻辑:从 stock.move、stock.quant、移除策略到 lot/serial 拆行。

发布时间:2026-03-14 17:20 分钟阅读 评论 点赞 收藏 阅读

结论先行

出库时,stock.move.line 不是先天就有,而是 Odoo 在给 stock.move 做库存预留(reserve) 时,根据当前可用的 stock.quant 自动拆出来的。

可以把它理解成:

  • stock.move:计划层,表示“应该搬多少货”
  • stock.move.line:执行层,表示“实际从哪个库位 / 哪个批次 / 哪个包裹 / 哪个货主拿多少货”

所以,stock.move.line 最终如何确定,主要由下面几件事共同决定:

  1. 当前 stock.move 还缺多少数量没有预留
  2. 源库位里有哪些可用 stock.quant
  3. 仓库或库位配置的移除策略(FIFO / LIFO / closest / least_packages)
  4. 产品是否启用了批次或序列号跟踪
  5. 这个 move 是否受上游 move 限制

整体流程:从出库单到 move line

普通出库时,常见链路是这样的:

  1. 销售单或其他业务单据生成 stock.move
  2. 出库单点击“检查可用量”或在确认时自动触发预留
  3. stock.picking.action_assign() 调用各条 move 的 _action_assign()
  4. Odoo 去 stock.quant 里找“从哪拿货”
  5. 根据 quant 的来源特征自动创建一个或多个 stock.move.line
  6. 用户可在详细作业里微调
  7. 验证出库时,系统按这些 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_id
  • location_id
  • lot_id
  • package_id
  • owner_id
  • strict 是否精确匹配

所以本质上,系统不是先想 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_id
  • lot_id
  • package_id
  • owner_id

也就是说,只要这些维度不同,就会拆成不同的 stock.move.line

典型情况:

  • 同一产品,但来自两个库位 → 两条 line
  • 同一库位,但来自两个批次 → 两条 line
  • 同一批次,但来自两个包裹 → 两条 line

这就是为什么一个 stock.move 明明只是一条需求,却可能在详细作业里看到多条操作行。


第五层:move line 上的字段从哪里来

_prepare_move_line_vals() 里能看到 move line 的基础字段来源。

来自 stock.move 的主干字段

  • move_id
  • product_id
  • product_uom_id
  • location_id
  • location_dest_id
  • picking_id
  • company_id

来自 reserved_quant 的执行细节

  • location_id = reserved_quant.location_id
  • lot_id = reserved_quant.lot_id
  • package_id = reserved_quant.package_id
  • owner_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() 后,大致会这样:

  1. 发现还需要预留 10
  2. _gather() 按 FIFO 顺序先取 LOT-A,再取 LOT-B
  3. _get_reserve_quantity() 决定: - 从 quant1 拿 6 - 从 quant2 拿 4
  4. _update_reserved_quantity_vals()(location, lot, package, owner) 拆 line
  5. 最终生成: - 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

评论区

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