[IMP] rating,project: use the average of rating value for satisfaction

Beforer this commit, we display a emoji to show if the customer is
satisfied, okay or not for the task via the last rating value. The
problem is we use only the value for the last rating for each task.

This commit uses all ratings for each by using the average of rating
value for each task to know if all the customers that done a rating are
satisfied or not.
Moreover, we add 4 filters in the project.task model using the average
of rating values.

1. Satisfied => rating_avg >= 3.66
2. Okay => 2.33 >= rating_avg < 3.66
3. Dissatisfied => rating_avg < 2.33
4. No rating => show the tasks without any ratings.

task-2671848

Closes #81027
This commit is contained in:
Xavier BOL (xbo)
2022-01-05 13:58:01 +01:00
parent 7965efe75f
commit 790cc8698a
3 changed files with 44 additions and 8 deletions
@@ -248,7 +248,9 @@ class TestProjectFlow(TestProjectCommon, MockEmail):
self.assertEqual(rating_good.parent_res_id, self.project_pigs.id)
self.assertEqual(self.project_goats.rating_percentage_satisfaction, -1)
self.assertEqual(self.project_goats.rating_avg, 0, 'Since there is no rating in this project, the Average Rating should be equal to 0.')
self.assertEqual(self.project_pigs.rating_percentage_satisfaction, 0) # There is a rating but not a "great" on, just an "okay".
self.assertEqual(self.project_pigs.rating_avg, rating_bad.rating, 'Since there is only one rating the Average Rating should be equal to the rating value of this one.')
# Consuming rating_good
first_task.rating_apply(5, rating_good.access_token)
@@ -257,10 +259,14 @@ class TestProjectFlow(TestProjectCommon, MockEmail):
# Our One2Many is linked to a res_id (int) for which the orm doesn't create an inverse
first_task.invalidate_cache()
rating_avg = (rating_good.rating + rating_bad.rating) / 2
self.assertEqual(first_task.rating_count, 2, "Task should have two ratings associated with it")
self.assertEqual(first_task.rating_avg_text, 'top')
self.assertEqual(rating_good.parent_res_id, self.project_pigs.id)
self.assertEqual(self.project_goats.rating_percentage_satisfaction, -1)
self.assertEqual(self.project_pigs.rating_percentage_satisfaction, 50)
self.assertEqual(self.project_pigs.rating_avg, rating_avg)
self.assertEqual(self.project_pigs.rating_avg_percentage, rating_avg / 5)
# We change the task from project_pigs to project_goats, ratings should be associated with the new project
first_task.project_id = self.project_goats.id
@@ -271,7 +277,9 @@ class TestProjectFlow(TestProjectCommon, MockEmail):
self.assertEqual(rating_good.parent_res_id, self.project_goats.id)
self.assertEqual(self.project_goats.rating_percentage_satisfaction, 50)
self.assertEqual(self.project_goats.rating_avg, rating_avg)
self.assertEqual(self.project_pigs.rating_percentage_satisfaction, -1)
self.assertEqual(self.project_pigs.rating_avg, 0)
def test_task_with_no_project(self):
"""
+10 -7
View File
@@ -33,7 +33,10 @@
<separator/>
<filter string="Open Tasks" name="open_tasks" domain="[('is_closed', '=', False)]"/>
<separator/>
<filter string="Rated Tasks" name="rating_task" domain="[('rating_last_value', '!=', 0.0)]" groups="project.group_project_rating"/>
<filter name="rating_satisfied" string="Satisfied" domain="[('rating_avg', '&gt;=', 3.66)]" groups="project.group_project_rating"/>
<filter name="rating_okay" string="Okay" domain="[('rating_avg', '&lt;', 3.66), ('rating_avg', '&gt;=', 2.33)]" groups="project.group_project_rating"/>
<filter name="dissatisfied" string="Dissatisfied" domain="[('rating_avg', '&lt;', 2.33), ('rating_last_value', '!=', 0)]" groups="project.group_project_rating"/>
<filter name="no_rating" string="No Rating" domain="[('rating_last_value', '=', 0)]" groups="project.group_project_rating"/>
<separator/>
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction', '=', True)]"/>
<separator/>
@@ -1121,8 +1124,8 @@
<field name="legend_done"/>
<field name="activity_ids"/>
<field name="activity_state"/>
<field name="rating_last_value"/>
<field name="rating_ids"/>
<field name="rating_count"/>
<field name="rating_avg"/>
<field name="allow_subtasks"/>
<field name="child_text"/>
<field name="is_private"/>
@@ -1181,10 +1184,10 @@
<div class="oe_kanban_bottom_left">
<field name="priority" widget="priority"/>
<field name="activity_ids" widget="kanban_activity"/>
<b t-if="record.rating_ids.raw_value.length">
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-smile-o text-success" t-if="record.rating_last_value.value == 5" title="Latest Rating: Satisfied" role="img" aria-label="Happy face"/>
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-meh-o text-warning" t-if="record.rating_last_value.value == 3" title="Latest Rating: Okay" role="img" aria-label="Neutral face"/>
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-frown-o text-danger" t-if="record.rating_last_value.value == 1" title="Latest Rating: Dissatisfied" role="img" aria-label="Sad face"/>
<b t-if="record.rating_count.raw_value &gt; 0" groups="project.group_project_rating">
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-smile-o text-success" t-if="record.rating_avg.raw_value &gt;= 3.66" title="Average Rating: Satisfied" role="img" aria-label="Happy face"/>
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-meh-o text-warning" t-elif="record.rating_avg.raw_value &gt;= 2.33" title="Average Rating: Okay" role="img" aria-label="Neutral face"/>
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-frown-o text-danger" t-else="" title="Average Rating: Dissatisfied" role="img" aria-label="Sad face"/>
</b>
</div>
<div class="oe_kanban_bottom_right" t-if="!selection_mode">
+26 -1
View File
@@ -1,10 +1,21 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import operator
from datetime import timedelta
from odoo import api, fields, models, tools
from odoo.addons.rating.models.rating import RATING_LIMIT_SATISFIED, RATING_LIMIT_OK, RATING_LIMIT_MIN
from odoo.osv import expression
from odoo.tools.float_utils import float_compare
OPERATOR_MAPPING = {
'=': operator.eq,
'!=': operator.ne,
'<': operator.lt,
'<=': operator.le,
'>': operator.gt,
'>=': operator.ge,
}
class RatingParentMixin(models.AbstractModel):
@@ -59,7 +70,8 @@ class RatingMixin(models.AbstractModel):
rating_last_feedback = fields.Text('Rating Last Feedback', groups='base.group_user', related='rating_ids.feedback')
rating_last_image = fields.Binary('Rating Last Image', groups='base.group_user', related='rating_ids.rating_image')
rating_count = fields.Integer('Rating count', compute="_compute_rating_stats", compute_sudo=True)
rating_avg = fields.Float("Rating Average", compute='_compute_rating_stats', compute_sudo=True)
rating_avg = fields.Float("Average Rating",
compute='_compute_rating_stats', compute_sudo=True, search='_search_rating_avg')
rating_percentage_satisfaction = fields.Float("Rating Satisfaction", compute='_compute_rating_satisfaction', compute_sudo=True)
rating_last_text = fields.Selection(string="Rating Text", groups='base.group_user', related="rating_ids.rating_text")
@@ -79,6 +91,19 @@ class RatingMixin(models.AbstractModel):
record.rating_count = mapping.get(record.id, {}).get('rating_count', 0)
record.rating_avg = mapping.get(record.id, {}).get('rating_avg', 0)
def _search_rating_avg(self, operator, value):
if operator not in OPERATOR_MAPPING:
raise NotImplementedError('This operator %s is not supported in this search method.' % operator)
rating_read_group = self.env['rating.rating'].sudo().read_group(
[('res_model', '=', self._name), ('consumed', '=', True), ('rating', '>=', RATING_LIMIT_MIN)],
['res_id', 'rating_avg:avg(rating)'], ['res_id'])
res_ids = [
res['res_id']
for res in rating_read_group
if OPERATOR_MAPPING[operator](float_compare(res['rating_avg'], value, 2), 0)
]
return [('id', 'in', res_ids)]
@api.depends('rating_ids.res_id', 'rating_ids.rating')
def _compute_rating_satisfaction(self):
""" Compute the rating satisfaction percentage, this is done separately from rating_count and rating_avg