Move Line 改写

Odoo 为什么改一条已预留 move line 会牵动整片库存:lot、package、owner 改写与 quant 同步逻辑讲透

很多人以为改 move line 只是改界面上的一行明细,但只要那行已经预留过库存,Odoo 实际上会先释放旧预留,再按新特征重抢 quant,甚至影响后续链路。本文把已预留 move line 的改写语义和排错顺序讲清楚。

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

很多实施和二开同学第一次碰库存明细改写,都会低估 stock.move.line 的重量。

界面上看,它只是一行:

  • 换个 lot;
  • 改个 owner;
  • 从一个 package 改到另一个 package;
  • 把数量从 5 改成 3。

看起来像改表格,实际上如果这条 line 已经参与了 reservation,Odoo 做的根本不是“单纯 update 一行记录”,而是一套 先拆旧库存关系、再重建新库存关系 的同步动作。

这也是很多人遇到以下问题的根因:

  • 改完 lot 以后预留数量忽然变少;
  • 改完 package 以后单据状态变成 partially_available;
  • 改 done move line 居然影响到下游单据;
  • 自定义 write() 以为只是补字段,结果把 quant 弄乱了。

先建立一个正确认识

在 Odoo 里,已预留 move line 不是“描述库存”的文本。 它更像一份 当前库存占用关系的声明

这份声明至少包含这些关键维度:

  • product
  • location
  • lot
  • package
  • owner
  • quantity

只要这些维度变了,系统就不能假装“还是原来那笔占用”。

所以源码在 stock.move.line.write() 里干脆把这件事处理成:

  1. 判断你是不是改了 reservation 相关触发字段;
  2. 如果是,就把旧特征对应的预留先释放;
  3. 再尝试按新特征重新预留;
  4. 如果新特征下可用量不足,只保留最大可能值;
  5. 必要时重算 move 状态。

这套设计,比“硬改字段”安全得多。

哪些字段会触发真正的库存同步

源码里这组字段尤其关键:

  • location_id
  • location_dest_id
  • lot_id
  • package_id
  • result_package_id
  • owner_id
  • product_uom_id
  • quantity

但要注意,result_package_id 的语义跟别的字段不完全一样。

为什么 result_package_id 相对特殊

package_id 表示货现在从哪个包里来, result_package_id 表示这次操作后要进哪个包。

所以改 result_package_id 更多是在改“目的包装结果”, 而改 lot_id / package_id / owner_id / location_id 则是在改“源侧占用条件”。

源码里专门把它们区分开,就是因为:

  • 改源侧特征,往往必须释放并重建 reservation;
  • 改结果包装,不一定等价于改源侧占用。

核心动作:先 unreserve,再 reserve

这是这篇最重要的一句话。

当一条已预留 move line 被改写时,Odoo 的思路不是“尝试原地修补 quant”,而是:

旧关系先清干净,新关系再按新条件重建。

源码里会先调用同步逻辑,把旧的 quantity_product_uom 从旧特征对应的 quant reservation 中扣掉;然后再按新的 lot / package / owner / location 去做一次 reservation。

这一步非常关键,因为库存系统最怕的就是“表面字段改了,底层 reservation 没跟着迁移”。

一旦出现那种半改半没改的状态,你看到的就会是:

  • move line 上写着 lot B;
  • quant 却还在 lot A 上挂着 reserved;
  • 后面任何 assign / validate 都会越来越诡异。

Odoo 正是为了避免这种裂缝,才把写入做得很重。

为什么改完数量,预留可能反而减少

很多人会疑惑:

  • 我只是把数量改成了 10;
  • 为什么最后 move line 上只剩 6;
  • 为什么状态也从 assigned 变 partial?

原因是源码并不承诺“你改成多少,就一定抢到多少”。

它承诺的是:

在新特征条件下,尽量保留最大可预留数量。

举个典型例子:

  • 原来这条 line 预留的是无 lot 库存;
  • 你把它改成某个指定 lot;
  • 但该 lot 真正可用只有 6。

