很多团队在做 Odoo 满意度看板时,第一反应都很朴素:
- 子记录有评分;
- 父对象也想看一个总体评分;
- 那就把子记录平均一下,不就完了吗?
听起来很合理,但 Odoo 官方并没有走这条“拍脑袋平均”的路。
在 addons/rating/models/rating_parent_mixin.py 里,官方单独做了 rating.parent.mixin,把父级满意度视为一套独立的汇总语义,而不是简单继承 rating.mixin 的结果。
这说明一件事:
“当前对象的评分统计”和“父对象的满意度 KPI”在数据意义上不是同一层问题。
如果你把它们混成一套,很容易在工单、项目、服务团队、阶段、客户经理等场景里得到看似有数字、其实口径不稳的报表。
一、为什么 Odoo 要同时保留 current record 和 parent record 两层
先看 rating.rating 的结构。
它不仅保存:
res_modelres_id
还保存:
parent_res_modelparent_res_id
这意味着官方从模型层就明确承认:
- 一条评分首先属于一个“被评价的具体对象”;
- 但它也可能需要被“归口统计”到另一个父对象。
这类业务在 Odoo 里非常常见。
例如:
- 某条消息或某次服务动作收到评价,但业务上你更想看整张工单的满意度;
- 某个阶段性触点产生评分,但管理层要看的却是项目、团队或服务人员的综合表现;
- 某些业务对象自己并不是最终 KPI 载体,它们只是评分发生的落点。
如果没有 parent 维度,你就只能在报表层临时 join、猜关系、重复算口径。这会让实现和调试都非常痛苦。
二、rating.mixin 解决“当前对象统计”,rating.parent.mixin 解决“父对象汇总”
很多人容易把这两个 mixin 当作“差不多的两个版本”。其实它们关注点不一样。
rating.mixin
更偏向当前对象自己的展示字段,比如:
- 最近一次评分;
- 当前对象直接关联评分的平均分;
- 当前对象的评分数量;
- 最近反馈文本。
rating.parent.mixin
更偏向父对象的 KPI 汇总,比如:
- 父对象整体满意度百分比;
- 父对象的汇总评分数;
- 父对象的平均分;
- 父对象的平均分百分比。
这不是简单重复,而是把“局部事实”和“管理口径”拆开了。
拆开的价值在于:
- 当前对象可以维持自己的真实反馈历史;
- 父对象可以按统一规则做上卷;
- 二者不会互相污染。
三、rating.parent.mixin 的核心不是 relation,而是口径
rating.parent.mixin 的关键字段包括:
rating_ids = One2many('rating.rating', 'parent_res_id', domain=parent_res_model=self._name)rating_percentage_satisfactionrating_countrating_avgrating_avg_percentage
很多人看到 One2many 会以为重点只是“能连到父对象”。其实重点在计算函数:
_compute_rating_percentage_satisfaction()
它不是从子对象的 rating_avg 再平均,而是:
- 直接去
rating.rating表上按parent_res_model/parent_res_id查; - 只统计
consumed = True且rating >= RATING_LIMIT_MIN的记录; - 用
_read_group()按parent_res_id、rating聚合; - 再把评分映射成
great / okay / bad三档; - 最后算满意度、总数、平均分。
这条链路告诉我们一件非常重要的事:
父对象 KPI 的真相应该来自评分明细表,而不是来自一层层中间对象已经算过的结果。
因为一旦你从中间聚合值再聚合,就很容易出现平均值的平均值问题。
四、为什么“平均值的平均值”在业务上是错的
举个简单例子。
假设一个团队下有两个工单:
- 工单 A:1 条评分,5 分;
- 工单 B:9 条评分,3 分。
如果你先算工单平均,再算团队平均,会得到:
- 工单 A 平均 = 5
- 工单 B 平均 = 3
- 团队平均 = (5 + 3) / 2 = 4
但真实明细平均应该是:
- (1×5 + 9×3) / 10 = 3.2
这差得非常大。
Odoo 之所以在 rating.parent.mixin 里直接按评分明细分组,而不是复用子对象已有平均值,本质上就是为了避免这种错误。
所以你如果自己做 KPI 汇总,最危险的写法往往就是:
- 先读子记录上已经算好的
rating_avg; - 再在 Python 里求一次平均。
看着省事,结果往往是错口径。
五、满意度百分比为什么不是 rating_avg / 5
rating.parent.mixin 同时给了两个指标:
rating_avgrating_percentage_satisfaction
这两个看起来都像“满意度”,但业务含义并不一样。
rating_avg
就是评分值的算术平均。
rating_percentage_satisfaction
它先把评分值映射成档位:
greatokaybad
然后计算:
- happy 数量 / 总评分数 × 100
这意味着:
- 一个 4 分和 5 分,在满意度百分比里都可能被视作 happy;
- 但在平均分里,4 分就是比 5 分低。
这对业务很重要,因为很多管理者真正想看的不是“平均 4.1 分”,而是“满意评价占比多少”。
Odoo 没把这两个指标强行合并,说明它默认接受:
- 一个系统可能同时需要运营视角和服务质量视角;
- 平均分和满意率应该并行存在,而不是互相替代。
六、时间窗口为什么被做成类属性 _rating_satisfaction_days
rating.parent.mixin 里还有一个容易被忽略的点:
_rating_satisfaction_days = False
计算时如果这个值被业务模型改成具体天数,就会自动限制在最近若干天的评分范围内。
这看起来很小,但设计很漂亮。
它不是把“最近 30 天满意度”写死在 SQL 或视图里,而是让具体模型自己决定:
- 有的对象需要全生命周期满意度;
- 有的对象只关心最近窗口;
- 有的团队 KPI 更适合按滚动周期看。
也就是说,官方把时间口径留成了模型级配置点,而不是报表层临时 hack。
七、为什么搜索平均分也要单独实现
rating.parent.mixin 提供了 _search_rating_avg()。
这意味着父对象上的平均分不只是“展示一下”,它还是可以参与 domain 搜索的业务字段。
实现方式也不是把平均分存成普通列,而是:
- 在
rating.rating上按 parent 分组求均值; - 再按比较运算符筛出 parent id;
- 返回
('id', 'in', parent_res_ids)。
这说明两个现实:
- 父级平均分是聚合语义,不适合粗暴冗余成一堆常驻值;
- 但业务又确实需要“筛出评分低于 3.5 的团队/项目/工单类别”。
所以官方给了搜索钩子,而不是强迫你放弃 domain 能力。
八、和 rating.mixin 的分工边界到底在哪
可以用一句话记住:
rating.mixin:这条记录自己被怎么评价;rating.parent.mixin:这条记录作为管理对象,应怎样归集子级反馈。
一个对象既可能只需要其中之一,也可能同时需要两者。
比如:
- 某个业务对象本身直接收评分,只要
rating.mixin; - 某个父对象只做汇总,不直接被评分,更适合
rating.parent.mixin; - 某些复合场景中,一个对象既有自己的反馈,也要吃下级反馈,那可能两个都用。
关键不是“哪个更高级”,而是你在建模时要先想清楚:
你要表达的是当前事实,还是管理口径?
九、实战里最常见的 5 个坑
1)把子对象平均分再平均一次
这是最常见、也最隐蔽的错。父级 KPI 应直接从 rating.rating 明细算,不要二次平均子对象聚合值。
2)忘了过滤 consumed = True
未提交邀请不应进入统计。否则催评多的对象会莫名其妙显得“评分数很多”。
3)只看平均分,不看满意率
平均分适合技术分析,满意率更接近管理视角。只留一个指标,往往不够用。
4)把时间窗口写死在报表层
如果你的模型只看最近 90 天,优先考虑扩展 _rating_satisfaction_days,不要在每个 dashboard 里各写一套日期过滤。
5)把 parent 关系靠 SQL join 临时猜
既然官方已经给了 _rating_get_parent_field_name() 和 parent 维度,优先把模型层关系接好,后面的统计与搜索都会更稳。
十、开发时怎样用好这套模型
场景 1:想让子对象评分自动上卷到父对象
先在被评分对象上实现 _rating_get_parent_field_name(),让 rating.rating 在创建时就自动拿到 parent 信息。
场景 2:想在父对象列表页筛出低满意度对象
别自己做存储字段同步任务,先复用 _search_rating_avg() 的思路,把 domain 建在聚合上。
场景 3:想区分“历史累计满意度”和“近 30 天满意度”
可以通过不同模型或扩展 _rating_satisfaction_days 做清晰分层,而不是拿一个字段同时承载所有口径。
结语
rating.parent.mixin 的价值,不在于“又多了几个评分字段”,而在于它把一个很容易被做烂的问题——父级满意度上卷——做成了明确的数据模型:
- 明细归明细;
- 汇总归汇总;
- 当前对象与父对象分层;
- 平均分与满意率分层;
- 时间窗口也可配置。
所以当你下次再想“把子对象评分平均一下就好了”时,最好先停一下。
因为 Odoo 官方源码已经告诉你:
真正可靠的满意度 KPI,不是随手平均,而是从明细事实出发、按稳定口径上卷出来的。
DISCUSSION
评论区