先说结论
Odoo 销售里的 Upselling Opportunity,和很多人口中的“订阅续费”“续签报价”“合同加购”,并不是一回事。
从 sale 官方源码看,Upsell 的核心语义其实非常克制:
- 它首先是
invoice_status的一种状态 - 它只在特定条件下成立:按 ordered quantity 开票、实际交付超过原始订购量
- 它进一步触发一条
mail.activity,提醒销售去处理
也就是说,核心销售模块真正提供的是:
一个‘这张已成交销售单存在追加收费机会’的信号系统。
它并没有直接提供:
- 完整的订阅续费对象模型
- 自动 renewal contract 链路
- 周期账期重建与续费计划排程
所以如果你把 sales 里的 upsell 当成 renewal engine,会对系统职责产生根本误读。
源码里的 Upsell,到底是什么
关键位置在:
sale/models/sale_order_line.pysale/models/sale_order.py
行级条件
sale.order.line._compute_invoice_status() 里,只有在这些条件同时成立时,行才会进入 upselling:
- 行状态是
sale - 产品
invoice_policy == 'order' qty_to_invoice == 0qty_delivered > product_uom_qty- 并且不是普通“还有待开票”的情况
这段逻辑表达得很精准:
Upsell 不是“可能还能卖点什么”,而是“这条线已经发生超出原始订购量的实际履约,但当前不会自动再开票”。
典型场景就是:
- 服务按订购数量先开票
- 项目实际投入超出合同
- 团队暂时没打算直接把超出部分自动计费
- 系统于是给你一个 upselling opportunity
这和“订阅到期该续费了”根本不是同一类事件。
订单级 Upsell 只是行级状态的汇总,不是独立业务对象
sale.order._compute_invoice_status() 会把订单状态汇总成:
to invoiceinvoicedupsellingno
其中 upselling 的成立条件是:
- 订单行状态里不存在还要正常开票的主线
- 所有 relevant line 都已经
invoiced或upselling
这说明订单级 invoice_status='upselling' 只是:
- 对订单行状态做汇总
- 给列表、筛选和操作界面一个总标签
它不是一个新的续费实体,也不是新的订单版本。
所以你不能把这个状态理解成:
- 系统自动帮你生成 renewal quotation 了
- 或者下一期订阅已经被建模好了
它只是提醒你这张已成交订单还有额外商业化空间。
Odoo 核心销售模块对“续费”的真正态度:给信号,不接管全部生命周期
更有意思的是 _compute_field_value('invoice_status') 的覆写。
当订单从非 upselling 切到 upselling 时,系统会:
- 找到负责人
order.user_id或partner_id.user_id - 调用
_create_upsell_activity() - 创建一条 TODO 活动:
Upsell <order> for customer <customer>
这条设计非常值得玩味。
因为它没有做下面这些更“重”的动作:
- 不会自动复制原订单
- 不会自动起一张 renewal quotation
- 不会自动重置订购周期
- 不会自动写入新的 recurring contract
它只发出一条销售待办。
这说明在核心 sale 里,Odoo 对 upsell 的定位很明确:
这是销售动作提醒,不是续费主引擎。
为什么这和 subscription / renewal 场景刚好构成边界
用户在实施中最常见的误会是:
- 订阅客户超用了服务
- 销售单变成 upselling
- 那系统应该自动把它接成 renewal 或追加周期账单
但从当前公开 sale 源码看,核心销售模块只知道:
- 某些行超量交付了
- 这些超量在当前计费策略下没有自动生成新 invoice
- 应该提醒销售去跟进
而“订阅续费”真正需要解决的却是另外一组问题:
- 新周期从什么时候开始
- 续费是同价续签、升配还是降配
- 旧合同与新合同如何衔接
- 发票周期和服务周期如何滚动
- 是否要重新出报价、重新签署、重新授权支付
这些都不是 sale.order.invoice_status = 'upselling' 能承载的。
所以正确边界是:
Upsell
- 处理 超量履约后的追加商业机会
- 核心动作是提醒销售跟进
Renewal
- 处理 下一周期商业关系如何续接
- 需要更完整的订阅/合同/周期建模
两者可以协作,但不能互相替代。
sale.order 在 renewal 场景里到底应该扮演什么角色
如果把职责摆正,sale.order 在订阅/续费类场景里最适合扮演的是:
1. 商业确认单据
当你决定:
- 升配
- 降配
- 续签
- 加购 seats / 工时 / 服务包
新的商业承诺仍然很适合落到一张 sales order / quotation 上。
2. Upsell 触发入口
现有订单在履约过程中触发 upselling,会提醒销售:
- 该和客户谈扩容了
- 该把超量服务转成正式商业承诺了
3. 后续 renewal 的谈判载体
真正的 renewal 策略可能来自订阅模块、实施规则或 CRM 流程,但最后形成正式报价时,sale order 仍然是最自然的承载体。
也就是说:
sale.order 很适合承接 renewal 的商业结果,但不应该被误认为 renewal 生命周期本身。
一个非常关键但经常被忽略的点:Upsell 依赖 invoice policy
源码把 upsell 绑得很死:
product_id.invoice_policy == 'order'
这意味着如果你的产品按 delivered quantities 开票,那么超出原订购量本来就会通过正常 qty_to_invoice 进入常规开票,不需要再走 upsell 提醒逻辑。
所以能进入 upselling 的,通常是这种业务设定:
- 先按订购量或合约量开票
- 但实际交付可能超出
- 团队不想系统自动强行把超出部分转成新发票
- 而是让销售先判断要不要追收、续签、扩容
这本质上是一种人工商业决策保留机制。
也正因为如此,它天然更像“销售提醒”,而不是“订阅引擎”。
实战里最容易犯的 4 个错
1. 把 upselling 菜单当成 renewal 列表
那里面聚合的是“超量但尚未转化的销售机会”,不是“即将到期的续费合同”。
2. 看到 upsell 就期待系统自动生成下一期账单
核心 sale 并不会这么做,它只会安排 activity。
3. 忽略负责人分配
如果订单没有 user_id,系统会退回 partner_id.user_id。负责人没配好,upsell 提醒就会落空或落错人。
4. 用错误的 invoice policy 期待 upsell
按 delivered 开票的产品,本来就更可能走正常开票而不是 upselling 状态。
一句话总结
Odoo 核心销售模块里的 Upselling Opportunity,本质上是:
- 由
sale.order.line超量履约触发的invoice_status - 由
sale.order汇总成订单级状态 - 再通过
mail.activity把“该跟客户谈扩容/补差/追加收费了”这件事提醒给销售
它可以和 subscription renewal 协作,但它自己并不是 renewal engine。
所以最稳的理解应该是:
Upsell 负责发出商业化信号,renewal 负责管理下一周期关系,而 sale.order 负责承接最终谈成的商业结果。
DISCUSSION
评论区