CRM 企业版

Odoo 企业版 CRM 为什么订阅报价不会把商机金额随便改来改去:同币种门槛、只增不减、默认月计划与跟踪可见性讲透

很多人以为 Odoo 企业版里,订阅报价一旦绑定商机,确认报价时系统就会把 expected revenue 和 recurring revenue 直接同步过去。`crm_sale_subscription` 真正做的是一套更保守的桥接:只在同币种前提下按更大值上调收入,必要时补默认月计划,还会按权限隐藏 recurring revenue 的跟踪明细。

CRM 企业
进阶 开发者 3 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

很多团队第一次把 订阅报价CRM 商机 连起来时,脑子里的模型都很直白:

  • 报价上有一次性金额;
  • 报价上也有订阅型金额;
  • 报价确认后,把这两个数同步回商机;
  • 后面销售看商机金额就行。

如果只是从产品界面猜,这种理解很自然。

但企业版 enterprise/crm_sale_subscription 的源码并没有采用“报价说了算,商机照单全收”的激进做法。

它真正实现的是一套非常克制的收入桥接规则:

只有当销售订单的金额比商机当前值更大、而且币种与公司币种一致时,系统才会上调商机的 expected revenue / recurring revenue;如果商机还没有 recurring plan,还会顺手补成默认月计划;同时,是否能在跟踪消息里看到 recurring revenue 变化,还取决于用户权限。

这意味着企业版这里要解决的核心问题并不是:

“确认订阅报价后,怎么把金额抄回商机?”

而是:

“确认订阅报价后,如何在不破坏 CRM 预测口径的前提下,让商机收入向更可信的成交事实靠拢?”

这才是这个小桥接模块真正值得读的地方。


一、这个模块虽然小,但它管的是“商机收入升级规则”

crm_sale_subscription 的 manifest 很短,依赖也很少:

  • crm
  • sale_subscription

这说明它不是重做一套 CRM,也不是重做订阅引擎。

它只是在两条已有业务链之间补一座桥:

  1. CRM 侧已经有商机、expected revenue、recurring revenue;
  2. Subscription / Sales 侧已经有销售订单、一次性金额、月度 recurring 金额。

这个桥接发生在 models/crm_lead.py 里最关键的方法:

  • _update_revenues_from_so(self, order)

光看方法名就能看出官方态度非常克制:

  • 不是“用订单覆盖商机收入”;
  • 不是“订单确认后重算全部 CRM 金额”;
  • 而是“从 sale order 更新 revenues”。

换句话说,订单是更强的业务信号,但仍然只是对商机收入做有限更新,而不是完全接管 CRM 的预测语义。


二、第一道边界:不是所有确认后的订单金额都能改写商机

源码里最先值得注意的,不是“怎么写入”,而是“什么时候根本不写入”。

1)一次性金额的更新条件

对于 expected_revenue,模块检查的是:

  • opportunity.expected_revenue < order.non_recurring_total
  • order.currency_id == opportunity.company_id.currency_id

只有两者同时成立,系统才会把商机的 expected_revenue 更新为订单的 non_recurring_total

这句话翻成业务语言就是:

只有当订单一次性金额更大,而且订单币种正好等于商机所属公司的公司币种时,才允许把商机 expected revenue 往上推。

2)订阅金额的更新条件

对于 recurring_revenue_monthly,条件也是同样的结构:

  • opportunity.recurring_revenue_monthly < order.recurring_monthly
  • order.currency_id == opportunity.company_id.currency_id

也就是说,订阅部分同样不是看“有没有 recurring amount”,而是看“月度 recurring amount 是否更大,且币种是否安全可比较”。

这点非常重要。

因为它说明企业版这里默认并不相信“任何订单金额都可以拿来刷新 CRM”。

官方先卡了两道门:

  1. 可比较性:币种得一致;
  2. 方向性:只能上调,不能下压。

三、为什么官方要加“同币种门槛”

很多人第一次看到这段逻辑,会觉得奇怪:

  • 订单有币种;
  • 商机也有公司;
  • 系统会换汇不就好了?

但源码在这里明确没有做自动换汇折算,而是直接要求:

  • order.currency_id == opportunity.company_id.currency_id

也就是说,只有销售订单币种与公司本位币一致时,这张订单才有资格直接推动商机收入。

这背后的设计非常务实。

原因一:CRM 收入字段首先是经营口径,不只是界面数字

