先说最重要的判断
在 Odoo 里,销售单确认之后再取消,通常不是“把状态改一下”这么简单。
因为从源码视角看,一张订单一旦进入 sale,它就可能已经把影响扩散到:
- 发票草稿
- 出库单 / 拣货单
- 库存规则触发链
- 活动日志与责任提醒
所以取消销售单,本质上是在问:
这条已经向下游扩散的商业承诺,现在还能撤回到什么程度?
这和“我在界面上不想要这张单了”完全不是一个层级的问题。
这篇文章主要参考哪些源码
核心参考包括:
/home/ubuntu/odoo-temp/addons/sale/models/sale_order.py/home/ubuntu/odoo-temp/addons/sale_stock/models/sale_order.py
最关键的源码信号有:
action_unlock()只是把locked设回 Falseaction_cancel()会先阻止取消 locked 订单- 基础 sale 的
_action_cancel()会先取消草稿发票,再写state = cancel sale_stock的_action_cancel()会取消未完成 picking,并记录数量下降相关日志
这说明取消逻辑是分层叠加的,不是 UI 按钮的一次性动作。
第一层:为什么要先区分“已确认”和“已锁定”
很多用户会说:
- 这张单已经确认了,怎么还不能取消?
源码给出的答案是:
- 因为确认和锁定不是一回事
- 但锁定会改变取消边界
在 sale_order.py 里:
action_lock()把locked设成 Trueaction_unlock()把它设回 Falseaction_cancel()一开头就检查是否有 locked 订单
只要是 locked,系统就直接报错:
- 不能取消,请先解锁
这层设计在表达:
商业上已成立的订单,不代表可以被任何人随时反向操作。
锁单的意义,就是把“还能不能随手改 / 随手撤”的门槛再抬高一层。
第二层:解锁不是回到草稿,而只是重新开放修改边界
action_unlock() 的实现其实很简单:
- 只是
locked = False
这正说明一个常见误区:
- 很多人把 unlock 理解成“回退流程”
其实不是。
unlock 并不会:
- 把
sale改回draft - 回退已生成的 picking
- 回退已生成的发票
- 自动清除已经触发的下游对象
它只是重新允许你对订单做后续修改或取消。
所以 unlock 是放开编辑边界,不是业务时间倒流。
第三层:基础 sale 的取消逻辑为什么先盯草稿发票
在 sale_order.py 的 _action_cancel() 里,系统先做这件事:
- 找出
invoice_ids里 state 为draft的发票 - 调
button_cancel() - 然后才把订单写成
cancel
这很有代表性。
它说明 Odoo 的标准态度是:
- 还没正式入账的会计对象,可以跟着销售单一起撤回
- 但不是所有发票都能被销售层随意带走
也就是说,这里明确保护的是“还在草稿中的会计草案”,而不是对全部会计后果一概回滚。
这也是为什么已过账发票、已付款发票等场景通常会需要更严肃的后续动作,而不是直接靠销售取消解决。
第四层:为什么 sale_stock 会继续取消未完成 picking
到了 sale_stock,取消逻辑进一步扩展:
- 找到非
done状态的 picking - 执行
action_cancel()
这说明标准设计承认一种合理回撤:
如果仓库履约还没真正完成,那么与这张销售承诺对应的物流动作也应该一起撤回。
但它同时也非常克制:
- 已
done的 picking 不会被直接抹掉
这才符合真实业务。
已经完成的物流动作,不可能因为销售想取消就当没发生过。
第五层:为什么源码还要记录 ordered quantity decrease
sale_stock 在取消时还会构造 documents,并调用 _log_decrease_ordered_quantity(...)。
这一步很多人会忽略,但它很重要。
因为系统不仅在撤对象,还在保留审计线索:
- 哪些履约相关文档受影响
- 谁应该注意到订单数量下降或被取消
- 哪些活动提醒需要重新被看见
这意味着取消不是“悄悄删痕迹”,而是带审计语义的回撤。
第六层:为什么很多“取消不了”本质上不是权限问题,而是下游已经太深
业务现场常会把这类情况归结为:
- 用户权限不够
- 按钮灰了
- 系统太死板
其实标准设计真正想保护的是下游一致性。
只要订单已经连接到:
- 已完成出库
- 已过账发票
- 已发生付款 / 核销
那你想回撤的就不再只是销售单,而是一整串已经生效的业务事实。
这时正确做法通常不是硬取消,而是:
- 先逆向处理库存
- 再逆向处理会计
- 最后让销售对象与现实状态重新一致
所以很多“不能取消”其实是在提醒你:
你现在面对的是完整业务链,不是一个表单状态。
第七层:确认后改单到底该怎么判断
我自己的判断顺序是这样的:
可以相对轻量处理的场景
- 订单已确认但未锁
- 没有已过账发票
- 没有已完成出库
- 下游动作都还停留在可取消状态
这时解锁、修改、取消往往还比较顺。
需要非常谨慎的场景
- 已锁单
- 已完成部分或全部出库
- 已有发票过账
- 甚至已经有付款、核销、对账
这时你要思考的已经不是“能不能取消按钮按下去”,而是如何做一套业务上可审计的逆向链路。
新手最容易误解的 5 件事
1. 以为 unlock 会把订单回退成草稿
不会,它只取消锁定。
2. 以为取消销售单一定会自动回滚所有下游
不会,标准只处理可安全回撤的那部分。
3. 以为已完成出库也能跟着销售取消一起抹掉
标准不会这么做。
4. 以为“取消不了”只是权限问题
很多时候是业务事实已经发生。
5. 以为取消动作不需要留痕
标准 sale_stock 明确会记录相关日志与活动语义。
实战排查顺序
如果用户说“这张销售单怎么取消不了”,建议按下面顺序看:
- 是否
locked - 是否先需要
action_unlock() - 有没有 draft invoice
- 有没有 posted invoice
- picking 是否已有
done - 下游库存 / 会计对象到底处在哪个状态
- 是否应该走逆向业务流程,而不是强行 cancel
一句话记忆法
Odoo 销售单取消不是状态回退,而是对一条已向发票、库存与审计链扩散的商业承诺做可控回撤;unlock 只是重新开放入口,不是让时间倒流。
DISCUSSION
评论区