先说结论
在 Odoo 里,销售单确认后不是简单分成“能改”和“不能改”两类。
标准逻辑其实分了三层:
- 订单层:订单是否锁定,决定你还能不能继续做很多后续动作。
- 字段层:有些字段确认后还能改,有些字段会被明确拦住。
- 订单行层:哪怕整张单已经解锁,某些订单行只要出现已交付或已开票,就会变成不可随意编辑。
所以真正该问的不是:
销售确认后到底能不能改?
而是:
哪一层对象、哪个字段、处在什么下游状态下还能改?
这篇文章主要参考哪些源码
核心参考文件包括:
/home/ubuntu/odoo-temp/addons/sale/models/sale_order.py/home/ubuntu/odoo-temp/addons/sale/models/sale_order_line.py/home/ubuntu/odoo-temp/addons/sale/wizard/res_config_settings.py
最关键的源码信号有:
group_auto_done_setting对应 “Lock Confirmed Sales” 设置action_confirm()在确认后会根据_should_be_locked()决定是否自动锁单write()明确禁止修改已确认订单的pricelist_idsale.order.line的_compute_product_updatable()会根据locked、qty_delivered、qty_invoiced决定产品是否还能改product_uom_readonly在sale/cancel状态下会被设成只读
这些点放在一起,才能看清 Odoo 对“确认后改单”的真实设计。
第一层:锁单不是确认本身,而是确认后的第二道闸门
在 res.config.settings 里,group_auto_done_setting 就是销售设置中的 Lock Confirmed Sales。
action_confirm() 完成后,会继续调用:
_should_be_locked()action_lock()
也就是说,标准 Odoo 的意思不是“确认必然锁单”,而是:
- 先确认订单进入
sale - 如果启用了自动锁单特性,再把
locked设成True
这就解释了为什么很多项目现场会出现两种完全不同的感受:
- 有的公司觉得“确认后几乎什么都改不了”
- 有的公司觉得“确认后仍然还能继续调整”
差异往往不在于你是不是进入了 sale,而在于有没有启用自动锁单,以及有没有产生更深的下游事实。
第二层:解锁不是恢复草稿,而是局部恢复修改能力
action_unlock() 的实现非常短:
- 只是把
locked = False
这意味着它并不会:
- 把订单状态从
sale改回draft - 自动撤销拣货
- 自动撤销发票
- 自动让所有字段重新完全可编辑
它真正做的是:
把订单从“锁定的确认单”变回“未锁定的确认单”。
注意这个差别非常关键。
很多顾问把 unlock 当成“回到还能像报价一样编辑”。 但源码根本不是这个语义。 它只是在订单层放开一道门,后面还要继续接受字段层和订单行层的限制。
第三层:已确认订单里,价目表就是被明确禁止修改的
sale.order.write() 里有一条非常直接的限制:
- 只要
vals里包含pricelist_id - 并且当前订单里存在
state == 'sale' - 就抛错:不能修改已确认订单的价目表
这条规则很有代表性。
因为它说明 Odoo 并没有采用“确认后全部字段冻结”的粗暴方案,而是采用:
- 某些字段仍可继续调整
- 某些字段一旦确认就不允许再变
为什么价目表被单独保护?
因为 pricelist 不是普通展示字段。它会影响:
- 新增行如何定价
- 折扣如何展示
- 后续重算价格的基准
- 税前后价格解释口径
如果确认后还能随便切 pricelist,整个报价承诺的价格基础就会漂移。
所以 Odoo 在模型层直接把它拦住,而不是只靠界面隐藏按钮。
第四层:订单行的“可编辑”比订单本身更严格
sale.order.line 里有个特别关键的计算字段:product_updatable。
它默认是 True,但以下场景会被设成 False:
- 行是 down payment
- 订单状态是
cancel - 订单状态是
sale,且满足以下任一条件: - 订单已锁定
- 行已经有
qty_invoiced > 0 - 行已经有
qty_delivered > 0
这条逻辑揭示了一个核心原则:
确认后的订单行能不能改,不只看订单是否解锁,还要看这条行是否已经开始兑现。
换句话说:
- 没交付、没开票、未锁定的确认行,仍可能可改
- 已交付或已开票的确认行,即便订单已经解锁,也不该继续改产品本身
这才符合业务现实。
因为一旦某行已经参与履约或结算,改产品不再是“修正文案”,而是在改已经发生过的商业事实。
第五层:产品可改,不代表单位与数量也能无限制改
在同一个 sale.order.line 里,product_uom_readonly 会在记录已保存且状态处于:
salecancel
时变成只读。
这说明即使某些情况下产品仍可更新,单位字段也不等价于完全自由。
原因很简单:
- 单位直接影响数量解释
- 数量影响交付数量、待开票数量、金额换算
- 一旦确认后履约链开始运转,单位变化往往比改备注危险得多
所以 Odoo 的限制不是随机的,而是在保护计量口径的一致性。
第六层:确认后的“可编辑性”本质上和 qty_delivered / qty_invoiced 绑定
很多新手会把 editability 只理解成前端表单状态。
但在源码里,最硬的边界来自两个数量:
qty_deliveredqty_invoiced
只要其中任一个大于 0,行级可编辑性就会明显收紧。
这背后的业务逻辑非常清楚:
已交付
说明这条销售承诺已经开始对应物流、工时或费用事实。
已开票
说明这条销售承诺已经开始对应会计与收款语义。
所以一条确认行在“尚未兑现”与“已经兑现一部分”之间,系统给出的可改边界完全不同。
这也是为什么现场经常会出现一种错觉:
- 同一张订单,有的行还能改,有的行已经灰掉
这不是系统不一致,而是系统在按履约深度做差异化保护。
第七层:确认后为什么不是所有东西都要冻结
标准 Odoo 没有把确认后的订单做成彻底只读,是有意为之。
因为真实业务里,确认后仍然常常需要调整:
- 客户参考号
- 交期承诺
- 内部备注
- 某些尚未交付的新加行
- 某些价格重算后的说明动作
如果把确认后完全冻结,很多实施场景会非常僵硬。
所以 Odoo 采用的是一种更实务化的策略:
- 允许一定范围内的后确认调整
- 但把会破坏价格承诺、履约事实、开票追踪的修改点收紧
这是一种“允许业务继续推进,但不允许历史失真”的设计。
新手最容易误解的 5 件事
1. 解锁后就等于恢复成报价单
不是。解锁只移除 locked,不回退订单生命周期。
2. 只要订单没锁,订单行就一定能随便改
不是。qty_delivered 和 qty_invoiced 也会让行不可改。
3. 已确认订单可以通过换 pricelist 整体重算
标准源码明确禁止这么做。
4. 可编辑性只是前端视图控制
不是。很多关键边界在模型层就直接拦住了。
5. 订单是否能改只取决于订单头
不是。Odoo 明显区分订单头与订单行,而且行级限制更细。
实战里我建议这样判断确认后改单
我通常按下面顺序判断:
第一问:订单有没有锁
- 锁了,先判断是否应该解锁
- 没锁,再继续看更细的边界
第二问:要改的是哪个字段
- 若是
pricelist_id这种模型层明确禁改字段,就别想着走标准修改 - 若是备注、参考号、承诺日期,再看项目场景是否允许
第三问:要改的是哪条行
- 还没交付、没开票、未锁定的行,通常更有调整空间
- 已交付或已开票的行,要优先考虑逆向业务处理,而不是直接改原行
第四问:改动会不会让下游失真
如果会影响:
- 出库口径
- 开票追踪
- 毛利分析
- 客户已确认价格
那就不要把“可编辑”误当成“应该编辑”。
最后一句话
Odoo 对 confirmed order 的 editability 设计,其实非常务实:
确认后不是一刀切锁死,而是按锁单状态、字段风险、履约深度来分层收紧。
所以真正成熟的实施方式,不是追问“为什么这里灰了”,而是先搞清楚:
- 这是订单层限制,
- 还是字段层限制,
- 还是订单行已经开始兑现后的保护机制。
只有这样,你才知道下一步应该是解锁、补充新行、走逆向流程,还是干脆不要改原单。
DISCUSSION
评论区