先说结论
很多人把 Odoo 的字段追踪理解成:字段一变,系统比一下旧值新值,然后在 Chatter 里写一句“XXX 从 A 改成 B”。
但 mail.thread 源码做的事情复杂得多。真实链路是:
_track_prepare()先把旧值塞进 precommit 缓存;_track_finalize()在提交前统一触发_message_track()做差异计算;_track_subtype()决定这次变化是否属于某个业务 subtype;- 最终再用
message_post()生成一条可展示、可订阅、可通知的 Chatter 消息。
所以一句话概括:
Odoo 的 tracking 不是单纯 diff,而是把字段变化包装成一条带协作语义的消息生成流程。
这篇主要看哪里
核心源码在:
addons/mail/models/mail_thread.py
重点方法:
_track_prepare()_track_finalize()_message_track()_track_subtype()_track_get_default_log_message()message_post()
第一层:为什么要先 _track_prepare()
字段一旦写入,旧值就没了。_track_prepare() 的职责就是在真正写操作完成前,把需要追踪的字段旧值保存到 self.env.cr.precommit.data。
它只处理 _track_get_fields() 返回的可追踪字段,而且对 properties 字段还会做特殊转换,确保最终拿到的是可以比较、可以展示的旧值。
这说明 Odoo 很清楚 tracking 的核心难点不是“展示新值”,而是在事务内正确保留旧值上下文。
第二层:为什么 _track_finalize() 要挂在 precommit
_track_prepare() 并不直接生成消息,而是把 _track_finalize() 注册到 precommit。
原因很直接:
- 要等字段真正写完,才能同时拿到旧值和新值;
- 要把同一事务里的 tracking 合并到统一处理点;
- 要确保最终产物仍然能自然接到
message_post()流程里。
这比在各个 write 分支里零散拼文案稳得多,也不容易产生“写了一半先发日志”的怪现象。
第三层:_message_track() 真正做的是“变化翻译”
_message_track() 并不只是 old != new。它会:
- 根据字段元信息拿到
string/type/selection/currency_field; - 调用
_mail_track()生成 changed fields 与 tracking values; - 再从 precommit 里取可能预设的 body 或 author;
- 最后决定这次变化应该生成什么消息。
这意味着 tracking 的输出至少有三层:
- 哪些字段变了;
- 日志正文长什么样;
- 这条消息有没有额外业务语义。
所以字段追踪不是 UI 文案拼接,而是“数据变化 → 协作消息”的翻译过程。
第四层:_track_subtype() 决定这是不是业务事件
默认的 _track_subtype() 返回 False,但很多业务模型会覆写它。
它的意义在于:
- 普通字段变化可以只是一般日志;
- 关键字段变化则可以升级成带 subtype 的业务事件。
例如阶段变化、负责人变化、关键状态切换,这些都不只是 diff,而可能意味着:
- followers 收到不同通知;
- 消息归属于不同订阅语义;
- 协作历史里这条记录的阅读方式不同。
所以 _track_subtype() 的存在,正是 Odoo 把 tracking 从“技术日志”抬升成“业务协作消息”的关键。
第五层:为什么 tracking 最终还是走 message_post()
源码里如果 subtype 存在,会直接 record.message_post(...);即便没有 subtype,tracking 的结果最终也还是围绕 mail.message 展开。
这说明 Odoo 的设计选择非常明确:
字段变更记录不做成独立日志系统,而做成 Chatter 消息生态的一部分。
这样做的好处是:
- 直接进入时间线;
- 复用 subtype、followers、通知机制;
- 不需要维护第二套展示与权限体系。
第六层:这套机制解决了什么问题
1. 保证旧值读取可靠
通过 precommit 缓存而不是事后猜测。
2. 保证消息生成发生在正确时机
既能看到最终新值,又不至于脱离当前事务。
3. 允许模型把字段变化升级成业务事件
通过 _track_subtype(),tracking 不再只是技术日志。
4. 让变更记录直接进入协作消息系统
不需要额外维护独立审计展示层。
实战里最容易误解的几个点
误解 1:字段 tracking 只是前端展示
不对。核心发生在 mail.thread 的事务与消息层。
误解 2:tracking 一定只发一条固定格式日志
不对。body、author、subtype 都可能被模型或上下文影响。
误解 3:_track_subtype() 只是锦上添花
不对。它决定某些变化是普通 diff 还是带业务语义的协作事件。
二开时最该守住的边界
1. 不要在 write 后自己手搓一条 Chatter 文案来替代 tracking
你会绕开旧值缓存、标准 tracking value、subtype 分流和统一消息生成链路。
2. 关键业务状态通知时,优先考虑覆写 _track_subtype()
这通常比简单在 write 里 message_post() 更符合 Odoo 原生协作语义。
3. 排查 tracking 异常时,不要只看最终 mail.message
还要看:
_track_prepare()有没有拿到旧值;- precommit data 有没有被清掉;
_message_track()有没有识别出真实 changes;_track_subtype()是否返回了预期 subtype。
结论
Odoo 的字段 tracking 机制,本质上是:
- 先缓存旧值;
- 再在事务末尾统一对比;
- 按 subtype 决定业务语义;
- 最终落成 Chatter 消息。
一句话总结:
Chatter 里的字段变化不是数据库 diff 的回显,而是 Odoo 用
mail.thread把“数据变化”翻译成“协作事件”的结果。
DISCUSSION
评论区