CRM 深度

Odoo CRM 为什么“看起来重复”不等于“马上合并”:重复提示、可操作重复集与 merge 入口边界讲透

很多团队看到 Odoo CRM 的 duplicate 提示,就以为系统已经确认这几条线索应该合并。源码其实分得很开:界面提示用的是更宽的启发式集合,真正拿来处理、分配和 merge 的重复集更保守,而且会明确排除部分历史记录。

CRM
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 33 阅读

先说结论

在 Odoo CRM 里,“系统提醒你这条 lead 可能重复”和“系统认为这几条记录现在就该合并”,根本不是一回事。

官方源码至少拆成了三层语义:

  1. 潜在重复提示:界面层面的风险信号
  2. 可操作重复搜索:拿来分配、转换、处理的更保守集合
  3. 真正 merge:还要再经过主记录排序、字段合并与依赖迁移

所以如果你把 duplicate banner、duplicate count、merge wizard 当成同一个东西, 你对 Odoo CRM 的 dedup 机制就一定会误判。

最典型的误判有两种:

  • 看见“Potential Duplicate”就以为系统已经确认应合并
  • 看见某条旧 lost 记录和当前 lead 很像,就以为 merge 一定会把它拉进来

源码并不是这么做的。


一、界面上的“潜在重复”,定位是提醒,不是裁决

_compute_potential_lead_duplicates() 的注释写得很清楚,它是为了更高 volume 场景下高效算“潜在重复线索”。

这里用到的标准有三类:

  • email_domain_criterion
  • phone_sanitized
  • 同一个 commercial_partner

这三条标准本身就说明它不是严格法官,而是风险探测器。

为什么?

因为它故意会把“相关度较高、但未必适合立刻 merge”的对象也找出来。

比如:

  • 企业域名相同,可能只是同一家公司不同联系人
  • 商业实体相同,可能是母子联系人与历史商机并存
  • 电话一致,可能是共享总机或前台号码

这套逻辑更像在说:

这里有重复风险,值得有人看一眼。

它没在说:

这些记录现在就该合成一条。

这是第一层边界。


二、为什么 email_domain_criterion 比你想的更“宽”

Odoo 在重复提示里并不只拿完整邮箱精确比对。

email_domain_criterion 的设计带有很强的业务偏向:

  • 如果是公共邮箱提供商,保留更接近完整邮箱粒度
  • 如果是企业域名,可能退化到公司域名维度

这会带来一个非常现实的结果:

  • john@company.com
  • sales@company.com
  • mary@company.com

它们在界面提示层可能会被纳入同一类重复风险观察面。

这不是 bug,而是 Odoo 想提前提醒你:

  • 同一家公司是不是正在被多条线索并行推进
  • 这里有没有分配冲突、归因冲突、商机碎片化风险

也就是说,提示层容忍“疑似过宽”, 因为它承担的是运营清洁度提醒,而不是最终合并裁决。


三、SEARCH_RESULT_LIMIT = 21 暗示了一个重要判断:结果太多,反而不算“有用重复”

_compute_potential_lead_duplicates() 里还有个很值得注意的保护:

  • 如果某个条件搜出来的结果过多,超过阈值
  • 它会认为这个条件的辨识度不够高
  • 返回空模型级结果,而不是把大批对象都挂进 duplicate 集合

这个设计很聪明。

因为当你碰到:

  • 一个超大企业域名
  • 一个客服总机电话
  • 一个热门公共入口

如果系统把几十上百条线索全标成彼此潜在重复, 这个提示就失去意义了。

所以 Odoo 明确承认:

重复提示不是越多越好,结果太大时说明条件本身不够可操作。

这其实比很多“宁可全报出来”的去重逻辑更成熟。


四、真正可操作的重复搜索,条件会更保守

和提示层相比,_get_lead_duplicates() 的语义明显更收敛。

它主要基于:

  • email_normalized
  • partner_id

然后再叠加业务过滤:

  • 默认只看 won_status = pending
  • 默认只看 active = True

这个差异非常关键。

