盘点回退

Odoo 盘点回退为什么不是把数量改回去:action_revert_inventory、反向 move 与审计留痕讲透

很多人以为 Odoo 撤销盘点就是把 quant 再改回原来的数量。但官方源码里,action_revert_inventory 走的是生成反向 inventory move 再 done 的路线。这说明系统要保留的是一条可审计的库存事实,而不是覆盖历史。本文把这层设计讲透。

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

先说结论

在 Odoo 里,撤销一条已经生效的盘点调整,不是把 quant 数字偷偷改回去

它真正做的是:

再生成一笔反向的 inventory move,然后把这笔 move 做掉。

这层设计非常重要。

因为它说明 Odoo 在盘点领域优先保护的是:

  • 事实链;
  • 审计留痕;
  • 可回看性。

而不是“让当前数量看起来对就行”。


为什么“直接改回去”看起来简单,却不符合 Odoo 思路

如果只是为了把库存数字恢复,最省事的办法当然是:

  • 找到受影响 quant;
  • 把 quantity 再写回去。

但这样会带来几个严重问题:

  • 你看不到到底哪次盘点被撤销了;
  • 历史链会被覆盖;
  • 审计时无法区分“原盘点”“回退动作”“后续再次调整”;
  • 很多依赖 move 事实的追溯会失真。

Odoo 显然不想接受这种做法。

所以它选择的是:

  • 用新的库存动作去抵消旧的库存动作。

这和会计里“冲销分录”而不是“改旧分录”是同一种思维。


关键方法:_get_revert_inventory_move_values()

stock_move_line.py 里,_get_revert_inventory_move_values() 把回退动作的核心语义写得很直白。

它会生成一笔新的 move,并且:

  • inventory_name 带上 [reverted]
  • product_uom_qty 用原 quantity;
  • location_idlocation_dest_id 直接反转;
  • is_inventory = True
  • picked = True
  • 同时塞入一条对应 move line。

这几项合在一起,基本已经把设计讲透了:

盘点回退不是删除历史,而是构造一笔方向相反、语义明确的新库存事实。


为什么来源地和目的地要对调

源码里最关键的两行是:

  • location_id = self.location_dest_id
  • location_dest_id = self.location_id

这不是简单字段互换,而是在表达:

  • 原盘点当时把货从 A 调整到了 B;
  • 回退时就要从 B 再走回 A。

也就是说,回退动作沿用的不是“改数量思维”,而是“库存流向反转思维”。

这正是它能留下完整痕迹的原因。


为什么 lot、package、owner 也要原样带上

生成反向 move line 时,源码会把这些维度带上:

  • lot_id
  • package_id
  • result_package_id
  • owner_id

这说明 Odoo 的回退并不是只想把产品数量改回来。

它要回退的是一笔有完整库存维度的盘点事实

如果这些维度丢了,会出现什么问题?

  • lot 追溯失真;
  • package 归属错位;
  • owner 维度库存回不到原语义;
  • 盘点历史无法和原动作精确对应。

所以盘点回退天然是多维度的,不只是数量层面。


action_revert_inventory() 为什么先关掉 inventory_mode

方法开头有一行:

  • self = self.with_context(inventory_mode=False)

这一步很有意思。

它在告诉系统:

  • 当前不是继续做“盘点录入模式”的写数动作;
  • 而是在执行一条正式库存回退流程。

也就是说,回退不是盘点 UI 的附属小操作,而是一次正常库存动作创建流程。

这和后面直接 create(move_vals)_action_done() 是一致的。


它只回退什么样的 move line

action_revert_inventory() 不会对所有 move line 动手。

它会筛:

  • move_line.is_inventory
  • 且数量不为零。

这说明它只回退那些真正属于 inventory adjustment 的库存事实。

如果你选中的不是盘点 move line,系统会直接提示:

  • There are no inventory adjustments to revert.

这层边界也很合理。

因为普通收发货、调拨、退货的回退语义,本来就不应该混进盘点回退入口里。


真正落账的时刻:moves._action_done()

生成 move 后,源码直接:

  • moves._action_done()

这一步说明盘点回退不是“挂一个草稿等你确认”。

它一旦执行,就是:

  • 创建反向库存动作;
  • 立即完成;
  • 让库存、历史、后续追溯一起更新。

这也是为什么它能作为审计链的一部分稳定存在。


返回的不是成功提示,而是“回退后的 move line 列表”

回退成功后,Odoo 返回一个窗口动作,domain 里同时包含:

  • 新生成的 move lines;
  • 原始被回退的 move lines。

这一步特别妙。

因为它等于在 UI 上把“原动作”和“反向动作”并排摆给你看。

这正符合审计和排错场景的真实需求:

  • 不是只告诉你“回退成功了”;
  • 而是让你立即看到哪笔事实被哪笔事实冲掉了

它和直接删除盘点记录最大的区别

如果删记录:

  • 当前数可能对;
  • 但历史会缺洞。

如果用反向 move:

  • 当前数也能回去;
  • 历史上还能看见“谁改了、谁又把它撤回”。

所以从治理角度看,后者几乎总是更稳妥。


实施里最常见的误区

1. 以为盘点回退只是 quant 层面的逆操作

不对。 它是 move / move line 层面的正式反向动作。

2. 以为回退会抹掉原盘点痕迹

也不对。 恰恰相反,它就是为了保留痕迹。

3. 只关注数量,不看 lot / package / owner

这样很容易误判回退是否真的完整。


调试建议:遇到盘点异常先看哪几层

如果有人说“这条盘点回退后数量对了,但追溯怪怪的”,建议按这个顺序查:

  1. 原始 move line 是否 is_inventory=True
  2. 反向 move 的 location_id/location_dest_id 是否确实对调;
  3. lot_id/package_id/owner_id 是否完整跟回;
  4. moves._action_done() 后生成的 move line 是否都落账成功;
  5. 再结合 inventory history 看前后链是否闭合。

一句话总结

Odoo 的 action_revert_inventory() 并不是“把盘点数量改回去”的快捷键。

它做的是:

  • 找到真正的盘点 move line;
  • 构造一笔方向相反、维度完整的新 inventory move;
  • 立即 done;
  • 把原动作和回退动作一起留在历史里。

最准确的理解是:

Odoo 回退盘点靠的是反向库存事实,不是覆盖旧事实。

DISCUSSION

评论区

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