商机上的 expected_revenuerecurring_revenue,后面会进入:

  • 漏斗排序;
  • 预测报表;
  • 负责人视图;
  • 概率加权分析。

如果这里轻易引入跨币种自动折算,后面所有比较都要承担汇率时点、取值来源、重算时机这些复杂性。

crm_sale_subscription 这个桥接模块的职责显然不想膨胀到这个程度。

原因二:订单确认是强信号,但不是汇率治理引擎

这个模块的重点,是把订阅订单的“更可信收入信号”传给商机。

它不是财务模块,也不是汇率模块。

所以官方宁可采用一个保守策略:

只有在币种完全一致、无需解释换算来源时,才把订单金额拿来升级 CRM 收入口径。

这会牺牲一点“自动化的完整性”,但换来更稳定、更可解释的预测口径。

这其实很像 Odoo 企业版一贯的设计哲学:

在经营关键字段上,宁可少做一点自动推断,也不要做一堆会带来歧义的“智能同步”。


四、第二道边界:收入只会被抬高,不会被压低

这段模块最值得管理层和实施顾问注意的地方,是它的更新方向。

源码用的是“小于才更新”:

  • 小于 non_recurring_total 才改 expected_revenue
  • 小于 order.recurring_monthly 才改 recurring 相关值

这意味着:

订单确认后,对商机收入的影响是“只增不减”。

这不是技术偷懒,而是明确的业务判断

想象一下这两种情况:

  1. 商机原本由销售估了一个更高金额,后来某一张订单暂时只签了一部分;
  2. 同一个商机可能挂接不止一张销售订单,某次确认的是较小订单,另一次才是更完整方案。

如果每次确认订单都去覆盖商机收入,就会出现非常难看的后果:

  • 商机金额忽高忽低;
  • 漏斗预测被较小订单“误伤”;
  • 销售历史判断被一次局部成交回写冲掉。

企业版这里显然选择了另一种原则:

销售订单可以证明“实际价值至少已经到这里”,但不能轻易证明“商机整体价值只能剩这么多”。

因此,订单确认被视为 下限被抬高 的证据,而不是 上限被压低 的命令。

这个设计非常适合复杂销售现实。

因为真实 B2B 订阅场景里,报价、增购、分期签约、补充订单都可能分多次发生。让 CRM 金额只向更大成交事实靠拢,通常比“跟着每一张订单上下波动”更符合经营直觉。


五、为什么 recurring revenue 更新时还会顺手补默认月计划

源码里有个很容易被忽略、但非常关键的细节:

order.recurring_monthly 足以推动商机更新时,如果商机还没有 recurring_plan,系统会先补:

  • crm.crm_recurring_plan_monthly

然后才写:

  • opportunity.recurring_revenue = order.recurring_monthly * (opportunity.recurring_plan.number_of_months or 1)

这说明官方不是把 order.recurring_monthly 直接塞进 recurring_revenue

它先确保商机上存在一个 周期语义,再把月收入反推成总 recurring revenue。

为什么这一步不能省

因为在 CRM 的数据模型里:

  • recurring_revenue 表达的不是“天然就是月收入”;
  • 真正的月口径要靠 recurring_plan 去解释。

如果商机没有 recurring plan,系统就无法说明:

  • 这个 recurring revenue 是按 1 个月口径;
  • 还是 3 个月;
  • 还是 12 个月。

而订单侧给你的却是一个明确的 order.recurring_monthly

所以企业版这里的处理逻辑是:

  1. 订单侧先给出月度 recurring 信号;
  2. CRM 侧如果还缺少 plan,就补默认 monthly;
  3. 再把月度值换算成 CRM 自己需要保存的 recurring revenue。

这一步其实是在做 语义补全,不是简单赋值。

一句话总结:

订单告诉你“每月多少钱”,CRM 还需要一张“这是多少钱周期单位”的说明书。没有说明书,就先补默认月计划。


六、为什么写入的是 recurring_revenue,判断的却是 recurring_revenue_monthly

这也是一个很值得读的细节。

源码比较大小时,用的是:

  • opportunity.recurring_revenue_monthly
  • order.recurring_monthly

但真正写回商机时,写的是:

  • opportunity.recurring_revenue = order.recurring_monthly * number_of_months

这说明官方把“比较口径”和“存储口径”分开了。

比较为什么按 monthly 做

因为只有月口径,才适合和销售订单的 recurring monthly 做直接比较。

不然你拿:

  • 一个按 12 个月 plan 保存的 recurring_revenue
  • 去和一个订单上的 monthly 值比较

