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 8c32043915d..00000000000 Binary files a/addons/rating/static/src/img/rating_-1.png and /dev/null differ diff --git a/addons/rating/static/src/img/rating_0.png b/addons/rating/static/src/img/rating_0.png index 3afa74fa3d8..8c32043915d 100644 Binary files a/addons/rating/static/src/img/rating_0.png and b/addons/rating/static/src/img/rating_0.png differ diff --git a/addons/rating/static/src/img/rating_1.png b/addons/rating/static/src/img/rating_1.png new file mode 100644 index 00000000000..3afa74fa3d8 Binary files /dev/null and b/addons/rating/static/src/img/rating_1.png differ diff --git a/addons/rating/views/rating_template.xml b/addons/rating/views/rating_template.xml index 8f84562265e..a3f6a4424b9 100644 --- a/addons/rating/views/rating_template.xml +++ b/addons/rating/views/rating_template.xml @@ -98,16 +98,44 @@ + + + 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 @@ + +