说明官方在“可疑重复发现”和“实际处理重复”之间故意做了分层:

  • 提示层可以宽一点
  • 操作层必须更保守、更偏当前可执行对象

所以你完全可能看到一种情况:

  • 界面上 duplicate count 很显眼
  • 但真正用来处理的重复集合并没有你想象中那么大

这不是不一致, 这是两种目标不同的集合。


五、为什么默认会排除很多历史记录

_get_lead_duplicates() 默认不把已归档 / 已失效 / 已赢单对象都拉进主要重复处理集。

逻辑上它更像在问:

当前 pipeline 里,有哪些活跃且仍待处理的重复对象需要清理?

而不是:

历史上所有长得像的记录都给我列出来。

这个区别非常业务化。

因为 dedup 真正最危险的场景,不是历史库里“有点像”的记录太多, 而是当前在跑的 pipeline 里:

  • 同一个客户被多销售并行跟
  • 新旧线索互相抢负责人
  • 当前归因和推进状态被打碎

所以默认排除非活跃历史记录,本质上是在保护当前运营动作。


六、include_lost=True 也不是把所有历史都捞回来

有人看到 _get_lead_duplicates(include_lost=True) 就会以为:

  • 那所有 lost / archived / closed 历史都会一起参与 merge 决策

实际上源码仍然保留业务边界:

  • won 的对象不是重点目标
  • include lost 主要是允许把某些 lost opportunity 纳进观察面
  • 但“是否应该真正处理它们”仍然取决于后续具体动作

换句话说,include_lost=True 是扩大可见边界, 不是取消所有处理边界。

这对历史数据很多的 CRM 很重要。

不然 dedup 逻辑就会变成:

  • 每次发现一个新 lead
  • 都把历史库几十条陈年记录拉进可操作集合

那系统会非常难用。


七、自动分配里的 dedup,目标是防止“同客多派”,不是做主数据审计

crm_team.py 的 lead assignment 流程里,官方会在 team allocation 阶段调用 _allocate_leads_deduplicate()

这里的 dedup 目的非常明确:

  • 候选 lead 在进入 team / salesperson 分配前
  • 先看看是否和当前活跃 lead 构成明显重复
  • 如有必要,先 merge,再继续分配

它不是在做公司级数据治理项目, 它是在做一件更务实的事:

别让同一个客户的重复线索被分给不同销售。

这是运营边界,不是主数据边界。

如果你理解错这个目标,就容易期待太多:

  • 以为分配前去重会把所有历史脏数据都自动洗干净
  • 以为一切“像重复”的对象都会被自动整平

这都不是官方设计目标。


八、真正 merge 之前,还要再过一层“谁最可信”的排序

就算记录进入 merge,Odoo 也不会机械地“把几条平均揉成一条”。

_sort_by_confidence_level() 会按这些线索给主记录排序:

  • 仍可推进的优先
  • opportunitylead 更可信
  • 阶段更靠后的优先
  • 概率更高的优先
  • 更新 / ID 更靠后的优先

这再次说明:

  • 提示层在问“是不是值得关注”
  • merge 层在问“如果真要合,谁最像真实主链路”

两者根本不是一个问题。


九、实战里最容易踩的 5 个坑

1. 把 duplicate count 当成 merge 建议数

它只是风险面,不是最终动作集。

2. 看到同域名就一键合并

同公司不同联系人,在 B2B 场景下非常常见。

3. 误以为潜在重复一定包含当前最关键的活跃记录

提示层和操作层集合并不完全相同。

4. 想让 assignment dedup 顺手完成历史库清洗

它主要服务当前分配秩序。

5. 把 include_lost 理解成“取消边界”

它只是扩大观察面,不是让 merge 失控。


最后一句

Odoo CRM 的 dedup 真正成熟的地方,不是它会告诉你“哪些像重复”, 而是它很清楚:

提示、处理、合并,是三件不同的事。

只要你把这三层边界分开,很多关于 duplicate banner、merge wizard 和历史记录处理的困惑都会一下子变清楚。

DISCUSSION

评论区

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