开发

Odoo 满意度汇总为什么不能只“平均一下”:rating.parent.mixin 的上卷模型讲透

基于 Odoo 官方 `rating.parent.mixin`、`rating.mixin` 与 `rating.rating` 源码,讲清评分明细为何要区分当前对象与父对象、满意度百分比怎样计算,以及父级 KPI 为什么不能简单拿子记录平均值代替。

Odoo 开发
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 4 阅读

很多团队在做 Odoo 满意度看板时,第一反应都很朴素:

  • 子记录有评分;
  • 父对象也想看一个总体评分;
  • 那就把子记录平均一下,不就完了吗?

听起来很合理,但 Odoo 官方并没有走这条“拍脑袋平均”的路。

addons/rating/models/rating_parent_mixin.py 里,官方单独做了 rating.parent.mixin,把父级满意度视为一套独立的汇总语义,而不是简单继承 rating.mixin 的结果。

这说明一件事:

“当前对象的评分统计”和“父对象的满意度 KPI”在数据意义上不是同一层问题。

如果你把它们混成一套,很容易在工单、项目、服务团队、阶段、客户经理等场景里得到看似有数字、其实口径不稳的报表。

一、为什么 Odoo 要同时保留 current record 和 parent record 两层

先看 rating.rating 的结构。

它不仅保存:

  • res_model
  • res_id

还保存:

  • parent_res_model
  • parent_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_satisfaction
  • rating_count
  • rating_avg
  • rating_avg_percentage

很多人看到 One2many 会以为重点只是“能连到父对象”。其实重点在计算函数:

_compute_rating_percentage_satisfaction()

它不是从子对象的 rating_avg 再平均,而是:

  1. 直接去 rating.rating 表上按 parent_res_model / parent_res_id 查;
  2. 只统计 consumed = Truerating >= RATING_LIMIT_MIN 的记录;
  3. _read_group()parent_res_idrating 聚合;
  4. 再把评分映射成 great / okay / bad 三档;
  5. 最后算满意度、总数、平均分。

这条链路告诉我们一件非常重要的事:

父对象 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_avg
  • rating_percentage_satisfaction

这两个看起来都像“满意度”,但业务含义并不一样。

rating_avg

就是评分值的算术平均。

rating_percentage_satisfaction

它先把评分值映射成档位:

  • great
  • okay
  • bad

然后计算:

  • 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 搜索的业务字段。

实现方式也不是把平均分存成普通列,而是:

  1. rating.rating 上按 parent 分组求均值;
  2. 再按比较运算符筛出 parent id;
  3. 返回 ('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

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。