那么系统就会:

  • 释放原来那笔更宽松条件下的 reservation;
  • 再尝试按指定 lot 重新预留;
  • 最终只能拿回 6,而不是你表面写的 10。

这不是 write 丢数据,而是你把约束收紧以后,可占用池变小了。

done move line 为什么更危险

源码里还有一层很多人没意识到:

如果你改的是已经 done 的 move line,影响不只在当前单据。

为什么? 因为已完成 move line 不只是“预留过”,它已经是实际库存流转事实的一部分。

这时系统会:

  1. 撤销原先对目的库位 / 来源库位造成的 quant 影响;
  2. 按新值重新同步可用量;
  3. 找出相关的下游 move_dest_ids
  4. 让后续 move 重新面对真实的可用情况。

换句话说,改 done line,本质上是在改链路历史。

所以这类改写很容易牵动:

  • 下游 move 的 reserved availability;
  • picking 状态;
  • 链式补货 / 履约判断;
  • 追溯报表的一致性。

如果你在二开里直接批量写 done move line,又没理解这层语义,后果通常比你想的严重得多。

_synchronize_quant() 才是真正的底层铰链

源码里最值得盯的,不只是 write(),而是它最终借力的 _synchronize_quant()

这个方法做了两件核心事:

  • action="available" 时,改可用量;
  • action="reserved" 时,改预留量。

同时它还会把:

  • lot
  • package
  • owner

这些维度一起带下去。

也就是说,move line 的“改一行”之所以很重,是因为它最后会落到 quant 粒度的真实同步,而不是停留在文档层。

lot / owner / package 为什么是同一类问题

很多人习惯把它们分开理解:

  • lot 是追溯;
  • owner 是寄售;
  • package 是装箱。

业务语义上当然不同,但在 reservation 粒度上,它们是一类问题:

它们都在缩小“哪一份库存才算这条 line 可占用的库存集合”。

一旦你改这些字段,就等于在问系统:

“请不要再按原来的那批货算了,请改按另一组更精确的库存边界来占用。”

那当然不可能只靠一条 SQL update 结束。

实战里最容易犯的二开错误

1)把 move line 当成普通明细表批量 update

这是最常见的坑。

如果你的脚本跳过了 ORM 语义,只在数据库层硬写 lot / package / owner,前端也许看着变了,但 quant 层基本注定乱。

2)在 write() 里补字段时忽略库存语义

有些扩展会重载 write(),以为只是补个 trace 或同步外部系统。问题是,只要你顺手把 reservation 相关字段也碰了,系统就不是在“补信息”,而是在“重建库存关系”。

3)改 done move line 却没意识到会波及下游

done 不代表安全,反而代表它已经进入因果链。历史被改,后续当然要重算。

推荐的排错顺序

如果你遇到“改完 line 后库存更乱了”,建议按这个顺序看:

  1. 改的是不是 lot_id / package_id / owner_id / location_id / quantity 这类触发字段;
  2. 这条 line 改之前是否已经 reserved;
  3. 改之后的新特征下,quant 是否真的有足够可用量;
  4. 这条 line 是否已经 done;
  5. 它有没有 move_dest_ids 或链式后续动作;
  6. 自定义代码是否绕过了 ORM 同步路径。

这套顺序能帮你把“表层字段变化”和“底层库存关系变化”分开看。

最后一句总结

在 Odoo 里,已预留 move line 不是一行可随便修的表单数据。 它是 库存占用关系在文档层的外显

所以你一旦改它,系统就必须去问:

  • 旧关系要不要拆;
  • 新关系能不能重建;
  • quant 要不要同步;
  • 下游链路要不要跟着重算。

理解了这一点,你就会明白:

Odoo 对 move line 改写之所以“反应很大”,不是因为它脆弱,而是因为它在认真维护库存事实的一致性。

DISCUSSION

评论区

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