先说结论
在 Odoo CRM 里,把 lead 或 opportunity 换到另一个 team,
从来不只是改一列 team_id。
源码里至少还有三层联动:
- 旧 stage 可能不再属于新 team,于是要重算 stage
- company 也要重新检查是否还和 team / user / partner 一致
- 如果 stage 真变了,
date_last_stage_update等阶段语义也会跟着更新
所以“转 team”在 Odoo 里本质上更像:
把这条商业对象重新放进另一条 pipeline 的语义坐标系。
一、为什么 stage 会因为 team 变化而失效
_compute_stage_id() 的逻辑非常直接:
- 如果当前没有 stage,找一个默认的非 folded stage
- 如果当前有 stage,但这个 stage 有
team_ids限制,且新 team 不在其中 - 那就重新找 stage
这意味着 stage 不是通用标签, 它可以是 带 team 作用域的 pipeline 节点。
所以当你把商机从 Team A 挪到 Team B 时, 旧阶段如果只属于 A, 在 B 的语境里它就不再是合法位置。
系统这时不是“给你保留旧值”, 而是要把它放回 B 能理解的某个默认阶段。
二、为什么默认回退不是 bug,而是保护语义
很多实施现场会抱怨:
- “我只是改 team,怎么 stage 变了?”
但从源码看,这其实是保护而不是破坏。
因为如果允许一条记录带着“不属于这个 team 的 stage”继续存在, 后续很多东西都会失真:
- kanban 列归属
- 赢单/输单语义
- rotting 阈值
- 阶段要求 tooltip
- 团队报表统计
所以 stage 回退的本质,是避免出现“挂在 Team B 名下,却站在 Team A 的漏斗节点里”的记录。
三、company 为什么也会跟着重算
_compute_company_id() 的规则也很严格。
源码会先检查当前 company_id 是否还成立:
- 是否属于负责人可用公司
- 是否与
team.company_id一致 - team 没公司、又没 assignee 时,这个 company 是否还说得通
- 没 user、没 team 时,除非 partner 本身就带这个 company,否则会清空
如果现有值不成立,才按优先级重新提议:
- team.company_id 优先
- 再看 user
- 再看 partner
这说明 team 迁移不仅是销售组织变化, 还可能是公司归属变化。
四、为什么 user 变了,team 也可能被反向纠正
_compute_team_id() 里还有另一条常被忽略的方向:
- 如果改了
user_id - 当前
team_id不是这个用户所属 team - Odoo 会尝试为该用户重新找默认 team
也就是说,Odoo 不是单向地“team 决定 user”。 在某些情形下,它也允许 user 反过来限制 team 的合法范围。
所以你在实施里如果同时改了 salesperson 和 team, 看到系统又帮你回填 team, 往往不是抽风, 而是在做组织一致性校验。
五、为什么 stage 真变了,会顺手改阶段时间语义
write() 里如果发现 stage_id 真的变化,
就会:
- 更新
date_last_stage_update - 如果新 stage 是 won,还会联动概率和 active 状态
- 再处理
date_closed是否应该落账
这说明 stage 对 Odoo 来说不是一个视觉列位, 而是带时间语义的业务状态。
所以 team 迁移引发 stage 重算时, 系统也必须连带维护“最后一次阶段变动是什么时候”。
否则漏斗时长、阶段停留分析都会失真。
六、stage 配置本身为什么风险很高
crm.stage.write() 对 is_won 的改动会批量影响记录概率;
而 crm.stage 本身又带 team_ids、fold、rotting_threshold_days、requirements。
这意味着你在后台改 stage 配置时, 其实是在同时改:
- 哪些 team 可用它
- 这个阶段是否折叠
- 这个阶段多久算发霉
- 这个阶段是否代表赢单
所以如果团队经常抱怨“转 team 后行为很奇怪”, 很多时候问题不在数据, 而在 stage 配置边界本来就不清。
一句话记忆法
Odoo CRM 里的 team 迁移不是搬个负责人而已,而是把记录重新放进另一条 pipeline;只要旧 stage 不再合法,系统就会回退到新 team 能理解的阶段,并重算相关语义。
DISCUSSION
评论区