两边单位都不一致,结论肯定不靠谱。

存储为什么仍写 recurring_revenue

因为 CRM 模型原本就围绕:

  • recurring_revenue
  • recurring_plan

去表达 recurring deal。

所以企业版桥接模块不是重塑 CRM 数据结构,而是:

  • 用 monthly 口径做安全比较;
  • 再把结果折回 CRM 原生字段体系。

这也解释了为什么默认 monthly plan 的回填如此重要。

没有它,系统就没法把订单月收入正确落回 CRM 的 recurring revenue 体系。


七、跟踪消息为什么还要按权限过滤 recurring revenue

这个模块另一个特别有意思的地方,不在金额更新本身,而在跟踪显示上。

_track_filter_for_display(self, tracking_values) 做了一个额外过滤:

  • 如果当前用户没有 crm.group_use_recurring_revenues
  • 就把 recurring_revenue 的 tracking value 从显示结果里过滤掉

这说明企业版这里考虑的不是“字段能不能改”,而是更细的另一个问题:

字段改了以后,谁应该在 chatter / 跟踪视图里看到这条变化?

这背后的设计含义很强

很多系统做权限时,只管表单字段是否可见、是否可编辑。

但 Odoo 在这里进一步考虑到:

  • 即使字段本身最终被更新了;
  • 也不代表所有看到这条记录的人,都应该从跟踪消息里读到 recurring revenue 的变化历史。

这类设计特别适合企业销售组织。

因为 recurring revenue 往往不只是普通金额字段,它还可能承载:

  • 续费经营目标;
  • 订阅业务体量;
  • 更敏感的经营预测信号。

所以官方把它做成:

  • 数据层可以更新
  • 展示层再按 group 做可见性裁剪

这是一种比“字段完全隐藏/完全公开”更细腻的治理方式。


八、测试文件其实把这套边界写得很清楚

tests/test_sale_crm.py 里,官方测试已经把模块意图说得非常直白。

测试重点验证了几件事:

1)不同币种时,不更新商机收入

测试先构造两张订单:

  • 一张使用与 lead 不同币种的 pricelist;
  • 一张使用相同币种的 pricelist。

然后确认第一张订单后,断言:

  • expected_revenue 仍然是 0
  • recurring_revenue 仍然是 0

这说明“同币种门槛”不是偶然代码写法,而是明确的产品边界。

2)没有 recurring plan 时,会自动回填月计划

测试确认同币种订单后,断言:

  • lead_1.recurring_plan.id == crm.crm_recurring_plan_monthly

这直接证明:

订单月收入回写 CRM 时,官方默认先把 recurring plan 补成 monthly。

3)已有 recurring plan 时,会按该 plan 的月数折算

测试里又人为把 lead 改成一个 12 个月 plan,然后再次确认订单,最后断言:

  • expected_revenue == 200
  • recurring_revenue == 240

因为订单的 recurring_monthly 是 20,乘以 12 个月后正好得到 240。

这说明模块不是简单写月收入,而是尊重 CRM 当前的 recurring plan 语义。


九、这套桥接逻辑最适合怎么理解

如果只看界面,你可能会以为:

  • 订阅订单确认 = CRM 金额自动同步。

但从源码和测试一起看,更准确的理解应该是:

订阅订单确认,为商机收入提供了一条“更可信、但受限的升级通道”。

它的边界非常明确:

  1. 不是任意币种都能更新
  2. 不是任意订单都会覆盖商机
  3. 更新方向只能上调,不能回退
  4. 订阅收入写回前,要先补齐 recurring plan 语义
  5. 即便更新成功,跟踪明细是否显示还受权限控制。

所以这套设计并不在追求“自动同步越多越好”。

它追求的是:

  • CRM 预测口径保持稳定;
  • 订单成交事实能被吸收;
  • 语义单位不被搞乱;
  • 敏感经营信息不被无差别广播。

这其实就是企业版桥接模块最有价值的地方:

它不是替你省一次录入,而是在收入治理上多守了几道门。


十、一句话记忆法

crm_sale_subscription 不是“订单确认后把金额抄回商机”,而是“只在同币种、且金额更大时,按 recurring plan 语义把商机收入往上升级,并按权限决定谁能看到这次 recurring 变化”。

理解了这句话,你就不会再把企业版 CRM 和订阅桥接,误读成一条粗暴的金额同步脚本。

DISCUSSION

评论区

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