CRM 深度

Odoo CRM 里的 Lost 为什么不是一个普通阶段:archive、restore、date_closed 和 lost reason 边界

很多人会把‘丢单’理解成把商机拖到一个 Lost 阶段,但 Odoo CRM 的源码语义并不是这样。真正的 lost 更接近 archive + probability=0,再加上 lost reason、date_closed 和 restore 时的概率回滚逻辑。

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

先说结论

在 Odoo CRM 里,Lost 最容易被误解成一个阶段名字。

但源码真正表达的 lost 语义更接近:

  • active = False
  • probability = 0
  • 可能附带 lost_reason_id
  • 同时落下 date_closed

restore 也不是简单取消归档, 它还会把这条记录重新拉回“正常生命周期”,并重置概率与丢单原因的关系。

所以如果你把 lost 当成普通 stage,很多现象都会变得解释不通:

  • 为什么标 lost 后记录像被归档了
  • 为什么 restore 后 lost reason 没了
  • 为什么 date_closed 会跟着变化
  • 为什么叫 Lost 的 stage,不一定真的代表 lost 语义

一句话说:

在 Odoo CRM 里,lost 更像一个业务终态动作,不是一个普通列。


一、源码自己就写了:Lost semantic = probability 0 AND active False

action_set_lost() 里的注释非常直接:

  • Lost semantic: probability = 0 AND active = False

然后它做的动作也很清楚:

  1. action_archive()
  2. write({'probability': 0, 'automated_probability': 0, ...})

这说明 Odoo 官方并不把 lost 定义成:

  • 切到某个 stage

而是定义成:

  • 归档
  • 概率归零
  • 并可附加 lost reason

所以 lost 的本体是 状态语义组合,不是阶段名称。


二、为什么“叫 Lost 的阶段”不等于真正 lost

很多实施会在 kanban 上建一个名字叫 Lost 的 stage, 然后以为这就完成丢单建模了。

源码语义告诉你,这个理解不牢。

因为真正的 won_status 计算是这样的:

  • probability == 100stage_id.is_won → won
  • not activeprobability == 0 → lost
  • 否则 → pending

注意 lost 这一支根本没看 stage 名字。

也就是说:

  • 你就算建了一个名叫 Lost 的 stage
  • 只要记录还是 active,而且 probability 没归零
  • 它在系统语义里仍然不是真 lost

这就是很多看板设计会埋下的坑。


三、archive 不等于 lost,但 lost 一定会走 archive

action_unarchive() 的注释也很关键:

  • archive 本身并不自动等于 lost
  • 因为 lead 可以被归档,但不一定是丢单

这句话很重要。

它说明:

  • archive 是技术动作 / 可见性动作
  • lost 是业务语义动作

两者有交集,但不是完全等价。

在 CRM 里:

  • 你手动 archive 一条记录,不一定是在表达“客户输了”
  • 但你执行 action_set_lost(),系统一定会借助 archive 来表达“退出活跃 pipeline”

所以更准确的关系是:

Lost 会调用 archive,但 archive 不天然代表 lost。


四、为什么 restore 不只是“取消归档”

action_restore() 的注释非常有含义。

它说 restore 的目标是:

  • 让 lost lead 回到正常生命周期
  • 重新激活
  • 重新按当前 stage 计算概率
  • 而不是只恢复 active=True

源码先 action_unarchive(),然后:

  • lead.probability = lead.automated_probability

这一步很关键。

因为 lost 时系统把:

  • probability = 0
  • automated_probability = 0

但恢复后,它希望这条记录重新变成一个“活的 pipeline 对象”, 所以会把手工 probability 拉回自动概率口径。

也就是说 restore 的真实语义是:

重新参战,而不是仅仅从回收站捞出来。


五、为什么 restore 后 lost reason 会被清空

action_unarchive() 里还有一段容易被忽略:

  • 对被重新激活的记录,lost_reason_id 会被清空
  • 然后重新计算自动概率

这背后的业务逻辑很合理。

因为如果一条记录已经恢复推进, 那它就不应该继续背着“上一次丢单原因”作为当前状态。

否则会出现一种很奇怪的混合语义:

  • 记录现在是活跃的
  • 但又挂着旧的丢单原因

从业务表达上看,这会让数据既像 lost 又像 pending。

所以源码选择在恢复时把旧 lost_reason_id 清掉,重新开始。


六、date_closed 为什么会跟着 lost / won / reopen 一起动

write() 逻辑里有一条很关键的统一规则:

  • probability >= 100active=Falsedate_closed = now
  • probability > 0date_closed = False
  • 阶段切到非 won 且没显式给概率时,也会清空 date_closed

这就解释了很多表面“诡异”的现象:

标 lost 时

  • 归档 + 概率归零
  • 所以 date_closed 被打上时间戳

恢复推进时

  • 重新 active,概率重新大于 0
  • 所以 date_closed 会被清掉

进入 won stage 时

  • 也会被视为 closed

也就是说,date_closed 不是“赢单日期”字段, 而更像:

这条记录何时进入关闭语义。

它既可以代表 won,也可以代表 lost。


七、为什么在 won stage 之间切换,不应该重复改关闭时间

测试里还有一个很实用的边界:

  • 一条已经在 won 状态的记录
  • 如果只是从一个 won stage 切到另一个 won stage
  • date_closed 不应该被重写

这说明 Odoo 认定:

  • 真正的关闭时刻,是第一次进入关闭语义的那一刻
  • 后面在关闭态内部再移动,不算重新关单

这个细节对报表特别重要。

否则赢单后每次调整 won stage,关单日期都会漂移,统计会非常假。


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

1. 建一个 Lost stage 就当做丢单建模完成

不够。lost 的核心语义是 inactive + probability 0。

2. 把 archive 和 lost 完全画等号

archive 更宽,lost 更具体。

3. restore 后还保留旧 lost reason

源码默认不这么做,因为这会污染当前状态表达。

4. 把 date_closed 当成只属于赢单的字段

其实 lost 也会写它。

5. 忽视 restore 会重置 probability 的自动语义

restore 不只是显示回来,它还让机会重新回到 pipeline 逻辑里。


九、一句话记忆法

Odoo CRM 里的 Lost 不是一个普通 stage,而是“退出活跃 pipeline”的业务动作:archive、probability=0、lost reason 和 date_closed 会一起表达这个终态。

理解这一句,lost / archive / restore 的很多边界就不会再混了。

DISCUSSION

评论区

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