很多人看到 Odoo Chatter 里的变更记录,会下意识地把它理解成一种“描述文本”:
- 状态从 Draft 改成 Posted
- 金额从 100 改成 120
- 负责人从 A 改成 B
但如果真去看 /home/ubuntu/odoo-temp/addons/mail/models/mail_tracking_value.py,会发现 Odoo 并不是先生成一句可读文本再存起来。
它真正做的是:
先把每个字段变化保存成结构化差异数据,再在展示阶段决定当前用户能看到什么样的差异。
这两个阶段分开,是理解 Chatter 追踪行为的关键。
一、Odoo 追踪的不是一句话,而是一组 tracking values
mail.tracking.value 这个模型专门承载“字段发生了什么变化”。
它不是只存一段 message body,而是按类型拆成多组字段:
old_value_integer/new_value_integerold_value_float/new_value_floatold_value_char/new_value_charold_value_text/new_value_textold_value_datetime/new_value_datetimecurrency_idfield_idmail_message_id
这意味着 Chatter 里你最后看到的那句变更提示,底层不是一段已经写死的人类语言,而是:
- 哪个字段变了;
- 旧值是什么;
- 新值是什么;
- 它属于什么类型;
- 是否需要货币精度、日期格式或 Many2one 显示名。
这也是为什么 Odoo 能把 tracking 同时用于:
- 普通 Chatter 展示;
- 通知摘要;
- account 审计日志预览;
- 搜索 old/new value。
因为它先保存的是结构,不是最终文案。
二、_create_tracking_values() 真正在做的是类型归一化
_create_tracking_values() 很值得细看。
它会根据字段类型,把变化装进不同的槽位。
普通标量字段
对于:
- integer
- float
- char
- text
- datetime
逻辑很直观:
- 旧值进 old_xxx
- 新值进 new_xxx
monetary
货币字段不是只存数字,还会把 currency_id 记录下来。
这一步很关键,因为前端展示差异时需要知道:
- 这是金额;
- 应该用哪种货币格式;
- 小数精度是多少。
所以金额变更不是“浮点数改了”,而是“带币种上下文的变更”。
date
date 会被转成 datetime 存入追踪值。
这说明 mail.tracking.value 为了统一展示与序列化,宁愿在存储层把 date 和 datetime 往同一类槽位靠,也不额外设计一组专门的日期字段。
selection
selection 存的不是技术值,而是显示文本。
也就是说,追踪记录优先服务于“人能看懂”,而不是“还能拿回原始 key 再二次翻译”。
many2one / many2many / one2many / tags
这类关联字段不会只存 ID,还会尽量存:
- 记录 ID
- display name
- 多值列表拼接后的文本
这就是为什么 Chatter 里看到的是“负责人从 Mitchell Admin 改成 Marc Demo”,而不是 3 -> 7。
换句话说:
tracking value 是结构化的,但它仍然优先面向展示,而不是做审计仓库里那种完全原子化、纯技术型快照。
三、为什么“谁在看”会影响看到的差异内容
这点特别容易被误解。
很多人默认以为:
- 既然差异已经存进数据库,谁打开消息都该看到一样的内容。
但 _filter_has_field_access(env) 恰恰说明不是这样。
源码逻辑是:
- 如果 tracking 绑定到了一个真实字段,当前用户必须对该字段有 read 权限;
- 如果 tracking 已经没有 field_id,只剩历史信息,则通常只有系统管理员能看。
这意味着:
一条 mail.message 是共享的,但其中 tracking_value_ids 的可见子集,不一定对所有用户相同。
所以你在现场看到“管理员能看到完整字段变更,普通用户只看到部分甚至看不到”,这不是前端随机出问题,而是设计如此。
四、为什么 account 模块还要再包一层 audit preview
在 /home/ubuntu/odoo-temp/addons/account/models/mail_message.py 里,account 给 mail.message 扩了一个 account_audit_log_preview。
它的核心流程很清楚:
- 先拿到 notification 类型消息;
- 再用
message.sudo().tracking_value_ids._filter_has_field_access(self.env)过滤当前环境能看的差异; - 再调用
_tracking_value_format()把结构化差异格式化成“旧值 ⇨ 新值 (字段名)”; - 最后拼成审计预览文本。
这里最值得注意的是第 2 步。
源码不是直接 tracking_value_ids._tracking_value_format(),而是先 sudo() 取数据,再按当前用户权限过滤。
这表达了一个很成熟的安全思路:
- 计算阶段可以从更高权限读取底层数据;
- 但最终展示阶段必须按当前用户的字段权限裁剪。
所以 account 审计预览不是“数据库里早就有一段完整文案”,而是每次按当前用户上下文即时生成可见版本。
五、_tracking_value_format() 为什么还要再做一次格式化
mail.tracking.value 已经存了 old/new 值,为什么不直接显示?
因为存储值和展示值不是同一件事。
_tracking_value_format() 还要补足这些信息:
- 字段 string 名称;
- 字段显示顺序;
- 是否是 property 字段;
- 金额精度;
- 日期 / datetime 的格式输出;
- boolean 的布尔化;
- 每种字段在前端该怎么呈现。
所以 tracking value 更像是一个“半成品差异对象”,而不是最终 UI 文案。
存储层负责:
- 可追溯;
- 可搜索;
- 可按类型重建。
展示层负责:
- 对人类友好;
- 对当前用户安全;
- 对具体模型语义友好。
六、为什么 account 还要限制 tracking 的删除与改写
addons/account/models/mail_tracking_value.py 很短,但信息量很大。
它做了两件事:
- 删除 tracking value 时,回调到
mail_message_id._except_audit_log(); - 写 tracking value 时,也先走
_except_audit_log()。
而 account 对 mail.message 的 _except_audit_log() 会检查:
- 是否属于受保护的 restricted audit trail;
- 是否允许修改主体、body、关联对象等关键内容;
- 如果不允许,就抛错。
翻成人话就是:
在会计场景里,tracking value 不是普通聊天碎片,而是审计链的一部分。
所以你不能把它当作“发错了就删、看不顺眼就改”的普通 chatter 消息。
这也解释了为什么很多会计客户会感受到:
- 一般消息可以改;
- 但某些审计相关变更记录几乎碰不得。
因为它们已经被提升成审计留痕对象了。
七、新手最容易误判的三个点
1)“消息存在”不等于“所有 tracking 字段都对所有人可见”
消息是同一条,但差异明细会按字段权限过滤。
2)“Chatter 上展示的文案”不等于“数据库原样保存的文本”
数据库里先存的是结构化 old/new 值,最终显示文案是后拼出来的。
3)“tracking value”不只是通知 UI 的附属品
在 account 里,它还可能属于受保护的 audit trail,具备审计约束。
八、开发时最值得注意的实战建议
1)自定义追踪字段时,先想清字段权限
如果字段本身有 groups 限制,不同用户在 Chatter 里看到的结果天然会不同。
2)不要把 tracking 当成纯文本日志来二次加工
更稳的做法是复用 _tracking_value_format() 这套格式化链,而不是自己拼 old/new 字符串。
3)会计相关消息不要随意改写或清理
尤其是在 restrictive audit trail 场景里,这些 tracking 可能受法规或审计链保护。
4)排查“为什么有人看得到、有人看不到”时,优先查字段 access groups
很多问题不是消息丢了,而是 _filter_has_field_access() 把差异裁掉了。
总结
mail.tracking.value 最值得记住的一点是:
Odoo 跟踪的不是一段“变更说明文字”,而是一份可按类型重建、可按权限裁剪、可按模型格式化的结构化差异。
所以 Chatter 里的字段追踪,其实是三层机制叠在一起:
- 存储层:按类型保存 old/new 值;
- 权限层:按当前用户字段权限过滤可见差异;
- 展示层:按模型字段语义重新格式化成可读文案。
把这三层分清后,你就会明白:
- 为什么同一条消息不同人看到的不完全一样;
- 为什么金额、日期、Many2one 的追踪表现不一样;
- 为什么 account 会把 tracking value 当成审计链的一部分来保护。
这才是 Odoo Chatter 字段追踪真正稳健的地方:
它不是“记一句话”,而是在维护一份既能读、又能控、还能审计的差异记录。
DISCUSSION
评论区