先说结论
在 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_id和location_dest_id直接反转;is_inventory = True;picked = True;- 同时塞入一条对应 move line。
这几项合在一起,基本已经把设计讲透了:
盘点回退不是删除历史,而是构造一笔方向相反、语义明确的新库存事实。
为什么来源地和目的地要对调
源码里最关键的两行是:
location_id = self.location_dest_idlocation_dest_id = self.location_id
这不是简单字段互换,而是在表达:
- 原盘点当时把货从 A 调整到了 B;
- 回退时就要从 B 再走回 A。
也就是说,回退动作沿用的不是“改数量思维”,而是“库存流向反转思维”。
这正是它能留下完整痕迹的原因。
为什么 lot、package、owner 也要原样带上
生成反向 move line 时,源码会把这些维度带上:
lot_idpackage_idresult_package_idowner_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
这样很容易误判回退是否真的完整。
调试建议:遇到盘点异常先看哪几层
如果有人说“这条盘点回退后数量对了,但追溯怪怪的”,建议按这个顺序查:
- 原始 move line 是否
is_inventory=True; - 反向 move 的
location_id/location_dest_id是否确实对调; lot_id/package_id/owner_id是否完整跟回;moves._action_done()后生成的 move line 是否都落账成功;- 再结合 inventory history 看前后链是否闭合。
一句话总结
Odoo 的 action_revert_inventory() 并不是“把盘点数量改回去”的快捷键。
它做的是:
- 找到真正的盘点 move line;
- 构造一笔方向相反、维度完整的新 inventory move;
- 立即 done;
- 把原动作和回退动作一起留在历史里。
最准确的理解是:
Odoo 回退盘点靠的是反向库存事实,不是覆盖旧事实。
DISCUSSION
评论区