先说结论
在 Odoo CRM 里,Lost 最容易被误解成一个阶段名字。
但源码真正表达的 lost 语义更接近:
active = Falseprobability = 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
然后它做的动作也很清楚:
action_archive()write({'probability': 0, 'automated_probability': 0, ...})
这说明 Odoo 官方并不把 lost 定义成:
- 切到某个 stage
而是定义成:
- 归档
- 概率归零
- 并可附加 lost reason
所以 lost 的本体是 状态语义组合,不是阶段名称。
二、为什么“叫 Lost 的阶段”不等于真正 lost
很多实施会在 kanban 上建一个名字叫 Lost 的 stage, 然后以为这就完成丢单建模了。
源码语义告诉你,这个理解不牢。
因为真正的 won_status 计算是这样的:
probability == 100且stage_id.is_won→ wonnot active且probability == 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 = 0automated_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 >= 100或active=False→date_closed = nowprobability > 0→date_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
评论区