From 0b1a27a3c7862bbe511614d05e04db696fcd71d5 Mon Sep 17 00:00:00 2001 From: jat-odoo Date: Fri, 1 Jul 2016 13:33:14 +0200 Subject: [PATCH] [REF,IMP] project,project_issue,rating: Handle ratings more efficiently Purpose: For our internal project management, we would like to track the customer satisfaction on the open projects. We send them an email every one or 2 weeks allowing them to give a feedback by clicking on one of 3 smileys: Happy, Average, Angry. They can also put a additional explanation. We already have something similar which is working on livechat with a unusable reporting, and on issues but unusable. Our project are managed by tasks. Specification: - On the project : Selection fields : (Periodical Rating or Rating on Stage) + Fields to choose the period if periodical - On the stage : email_template_id field. - IF : ---> Periodical : Send an email to all the customers for the tasks on this stage periodically ---> On Stage : Send an email to the customer's tasks when the task reaches the stage - That way, it's impossible to send a satisfaction request both periodically and sequentially. FP request - Who is the customer : The customer on the task OR the customer on the related sales order OR the customer on the projet OR nobody - The last feedback is displayed on the task kanban card (Thumb up, down, or neutral) - On the project kanban card, the customer satisfaction is displayed. This is the simple mean of all the previous ratings. --- addons/project/views/res_config_views.xml | 4 +- addons/project_issue/__openerp__.py | 1 + .../models/project_issue_config_settings.py | 5 + addons/project_issue/models/res_partner.py | 8 +- .../views/project_config_settings_views.xml | 17 ++ addons/rating/controllers/main.py | 32 ++-- addons/rating/models/rating.py | 171 +++++++++--------- addons/rating/static/src/img/rating_-1.png | Bin 2495 -> 0 bytes addons/rating/static/src/img/rating_0.png | Bin 5184 -> 2495 bytes addons/rating/static/src/img/rating_1.png | Bin 0 -> 5184 bytes addons/rating/views/rating_template.xml | 42 ++++- addons/rating/views/rating_view.xml | 38 ++-- addons/rating_project/data/project_data.xml | 138 ++++++++++---- addons/rating_project/data/project_demo.xml | 25 ++- addons/rating_project/models/project.py | 92 ++++++---- .../views/project_dashboard.xml | 25 ++- addons/rating_project/views/project_view.xml | 71 +++++++- addons/rating_project_issue/__openerp__.py | 1 + .../data/project_issue_data.xml | 20 ++ .../data/project_issue_demo.xml | 15 +- .../models/project_issue.py | 97 ++++------ .../views/project_issue_view.xml | 50 ++++- addons/sale_service_rating/__init__.py | 4 + addons/sale_service_rating/__openerp__.py | 15 ++ addons/sale_service_rating/models/__init__.py | 4 + addons/sale_service_rating/models/project.py | 13 ++ .../views/website_livechat.xml | 2 +- .../controllers/main.py | 43 +++-- .../src/css/website_rating_project_issue.css | 8 - .../views/project_project_view.xml | 2 +- .../views/website_rating_project.xml | 83 ++++----- 31 files changed, 658 insertions(+), 368 deletions(-) create mode 100644 addons/project_issue/views/project_config_settings_views.xml delete mode 100644 addons/rating/static/src/img/rating_-1.png create mode 100644 addons/rating/static/src/img/rating_1.png create mode 100644 addons/rating_project_issue/data/project_issue_data.xml create mode 100644 addons/sale_service_rating/__init__.py create mode 100644 addons/sale_service_rating/__openerp__.py create mode 100644 addons/sale_service_rating/models/__init__.py create mode 100644 addons/sale_service_rating/models/project.py diff --git a/addons/project/views/res_config_views.xml b/addons/project/views/res_config_views.xml index 4baf40323d0..73c8551e0ba 100644 --- a/addons/project/views/res_config_views.xml +++ b/addons/project/views/res_config_views.xml @@ -20,11 +20,9 @@ More Info - - - + diff --git a/addons/project_issue/__openerp__.py b/addons/project_issue/__openerp__.py index 7c62d6b7d4c..24e1f319331 100644 --- a/addons/project_issue/__openerp__.py +++ b/addons/project_issue/__openerp__.py @@ -30,6 +30,7 @@ It allows the manager to quickly check the issues, assign them and decide on the 'views/res_partner_view.xml', 'views/project_issue_menu.xml', 'views/project_dashboard.xml', + 'views/project_config_settings_views.xml', ], 'demo': ['data/project_issue_demo.xml'], 'application': True, diff --git a/addons/project_issue/models/project_issue_config_settings.py b/addons/project_issue/models/project_issue_config_settings.py index 393f3a8a6bb..72c8cb04cba 100644 --- a/addons/project_issue/models/project_issue_config_settings.py +++ b/addons/project_issue/models/project_issue_config_settings.py @@ -12,3 +12,8 @@ class ProjectIssueConfiguration(models.TransientModel): ], string="Timesheets on Issues", help='Provides timesheet support for the issues/bugs management in project.\n' '-This installs the module project_issue_sheet.') + module_rating_project_issue = fields.Selection([ + (0, "No customer rating"), + (1, 'Track customer satisfaction on issues') + ], "Rating on issue", + help="This allows customers to give rating on issue") diff --git a/addons/project_issue/models/res_partner.py b/addons/project_issue/models/res_partner.py index 2ec0f77a5f7..8bfe514987f 100644 --- a/addons/project_issue/models/res_partner.py +++ b/addons/project_issue/models/res_partner.py @@ -10,7 +10,7 @@ class ResPartner(models.Model): issue_count = fields.Integer(compute='_compute_issue_count', string='# Issues') def _compute_issue_count(self): - Issue = self.pool['project.issue'] - partners = {id: self.search([('id', 'child_of', self.ids)]) for id in self.ids} - for partner_id in partners.keys(): - partner.issue_count = Issue.search_count([('partner_id', 'in', partners[partner_id])]) + Issue = self.env['project.issue'] + partners = {id: self.search([('id', 'child_of', self.ids)]).ids for id in self.ids} + for partner in self: + partner.issue_count = Issue.search_count([('partner_id', 'in', partners[partner.id])]) diff --git a/addons/project_issue/views/project_config_settings_views.xml b/addons/project_issue/views/project_config_settings_views.xml new file mode 100644 index 00000000000..3880f7235b1 --- /dev/null +++ b/addons/project_issue/views/project_config_settings_views.xml @@ -0,0 +1,17 @@ + + + + + project.config.settings.view.form.inherit.project.issue + project.config.settings + + + + + + + + + + + diff --git a/addons/rating/controllers/main.py b/addons/rating/controllers/main.py index e89001d1751..ed05f8d372c 100644 --- a/addons/rating/controllers/main.py +++ b/addons/rating/controllers/main.py @@ -8,16 +8,24 @@ from openerp.http import request class Rating(http.Controller): @http.route('/rating//', type='http', auth="public") - def add_rating(self, token, rate, **kwargs): - Rating = request.env['rating.rating'] - rating = Rating.search([('access_token', '=', token)]) - if rating: - is_rated = bool(rating.rating != -1) - if not is_rated: - request.env['rating.rating'].apply_rating(rate, token=token) - # redirect to the form view if logged person - if request.session.uid and not is_rated: - record = request.env[rating.res_model].browse(rating.res_id) - return werkzeug.utils.redirect('/web#model=%s&id=%s&view_type=form' % (record._name, record.id)) - return request.render('rating.rating_external_page_view', {'rating': rate, 'is_rated': is_rated}) + def open_rating(self, token, rate, **kwargs): + rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)]) + if not rating.consumed: + return request.render('rating.rating_external_page_submit', {'rating': rate, 'token': token}) + else: + return request.render('rating.rating_external_page_view', {'is_rated': True}) + return request.not_found() + + @http.route(['/rating///submit_feedback'], type="http", auth="public", method=['post']) + def submit_rating(self, token, rate, **kwargs): + rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)]) + if not rating.consumed: + record_sudo = request.env[rating.res_model].sudo().browse(rating.res_id) + record_sudo.rating_apply(rate, token=token, feedback=kwargs.get('feedback')) + # redirect to the form view if logged person + if request.session.uid: + return werkzeug.utils.redirect('/web#model=%s&id=%s&view_type=form' % (record_sudo._name, record_sudo.id)) + return request.render('rating.rating_external_page_view', {'is_public': True}) + else: + return request.render('rating.rating_external_page_view', {'is_rated': True}) return request.not_found() diff --git a/addons/rating/models/rating.py b/addons/rating/models/rating.py index beff19da27c..a723d2514e5 100644 --- a/addons/rating/models/rating.py +++ b/addons/rating/models/rating.py @@ -1,15 +1,16 @@ # -*- coding: utf-8 -*- import uuid -from openerp import api, fields, models, _ +from odoo import api, fields, models + class Rating(models.Model): _name = "rating.rating" _description = "Rating" - _order = 'create_date desc' + _order = 'write_date desc' _rec_name = 'res_name' _sql_constraints = [ - ('rating_range', 'check(rating >= -1 and rating <= 10)', 'Rating should be between -1 to 10'), + ('rating_range', 'check(rating >= 0 and rating <= 10)', 'Rating should be between 0 to 10'), ] @api.one @@ -27,99 +28,101 @@ class Rating(models.Model): res_id = fields.Integer(string='Document ID', required=True, help="Identifier of the rated object", index=True) rated_partner_id = fields.Many2one('res.partner', string="Rated Partner", help="Owner of the rated resource") partner_id = fields.Many2one('res.partner', string='Customer', help="Author of the rating") - rating = fields.Float(string="Rating", group_operator="avg", default=-1, help="Rating value") + rating = fields.Float(string="Rating", group_operator="avg", default=0, help="Rating value") feedback = fields.Text('Feedback reason', help="Reason of the rating") - access_token = fields.Char(string='Security Token', default=new_access_token, help="Access token to set the rating of the value") - message_id = fields.Many2one('mail.message', string="Linked message", help="Associated message when posting a review. Mainly used in website addons.", index=True) - - @api.model - def apply_rating(self, rate, res_model=None, res_id=None, token=None): - """ apply a rating for given res_model/res_id or token. If the res_model is a mail.thread - object, a message will be posted in the chatter. - :param rate : the rating value to apply - :type rate : float - :param res_id : id of the rated object. - :param res_model : name of model. - :param token : access token - :returns rating.rating record - """ - domain = [('access_token', '=', token)] if token else [('res_model', '=', res_model), ('res_id', '=', res_id)] - rating = self.search(domain, limit=1) - if rating: - if token: - rating = rating.sudo() - rating.write({'rating' : rate}) - if hasattr(self.env[rating.res_model], 'message_post'): - record = self.env[rating.res_model].sudo().browse(rating.res_id) - record.sudo().message_post( - body="%s %s
" - % (rating.sudo().partner_id.name, _('rated it'), rate), - subtype='mail.mt_comment', - author_id=rating.partner_id and rating.partner_id.id or None # None will set the default author in mail_thread.py - ) - return rating + access_token = fields.Char('Security Token', default=new_access_token, help="Access token to set the rating of the value") + consumed = fields.Boolean(string="Filled Rating", help="Enabled if the rating has been filled.") @api.multi def reset(self): for record in self: record.write({ - 'rating': -1, + 'rating': 0, 'access_token': record.new_access_token(), - 'feedback' : False + 'feedback': False, + 'consumed': False, }) - class RatingMixin(models.AbstractModel): - _name = 'rating.mixin' _description = "Rating Mixin" rating_ids = fields.One2many('rating.rating', 'res_id', string='Rating', domain=lambda self: [('res_model', '=', self._name)]) + rating_last_value = fields.Float('Last Value', related='rating_ids.rating', store=True) + rating_count = fields.Integer('Rating count', compute="_compute_rating_count") @api.multi - def rating_send_request(self, template, partner_id, rated_partner_id, reuse_rating=True): - """ This method create (empty) rating objects for the current recordsets - and send this request by mail (given email template) with the given - rated_partner_id and partner_id as recipient and sender of the email. - :param template : the email template to send. The res_model of the - template must be 'rating.rating'. - :type template : mail.template - :param res_model : model name of the object to rated_partner_id - :type res_model : string - :param res_id : id the record to rate - :type res_id : int - :param partner_id : the recipient partner - :type partner : res.partner - :param rated_partner_id : the sender partner - :type rated_partner_id : res.partner - :param reuse_rating : if True, the rating of the current objects will - be reset. Otherwise a new rating will be create - :type reuse_rating : boolean - """ - if not rated_partner_id.email or not partner_id.email: - return False - Rating = self.env['rating.rating'] - res_model = self._name + def _compute_rating_count(self): + read_group_res = self.env['rating.rating'].read_group( + [('res_model', '=', self._name), ('res_id', 'in', self.ids), ('consumed', '=', True)], + ['res_id'], groupby=['res_id']) + result = dict.fromkeys(self.ids, 0) + for data in read_group_res: + result[data['res_id']] += data['res_id_count'] for record in self: - res_id = record.id - values = { - 'res_model': res_model, - 'res_id': res_id, - 'partner_id': partner_id.id, - 'rated_partner_id': rated_partner_id.id - } - if reuse_rating: - # search the existing rating for the given res_model/res_id - rating = Rating.search([('res_id', '=', res_id), ('res_model', '=', res_model), ('partner_id', '=', partner_id.id)], limit=1) - if rating: # reset the rating - rating.reset() - else: # create a new one - rating = Rating.create(values) - else: - rating = Rating.create(values) - # send the mail - template.send_mail(rating.id, force_send=True) + record.rating_count = result[record.id] + + def rating_get_partner_id(self): + if hasattr(self, 'partner_id') and self.partner_id: + return self.partner_id + return self.env['res.partner'] + + def rating_get_rated_partner_id(self): + if hasattr(self, 'user_id') and self.user_id.partner_id: + return self.user_id.partner_id + return self.env['res.partner'] + + def rating_get_access_token(self, partner=None): + if not partner: + partner = self.rating_get_partner_id() + ratings = self.rating_ids.filtered(lambda x: x.partner_id == partner.id and not x.consumed) + if not ratings: + rating = self.env['rating.rating'].create({'partner_id': partner.id, 'res_model': self._name, 'res_id': self.id}) + else: + rating = ratings[0] + return rating.access_token + + @api.multi + def rating_send_request(self, template, partner=None, rated_partner=None, reuse_rating=True, force_send=True): + """ This method send rating request by email, using a template given + in parameter. """ + for record in self: + template.send_mail(record.id, force_send=force_send) + + @api.multi + def rating_apply(self, rate, token=None, feedback=None, subtype=None): + """ Apply a rating given a token. If the current model inherits from + mail.thread mixing, a message is posted on its chatter. + :param rate : the rating value to apply + :type rate : float + :param token : access token + :param feedback : additional feedback + :type feedback : string + :param subtype : subtype for mail + :type subtype : string + :returns rating.rating record + """ + Rating, rating = self.env['rating.rating'], None + if token: + rating = self.env['rating.rating'].search([('access_token', '=', token), ('consumed', '=', False)], limit=1) + else: + rating = Rating.search([('res_model', '=', self._name), ('res_id', '=', self.ids[0]), ('consumed', '=', False)], limit=1) + if rating: + rating.write({'rating': rate, 'feedback': feedback, 'consumed': True}) + if hasattr(self, 'message_post'): + self.message_post( + body="%s" + % (rate, '
' + feedback if feedback else ''), + subtype=subtype or "mail.mt_comment", + author_id=rating.partner_id and rating.partner_id.id or None # None will set the default author in mail_thread.py + ) + if hasattr(self, 'stage_id') and self.stage_id and hasattr(self.stage_id, 'auto_validation_kanban_state') and self.stage_id.auto_validation_kanban_state: + if rating.rating > 5: + self.write({'kanban_state': 'done'}) + else: + self.write({'kanban_state': 'blocked'}) + return rating @api.multi def rating_get_repartition(self, add_stats=False, domain=None): @@ -134,20 +137,20 @@ class RatingMixin(models.AbstractModel): 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 = [('res_model', '=', self._name), ('res_id', 'in', self.ids), ('rating', '>=', 0)] + base_domain = [('res_model', '=', self._name), ('res_id', 'in', self.ids), ('rating', '>=', 1), ('consumed', '=', True)] if domain: base_domain += domain data = self.env['rating.rating'].read_group(base_domain, ['rating'], ['rating', 'res_id']) - # init dict with all posible rate value, except -1 (no value for the rating) - values = dict.fromkeys(range(11), 0) + # init dict with all posible rate value, except 0 (no value for the rating) + values = dict.fromkeys(range(1, 11), 0) values.update((d['rating'], d['rating_count']) for d in data) # add other stats if add_stats: rating_number = sum(values.values()) result = { 'repartition': values, - 'avg': sum([float(key*values[key]) for key in values])/rating_number if rating_number > 0 else 0, - 'total': reduce(lambda x, y: y['rating_count']+x, data, 0), + 'avg': sum([float(key * values[key]) for key in values]) / rating_number if rating_number > 0 else 0, + 'total': reduce(lambda x, y: y['rating_count'] + x, data, 0), } return result return values @@ -185,7 +188,7 @@ class RatingMixin(models.AbstractModel): result = { 'avg': data['avg'], 'total': data['total'], - 'percent': dict.fromkeys(range(11), 0), + 'percent': dict.fromkeys(range(1, 11), 0), } for rate in data['repartition']: result['percent'][rate] = (data['repartition'][rate] * 100) / data['total'] if data['total'] > 0 else 0 diff --git a/addons/rating/static/src/img/rating_-1.png b/addons/rating/static/src/img/rating_-1.png deleted file mode 100644 index 8c32043915da457e76b15db5702fed781e89b64f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2495 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 + -
-

Thanks for voting !

-
-

Your vote has been taken into account.

-

It was

+
+
+

+

Sorry you have already voted!

+
+
+

+

Thank you for submitting your feedback!

-
-

Sorry you have already voted !

+ + + + diff --git a/addons/rating/views/rating_view.xml b/addons/rating/views/rating_view.xml index d123be23ed6..841ba0a19b6 100644 --- a/addons/rating/views/rating_view.xml +++ b/addons/rating/views/rating_view.xml @@ -7,25 +7,19 @@ - - - - - -
- - - +
+ +
+
+
by
+
for
+
on
+
@@ -38,7 +32,8 @@ rating.rating - + + @@ -49,7 +44,7 @@ rating.rating - + @@ -67,9 +62,11 @@ - + + + - + @@ -84,7 +81,8 @@ Rating rating.rating - graph,pivot,kanban + kanban,graph,pivot + [('consumed','=',True)]

There is no rating for this object at the moment. diff --git a/addons/rating_project/data/project_data.xml b/addons/rating_project/data/project_data.xml index 5fa22af596f..1332a726e39 100644 --- a/addons/rating_project/data/project_data.xml +++ b/addons/rating_project/data/project_data.xml @@ -1,51 +1,109 @@ + + Send Customer Rating Forms + days + -1 + project.project + _send_rating_all + + + Task Rating + project.task + + Ratings + + + + Task Rating + 14 + project.project + + + project_id + - Rating Project Request - ${object.rated_partner_id.email or ''} - ${object.res_name} - - ${object.partner_id.email} + Task: Rating Request + ${object.rating_get_rated_partner_id().email or '' | safe} + ${object.project_id.company_id.name}: Service Rating Request + + ${object.rating_get_partner_id().id} -

-
- Dear ${object.partner_id.name}, -

-
- We hope the services provided have met your expectations. -
If you have any more questions or comments, do not hesitate to reply to this e-mail. -

Please, help us to improve ourself by rating the service we provide. -

- - - - - - -
- - Great - - - - Okay - - - - Bad - -

-

Thank you

-

${object.rated_partner_id.name or ''}

-
- - ]]> -
+% set access_token = object.rating_get_access_token() +
+ + + + +
+ + + Satisfaction Survey +
+ + + + + + +
+

Hello,

+

Please take a moment to rate our services related to the task "${object.name}" + assigned to ${object.rating_get_rated_partner_id().name}.

+

We appreciate your feedback. It helps us to improve continuously.

+
+ + + + + + + +
+

+ Tell us how you feel about our service +

+
+ + + + + + +
+ + Great + + + + Okay + + + + Bad + +
+
+
${object.user_id.signature | safe}
+ + + + +
+ % if object.project_id.rating_status == 'stage': +

This customer survey has been sent because your task has been moved to the stage ${object.stage_id.name}

+ % endif + % if object.project_id.rating_status == 'periodic': +

This customer survey has been sent ${object.project_id.rating_status_period} as long as the task is in the ${object.stage_id.name} stage. + % endif +

Email automatically sent by Odoo Project for ${object.project_id.company_id.name}

+
+
+]]>
diff --git a/addons/rating_project/data/project_demo.xml b/addons/rating_project/data/project_demo.xml index 2155fde48e2..3ca1038951c 100644 --- a/addons/rating_project/data/project_demo.xml +++ b/addons/rating_project/data/project_demo.xml @@ -3,22 +3,32 @@ project.task + + project.task + + project.task + + project.task - + + + project.task + + @@ -26,11 +36,14 @@ + + stage + - - - - - + + + + + diff --git a/addons/rating_project/models/project.py b/addons/rating_project/models/project.py index 211a9daca6d..67b5eb02764 100644 --- a/addons/rating_project/models/project.py +++ b/addons/rating_project/models/project.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -from openerp import models, fields, api +from datetime import timedelta +from openerp import api, fields, models, _ class ProjectTaskType(models.Model): @@ -9,38 +10,62 @@ class ProjectTaskType(models.Model): rating_template_id = fields.Many2one( 'mail.template', string='Rating Email Template', - domain=[('model', '=', 'rating.rating')], + domain=[('model', '=', 'project.task')], help="Select an email template. An email will be sent to the customer when the task reach this step.") - auto_validation_kanban_state = fields.Boolean('Auto Kanban state validation', default=False, + auto_validation_kanban_state = fields.Boolean('Automatic kanban status', default=False, help="Automatically modify the kanban state when the customer reply to the feedback for this stage.\n" - " * A great feedback from the customer will update the kanban state to 'ready for the new stage' (green bullet).\n" + " * A good feedback from the customer will update the kanban state to 'ready for the new stage' (green bullet).\n" " * A medium or a bad feedback will set the kanban state to 'blocked' (red bullet).\n") -class Task(models.Model): +class Task(models.Model): _name = 'project.task' _inherit = ['project.task', 'rating.mixin'] @api.multi def write(self, values): + res = super(Task, self).write(values) if 'stage_id' in values and values.get('stage_id'): - template = self.env['project.task.type'].browse(values.get('stage_id')).rating_template_id - if template: - rated_partner_id = self.user_id.partner_id - partner_id = self.partner_id - if partner_id and rated_partner_id: - self.rating_send_request(template, partner_id, rated_partner_id) - return super(Task, self).write(values) + self.filtered(lambda x: x.project_id.rating_status == 'stage')._send_task_rating_mail() + return res + def _send_task_rating_mail(self): + for task in self: + rating_template = task.stage_id.rating_template_id + if rating_template: + task.rating_send_request(rating_template, reuse_rating=False) + + def _rating_get_partner_id(self): + res = super(Task, self)._rating_get_partner_id() + if not res and self.project_id.partner_id: + return self.project_id.partner_id + return res + + @api.multi + def rating_apply(self, rate, token=None, feedback=None, subtype=None): + return super(Task, self).rating_apply(rate, token=token, feedback=feedback, subtype="rating_project.mt_task_rating") class Project(models.Model): _inherit = "project.project" - @api.one + # This method should be called once a day by the scheduler + @api.model + def _send_rating_all(self): + projects = self.search([('rating_status', '=', 'periodic'), ('rating_request_deadline', '<=', fields.Datetime.now())]) + projects._send_rating_mail() + projects._compute_rating_request_deadline() + + def _send_rating_mail(self): + for project in self: + project.task_ids._send_task_rating_mail() + @api.depends('percentage_satisfaction_task') def _compute_percentage_satisfaction_project(self): - self.percentage_satisfaction_project = self.percentage_satisfaction_task + domain = [('create_date', '>=', fields.Datetime.to_string(fields.datetime.now() - timedelta(days=30)))] + for project in self: + activity = project.tasks.rating_get_grades(domain) + project.percentage_satisfaction_project = activity['great'] * 100 / sum(activity.values()) if sum(activity.values()) else -1 @api.one @api.depends('tasks.rating_ids.rating') @@ -52,38 +77,29 @@ class Project(models.Model): compute='_compute_percentage_satisfaction_task', string="Happy % on Task", store=True, default=-1) percentage_satisfaction_project = fields.Integer( compute="_compute_percentage_satisfaction_project", string="Happy % on Project", store=True, default=-1) - is_visible_happy_customer = fields.Boolean(string="Customer Satisfaction", default=False, - help="Display information about rating of the project on kanban and form view. This buttons will only be displayed if at least a rating exists.") + rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True) + rating_status = fields.Selection([('stage', 'Rating on Stage'), ('periodic', 'Periodical Rating')], 'Customer(s) Ratings', help="How to get the customer's feedbacks?\n" + "- Rating on stage: Email will be sent when a task/issue is pulled in another stage\n" + "- Periodical Rating: Email will be sent periodically\n\n" + "Don't forget to set up the mail templates on the stages for which you want to get the customer's feedbacks.") + rating_status_period = fields.Selection([ + ('daily', 'Daily'), ('weekly', 'Weekly'), ('bimonthly', 'Twice a Month'), + ('monthly', 'Once a Month'), ('quarterly', 'Quarterly'), ('yearly', 'Yearly') + ], 'Rating Frequency') + @api.depends('rating_status', 'rating_status_period') + def _compute_rating_request_deadline(self): + periods = {'daily': 1, 'weekly': 7, 'bimonthly': 15, 'monthly': 30, 'quarterly': 90, 'yearly': 365} + for project in self: + project.rating_request_deadline = fields.datetime.now() + timedelta(days=periods.get(project.rating_status_period, 0)) @api.multi def action_view_task_rating(self): """ return the action to see all the rating about the tasks of the project """ action = self.env['ir.actions.act_window'].for_xml_id('rating', 'action_view_rating') - return dict(action, domain=[('rating', '!=', -1), ('res_id', 'in', self.tasks.ids), ('res_model', '=', 'project.task')]) + return dict(action, domain=[('res_id', 'in', self.tasks.ids), ('res_model', '=', 'project.task')]) @api.multi def action_view_all_rating(self): """ return the action to see all the rating about the all sort of activity of the project (tasks, issues, ...) """ return self.action_view_task_rating() - - - -class Rating(models.Model): - - _inherit = "rating.rating" - - @api.model - def apply_rating(self, rate, res_model=None, res_id=None, token=None): - """ check if the auto_validation_kanban_state is activated. If so, apply the modification of the - kanban state according to the given rating. - """ - rating = super(Rating, self).apply_rating(rate, res_model, res_id, token) - if rating.res_model == 'project.task': - task = self.env[rating.res_model].sudo().browse(rating.res_id) - if task.stage_id.auto_validation_kanban_state: - if rating.rating > 5: - task.write({'kanban_state' : 'done'}) - else: - task.write({'kanban_state' : 'blocked'}) - return rating diff --git a/addons/rating_project/views/project_dashboard.xml b/addons/rating_project/views/project_dashboard.xml index 9c028a12f54..1188ddd4560 100644 --- a/addons/rating_project/views/project_dashboard.xml +++ b/addons/rating_project/views/project_dashboard.xml @@ -6,15 +6,26 @@ - + - -
- -
- % -
+ +
+ + + + No rating + + + % + + +
+
+ +
diff --git a/addons/rating_project/views/project_view.xml b/addons/rating_project/views/project_view.xml index baf7f911fcb..466581cbabb 100644 --- a/addons/rating_project/views/project_view.xml +++ b/addons/rating_project/views/project_view.xml @@ -1,6 +1,52 @@ + + + Customer Ratings + rating.rating + kanban,pivot,graph + [('res_model', '=', 'project.task'), ('res_id', '=', active_id), ('consumed', '=', True)] + +

+ Customer ratings on tasks. If you have no rating, change your project Settings to activate it. +

+
+
+ + + project.task.view.form.inherit.rating + project.task + + + + + + + + + + project.task.view.kanban.inherit.rating + project.task + + + + + + + + + + + + + + + + project.project.rating.form.view @@ -10,15 +56,21 @@
- +
@@ -37,5 +89,18 @@ + + Customers Ratings + rating.rating + kanban,pivot,graph + [('res_model','in',['project.task', 'project.issue']), ('consumed', '=', True)] + +

+ Customer ratings on tasks/issues. If you have no rating, change your project Settings to activate it. +

+
+
+ +
\ No newline at end of file diff --git a/addons/rating_project_issue/__openerp__.py b/addons/rating_project_issue/__openerp__.py index 110e6dd578e..b43f64909d3 100644 --- a/addons/rating_project_issue/__openerp__.py +++ b/addons/rating_project_issue/__openerp__.py @@ -12,6 +12,7 @@ This module allows a customer to give rating on Project Issue. 'project_issue' ], 'data': [ + 'data/project_issue_data.xml', 'views/project_issue_view.xml', ], 'demo': [ diff --git a/addons/rating_project_issue/data/project_issue_data.xml b/addons/rating_project_issue/data/project_issue_data.xml new file mode 100644 index 00000000000..f247aaae5bd --- /dev/null +++ b/addons/rating_project_issue/data/project_issue_data.xml @@ -0,0 +1,20 @@ + + + + + Issue Rating + project.issue + + Ratings + + + + Issue Rating + 24 + project.project + + + project_id + + + diff --git a/addons/rating_project_issue/data/project_issue_demo.xml b/addons/rating_project_issue/data/project_issue_demo.xml index 11173fbf07b..d291263d397 100644 --- a/addons/rating_project_issue/data/project_issue_demo.xml +++ b/addons/rating_project_issue/data/project_issue_demo.xml @@ -3,22 +3,25 @@ project.issue + + project.issue + + project.issue + + - - - + + + diff --git a/addons/rating_project_issue/models/project_issue.py b/addons/rating_project_issue/models/project_issue.py index ba39e9b4320..32b5c149df7 100644 --- a/addons/rating_project_issue/models/project_issue.py +++ b/addons/rating_project_issue/models/project_issue.py @@ -1,65 +1,54 @@ # -*- coding: utf-8 -*- +from datetime import timedelta from openerp import api, fields, models class ProjectIssue(models.Model): - _name = "project.issue" _inherit = ['project.issue', 'rating.mixin'] @api.multi def write(self, values): + res = super(ProjectIssue, self).write(values) if 'stage_id' in values and values.get('stage_id'): - template = self.env['project.task.type'].browse(values.get('stage_id')).rating_template_id - if template: - rated_partner_id = self.user_id.partner_id - partner_id = self.partner_id - if partner_id and rated_partner_id: - self.rating_send_request(template, partner_id, rated_partner_id) - return super(ProjectIssue, self).write(values) + self.filtered(lambda x: x.project_id.rating_status == 'stage')._send_issue_rating_mail() + return res + + def _send_issue_rating_mail(self): + for issue in self: + rating_template = issue.stage_id.rating_template_id + if rating_template: + issue.rating_send_request(rating_template, reuse_rating=False) + + @api.multi + def rating_apply(self, rate, token=None, feedback=None, subtype=None): + return super(ProjectIssue, self).rating_apply(rate, token=token, feedback=feedback, subtype="rating_project_issue.mt_issue_rating") class Project(models.Model): _inherit = "project.project" - @api.multi + def _send_rating_mail(self): + super(Project, self)._send_rating_mail() + for project in self: + project.issue_ids._send_issue_rating_mail() + @api.depends('percentage_satisfaction_task', 'percentage_satisfaction_issue') def _compute_percentage_satisfaction_project(self): super(Project, self)._compute_percentage_satisfaction_project() - Rating = self.env['rating.rating'] - Issue = self.env['project.issue'] - for record in self.filtered(lambda record: record.use_tasks or record.use_issues): - if record.use_tasks or record.use_issues: - # built the domain according the project parameters (use tasks and/or issues) - res_models = [] - domain = [] - if record.use_tasks: - res_models.append('project.task') - domain += ['&', ('res_model', '=', 'project.task'), ('res_id', 'in', record.tasks.ids)] - if record.use_issues: - # TODO: if performance issue, compute the satisfaction with a custom request joining rating and task/issue. - issues = Issue.search([('project_id', '=', record.id)]) - res_models.append('project.issue') - domain += ['&', ('res_model', '=', 'project.issue'), ('res_id', 'in', issues.ids)] - if len(res_models) == 2: - domain = ['|'] + domain - domain = ['&', ('rating', '>=', 0)] + domain - # get the number of rated tasks and issues with a read_group (more perfomant !) - grouped_data = Rating.read_group(domain, ['res_model'], ['res_model']) - # compute the number of each model and total number - res = dict.fromkeys(res_models, 0) - for data in grouped_data: - res[data['res_model']] += data['res_model_count'] - nbr_rated_task = res.get('project.task', 0) - nbr_rated_issue = res.get('project.issue', 0) - nbr_project_rating = nbr_rated_issue + nbr_rated_task - # compute the weighted arithmetic average - ratio_task = float(nbr_rated_task) / float(nbr_project_rating) if nbr_project_rating else 0 - ratio_issue = float(nbr_rated_issue) / float(nbr_project_rating) if nbr_project_rating else 0 - record.percentage_satisfaction_project = round((ratio_task*record.percentage_satisfaction_task)+(ratio_issue*record.percentage_satisfaction_issue)) if nbr_project_rating else -1 - else: - record.percentage_satisfaction_project = -1 + for project in self: + domain = [('create_date', '>=', fields.Datetime.to_string(fields.datetime.now() - timedelta(days=30)))] + activity_great, activity_sum = 0, 0 + if project.use_tasks: + activity_task = project.tasks.rating_get_grades(domain) + activity_great = activity_task['great'] + activity_sum = sum(activity_task.values()) + if project.use_issues: + activity_issue = project.issue_ids.rating_get_grades(domain) + activity_great += activity_issue['great'] + activity_sum += sum(activity_issue.values()) + project.percentage_satisfaction_project = activity_great * 100 / activity_sum if activity_sum else -1 @api.one @api.depends('issue_ids.rating_ids.rating') @@ -80,7 +69,7 @@ class Project(models.Model): @api.multi def action_view_all_rating(self): action = super(Project, self).action_view_all_rating() - task_domain = action['domain'][1:] # remove the (rating != -1) condition + task_domain = action['domain'] domain = [] if self.use_tasks: # add task domain, if neeeded domain = ['&'] + task_domain @@ -89,26 +78,4 @@ class Project(models.Model): domain = domain + ['&', ('res_id', 'in', issues.ids), ('res_model', '=', 'project.issue')] if self.use_tasks and self.use_issues: domain = ['|'] + domain - domain = [('rating', '!=', -1)] + domain # prepend the condition to avoid empty rating return dict(action, domain=domain) - - - -class Rating(models.Model): - - _inherit = "rating.rating" - - @api.model - def apply_rating(self, rate, res_model=None, res_id=None, token=None): - """ check if the auto_validation_kanban_state is activated. If so, apply the modification of the - kanban state according to the given rating. - """ - rating = super(Rating, self).apply_rating(rate, res_model, res_id, token) - if rating.res_model == 'project.issue': - issue = self.env[rating.res_model].sudo().browse(rating.res_id) - if issue.stage_id.auto_validation_kanban_state: - if rating.rating > 5: - issue.write({'kanban_state' : 'done'}) - else: - issue.write({'kanban_state' : 'blocked'}) - return rating diff --git a/addons/rating_project_issue/views/project_issue_view.xml b/addons/rating_project_issue/views/project_issue_view.xml index 660886e8b80..33b774eaf2d 100644 --- a/addons/rating_project_issue/views/project_issue_view.xml +++ b/addons/rating_project_issue/views/project_issue_view.xml @@ -1,6 +1,52 @@ + + + Customer Ratings + rating.rating + kanban,pivot,graph + [('res_model','=','project.issue'),('res_id','=',active_id), ('consumed', '=', True)] + +

+ Customer ratings on issues. If you have no rating, change your project Settings to activate it. +

+
+
+ + + project.issue.view.form.inherit.rating + project.issue + + + + + + + + + + project.issue.view.kanban.inherit.rating + project.issue + + + + + + + + + + + + + + + + project.issue.view.rating @@ -10,11 +56,11 @@
diff --git a/addons/sale_service_rating/__init__.py b/addons/sale_service_rating/__init__.py new file mode 100644 index 00000000000..81fc81d29c9 --- /dev/null +++ b/addons/sale_service_rating/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import models diff --git a/addons/sale_service_rating/__openerp__.py b/addons/sale_service_rating/__openerp__.py new file mode 100644 index 00000000000..8de367c2944 --- /dev/null +++ b/addons/sale_service_rating/__openerp__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Sale Service Rating', + 'category': 'Hidden', + 'description': """ +This module allows a customer to give rating on task which are created from sale order. +""", + 'depends': [ + 'rating_project', + 'sale_timesheet' + ], + 'auto_install': True, +} diff --git a/addons/sale_service_rating/models/__init__.py b/addons/sale_service_rating/models/__init__.py new file mode 100644 index 00000000000..f08438b04dc --- /dev/null +++ b/addons/sale_service_rating/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import project diff --git a/addons/sale_service_rating/models/project.py b/addons/sale_service_rating/models/project.py new file mode 100644 index 00000000000..f6afa1b4bb9 --- /dev/null +++ b/addons/sale_service_rating/models/project.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class Task(models.Model): + _inherit = 'project.task' + + def rating_get_partner_id(self): + if self.partner_id or self.self.order_id.partner_id: + return self.partner_id or self.sale_line_id.order_id.partner_id + return super(Task, self).rating_get_partner_id() diff --git a/addons/website_livechat/views/website_livechat.xml b/addons/website_livechat/views/website_livechat.xml index fe1b71550c2..44cb90a809e 100644 --- a/addons/website_livechat/views/website_livechat.xml +++ b/addons/website_livechat/views/website_livechat.xml @@ -88,7 +88,7 @@
- +

diff --git a/addons/website_rating_project_issue/controllers/main.py b/addons/website_rating_project_issue/controllers/main.py index 9d1bbb58d61..ceae1822438 100644 --- a/addons/website_rating_project_issue/controllers/main.py +++ b/addons/website_rating_project_issue/controllers/main.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from openerp import _ from openerp.addons.web import http from openerp.addons.web.http import request @@ -11,8 +10,8 @@ class WebsiteRatingProject(http.Controller): @http.route(['/project/rating/'], type='http', auth="public", website=True) def index(self, **kw): - projects = request.env['project.project'].sudo().search([('is_visible_happy_customer', '=', True), ('website_published', '=', True)]) - values = {'projects' : projects} + projects = request.env['project.project'].sudo().search([('rating_status', '!=', False), ('website_published', '=', True)]) + values = {'projects': projects} return request.website.render('website_rating_project_issue.index', values) @http.route(['/project/rating/'], type='http', auth="public", website=True) @@ -21,25 +20,29 @@ class WebsiteRatingProject(http.Controller): project = request.env['project.project'].sudo().browse(project_id) # to avoid giving any access rights on projects to the public user, let's use sudo # and check if the user should be able to view the project (project managers only if it's unpublished or has no rating) - if not (project.is_visible_happy_customer and project.website_published) and not user.sudo(user).has_group('project.group_project_manager'): + if not (project.rating_status and project.website_published) and not user.sudo(user).has_group('project.group_project_manager'): raise NotFound() - issues = request.env['project.issue'].sudo().search([('project_id', '=', project.id)]) - domain = [('res_model', '=', 'project.issue'), ('res_id', 'in', issues.ids)] - ratings = request.env['rating.rating'].search(domain, order="id desc", limit=100) - - yesterday = (datetime.date.today()-datetime.timedelta(days=-1)).strftime('%Y-%m-%d 23:59:59') - stats = {} - for x in (7, 30, 90): - todate = (datetime.date.today()-datetime.timedelta(days=x)).strftime('%Y-%m-%d 00:00:00') - domdate = domain + [('create_date', '<=', yesterday), ('create_date', '>=', todate)] - stats[x] = {0: 0, 5: 0, 10: 0} - rating_stats = request.env['rating.rating'].read_group(domdate, [], ['rating']) - total = reduce(lambda x, y: y['rating_count']+x, rating_stats, 0) - for rate in rating_stats: - stats[x][rate['rating']] = (rate['rating_count'] * 100) / total values = { 'project': project, - 'ratings': ratings, - 'stats': stats, + 'task_data': self._calculate_rating(project.id, "project.task"), + 'issue_data': self._calculate_rating(project.id, "project.issue"), } return request.website.render('website_rating_project_issue.project_rating_page', values) + + def _calculate_rating(self, project_id, model_name): + # Calculate rating for Tasks and Issues + records = request.env[model_name].sudo().search([('project_id', '=', project_id)]) + domain = [('res_model', '=', model_name), ('res_id', 'in', records.ids), ('consumed', '=', True)] + ratings = request.env['rating.rating'].search(domain, order="id desc", limit=100) + + yesterday = (datetime.date.today() - datetime.timedelta(days=-1)).strftime('%Y-%m-%d 23:59:59') + stats = {} + for x in (7, 30, 90): + todate = (datetime.date.today() - datetime.timedelta(days=x)).strftime('%Y-%m-%d 00:00:00') + domdate = domain + [('create_date', '<=', yesterday), ('create_date', '>=', todate)] + stats[x] = {1: 0, 5: 0, 10: 0} + rating_stats = request.env['rating.rating'].read_group(domdate, [], ['rating']) + total = reduce(lambda x, y: y['rating_count'] + x, rating_stats, 0) + for rate in rating_stats: + stats[x][rate['rating']] = float("%.2f" % (rate['rating_count'] * 100.0 / total)) + return {'ratings': ratings, 'stats': stats} diff --git a/addons/website_rating_project_issue/static/src/css/website_rating_project_issue.css b/addons/website_rating_project_issue/static/src/css/website_rating_project_issue.css index 7fdf57da316..6f5c549cc9a 100644 --- a/addons/website_rating_project_issue/static/src/css/website_rating_project_issue.css +++ b/addons/website_rating_project_issue/static/src/css/website_rating_project_issue.css @@ -1,11 +1,3 @@ -.oe_website_rating_project .oe_rate{ - clear: both; -} -.oe_website_rating_project .oe_rate img { - margin-right: 6px; -} - .oe_website_rating_project .thumbnail{ height: 355px; } - diff --git a/addons/website_rating_project_issue/views/project_project_view.xml b/addons/website_rating_project_issue/views/project_project_view.xml index 2549b244bc0..dd25a692404 100644 --- a/addons/website_rating_project_issue/views/project_project_view.xml +++ b/addons/website_rating_project_issue/views/project_project_view.xml @@ -6,7 +6,7 @@ diff --git a/addons/website_rating_project_issue/views/website_rating_project.xml b/addons/website_rating_project_issue/views/website_rating_project.xml index 3b3a0267127..0e76e53ac52 100644 --- a/addons/website_rating_project_issue/views/website_rating_project.xml +++ b/addons/website_rating_project_issue/views/website_rating_project.xml @@ -71,30 +71,48 @@ + +