1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.addons.rating.models import rating_data
from odoo.osv import expression
from odoo.tools.float_utils import float_compare, float_round
class RatingMixin(models.AbstractModel):
"""This mixin adds rating statistics to mail.thread that already support ratings."""
_name = 'rating.mixin'
_description = "Rating Mixin"
_inherit = 'mail.thread'
rating_last_value = fields.Float('Rating Last Value', groups='base.group_user', compute='_compute_rating_last_value', compute_sudo=True, store=True, aggregator="avg")
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("Average Rating", groups='base.group_user',
compute='_compute_rating_stats', compute_sudo=True, search='_search_rating_avg')
rating_avg_text = fields.Selection(rating_data.RATING_TEXT, groups='base.group_user',
compute='_compute_rating_avg_text', compute_sudo=True)
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")
@api.depends('rating_ids', 'rating_ids.rating', 'rating_ids.consumed')
def _compute_rating_last_value(self):
# Pure SQL instead of calling read_group to allow ordering array_agg
self.flush_model(['rating_ids'])
self.env['rating.rating'].flush_model(['consumed', 'rating'])
if not self.ids:
self.rating_last_value = 0
return
self.env.cr.execute("""
SELECT
array_agg(rating ORDER BY write_date DESC, id DESC) AS "ratings",
res_id as res_id
FROM "rating_rating"
WHERE
res_model = %s
AND res_id in %s
AND consumed = true
GROUP BY res_id""", [self._name, tuple(self.ids)])
read_group_raw = self.env.cr.dictfetchall()
rating_by_res_id = {e['res_id']: e['ratings'][0] for e in read_group_raw}
for record in self:
record.rating_last_value = rating_by_res_id.get(record.id, 0)
@api.depends('rating_ids.res_id', 'rating_ids.rating')
def _compute_rating_stats(self):
""" Compute avg and count in one query, as thoses fields will be used together most of the time. """
domain = expression.AND([self._rating_domain(), [('rating', '>=', rating_data.RATING_LIMIT_MIN)]])
read_group_res = self.env['rating.rating']._read_group(domain, ['res_id'], aggregates=['__count', 'rating:avg']) # force average on rating column
mapping = {res_id: {'rating_count': count, 'rating_avg': rating_avg} for res_id, count, rating_avg in read_group_res}
for record in self:
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 rating_data.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_data.RATING_LIMIT_MIN)],
['res_id'], ['rating:avg'])
res_ids = [
res_id
for res_id, rating_avg in rating_read_group
if rating_data.OPERATOR_MAPPING[operator](float_compare(rating_avg, value, 2), 0)
]
return [('id', 'in', res_ids)]
@api.depends('rating_avg')
def _compute_rating_avg_text(self):
for record in self:
record.rating_avg_text = rating_data._rating_avg_to_text(record.rating_avg)
@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
since the query is different, to avoid computing if it is not necessary"""
domain = expression.AND([self._rating_domain(), [('rating', '>=', rating_data.RATING_LIMIT_MIN)]])
# See `_compute_rating_percentage_satisfaction` above
read_group_res = self.env['rating.rating']._read_group(domain, ['res_id', 'rating'], aggregates=['__count'])
default_grades = {'great': 0, 'okay': 0, 'bad': 0}
grades_per_record = {record_id: default_grades.copy() for record_id in self.ids}
for record_id, rating, count in read_group_res:
grade = rating_data._rating_to_grade(rating)
grades_per_record[record_id][grade] += count
for record in self:
grade_repartition = grades_per_record.get(record.id, default_grades)
grade_count = sum(grade_repartition.values())
record.rating_percentage_satisfaction = grade_repartition['great'] * 100 / grade_count if grade_count else -1
def write(self, values):
""" If the rated ressource name is modified, we should update the rating res_name too.
If the rated ressource parent is changed we should update the parent_res_id too"""
result = super(RatingMixin, self).write(values)
for record in self:
if record._rec_name in values: # set the res_name of ratings to be recomputed
res_name_field = self.env['rating.rating']._fields['res_name']
self.env.add_to_compute(res_name_field, record.rating_ids)
if record._rating_get_parent_field_name() in values:
record.rating_ids.sudo().write({'parent_res_id': record[record._rating_get_parent_field_name()].id})
return result
def _rating_get_parent_field_name(self):
"""Return the parent relation field name. Should return a Many2One"""
return None
def _rating_domain(self):
""" Returns a normalized domain on rating.rating to select the records to
include in count, avg, ... computation of current model.
"""
return ['&', '&', ('res_model', '=', self._name), ('res_id', 'in', self.ids), ('consumed', '=', True)]
def _rating_get_repartition(self, add_stats=False, domain=None):
""" get the repatition of rating grade for the given res_ids.
:param add_stats : flag to add stat to the result
:type add_stats : boolean
:param domain : optional extra domain of the rating to include/exclude in repartition
:return dictionnary
if not add_stats, the dict is like
- key is the rating value (integer)
- value is the number of object (res_model, res_id) having the value
otherwise, key is the value of the information (string) : either stat name (avg, total, ...) or 'repartition'
containing the same dict if add_stats was False.
"""
base_domain = expression.AND([self._rating_domain(), [('rating', '>=', 1)]])
if domain:
base_domain += domain
rg_data = self.env['rating.rating']._read_group(base_domain, ['rating'], ['__count'])
# init dict with all possible rate value, except 0 (no value for the rating)
values = dict.fromkeys(range(1, 6), 0)
for rating, count in rg_data:
rating_val_round = float_round(rating, precision_digits=1)
values[rating_val_round] = values.get(rating_val_round, 0) + count
# add other stats
if add_stats:
rating_number = sum(values.values())
return {
'repartition': values,
'avg': sum(float(key * values[key]) for key in values) / rating_number if rating_number > 0 else 0,
'total': sum(count for __, count in rg_data),
}
return values
def rating_get_grades(self, domain=None):
""" get the repatition of rating grade for the given res_ids.
:param domain : optional domain of the rating to include/exclude in grades computation
:return dictionnary where the key is the grade (great, okay, bad), and the value, the number of object (res_model, res_id) having the grade
the grade are compute as 0-30% : Bad
31-69%: Okay
70-100%: Great
"""
data = self._rating_get_repartition(domain=domain)
res = dict.fromkeys(['great', 'okay', 'bad'], 0)
for key in data:
grade = rating_data._rating_to_grade(key)
res[grade] += data[key]
return res
def rating_get_stats(self, domain=None):
""" get the statistics of the rating repatition
:param domain : optional domain of the rating to include/exclude in statistic computation
:return dictionnary where
- key is the name of the information (stat name)
- value is statistic value : 'percent' contains the repartition in percentage, 'avg' is the average rate
and 'total' is the number of rating
"""
data = self._rating_get_repartition(domain=domain, add_stats=True)
result = {
'avg': data['avg'],
'total': data['total'],
'percent': dict.fromkeys(range(1, 6), 0),
}
for rate in data['repartition']:
result['percent'][rate] = (data['repartition'][rate] * 100) / data['total'] if data['total'] > 0 else 0
return result
|