[IMP] customer ratings flow improvements

- Rename the 'Use Rating on Project' feature into 'Customer Ratings'
- Rename the 'Set Email Template to Stages' link to 'Set a Rating Email Template on Stages'
- Add an optional list view for the Stages menu
- display warning if the rating_template_id field is set and if one of the selected project_ids doesn't have the rating_status field set to true
- Project form view revamp
- rename the '% on tasks' stat button into 'Customer Satisfaction'
- Remove the 'no option' for the rating frequency field because it is required
- project form : Add a 'Go to Website' stat button
- Project dashboard: remove the 'Customer Ratings' menu item in more
- Ratings page: the 'Last 30 days' filter include ratings from today
- remove the Appointment / Helpdesk Customer Satisfaction / Live Support menu items

TASK ID : 1251
This commit is contained in:
Benjamin Frantzen (bfr)
2020-05-08 12:17:20 +00:00
committed by Yannick Tivisse
parent f427a38c4e
commit e4a4ffb974
46 changed files with 407 additions and 324 deletions
@@ -31,7 +31,7 @@
<field eval="False" name="partner_id"/> <field eval="False" name="partner_id"/>
<field eval="True" name="consumed"/> <field eval="True" name="consumed"/>
</record> </record>
<function eval="([ref('im_livechat.im_livechat_mail_channel_data_0')], 10)" model="mail.channel" name="rating_apply"/> <function eval="([ref('im_livechat.im_livechat_mail_channel_data_0')], 5)" model="mail.channel" name="rating_apply"/>
<record id="im_livechat_mail_message_5_data" model="mail.message"> <record id="im_livechat_mail_message_5_data" model="mail.message">
<field name="model">mail.channel</field> <field name="model">mail.channel</field>
<field name="res_id" ref="im_livechat.im_livechat_mail_channel_data_0"/> <field name="res_id" ref="im_livechat.im_livechat_mail_channel_data_0"/>
@@ -92,7 +92,7 @@
<field eval="False" name="partner_id"/> <field eval="False" name="partner_id"/>
<field eval="True" name="consumed"/> <field eval="True" name="consumed"/>
</record> </record>
<function eval="([ref('im_livechat.im_livechat_mail_channel_data_1')], 10)" model="mail.channel" name="rating_apply"/> <function eval="([ref('im_livechat.im_livechat_mail_channel_data_1')], 5)" model="mail.channel" name="rating_apply"/>
<record id="im_livechat_mail_message_10_data" model="mail.message"> <record id="im_livechat_mail_message_10_data" model="mail.message">
<field name="model">mail.channel</field> <field name="model">mail.channel</field>
<field name="res_id" ref="im_livechat.im_livechat_mail_channel_data_1"/> <field name="res_id" ref="im_livechat.im_livechat_mail_channel_data_1"/>
@@ -205,7 +205,7 @@
<field name="partner_id" ref="base.partner_demo_portal"/> <field name="partner_id" ref="base.partner_demo_portal"/>
<field eval="True" name="consumed"/> <field eval="True" name="consumed"/>
</record> </record>
<function eval="([ref('im_livechat.im_livechat_mail_channel_data_3')], 10)" model="mail.channel" name="rating_apply"/> <function eval="([ref('im_livechat.im_livechat_mail_channel_data_3')], 5)" model="mail.channel" name="rating_apply"/>
<record id="im_livechat_mail_message_17_data" model="mail.message"> <record id="im_livechat_mail_message_17_data" model="mail.message">
<field name="model">mail.channel</field> <field name="model">mail.channel</field>
<field name="res_id" ref="im_livechat.im_livechat_mail_channel_data_3"/> <field name="res_id" ref="im_livechat.im_livechat_mail_channel_data_3"/>
@@ -300,7 +300,7 @@
<field eval="False" name="partner_id"/> <field eval="False" name="partner_id"/>
<field eval="True" name="consumed"/> <field eval="True" name="consumed"/>
</record> </record>
<function eval="([ref('im_livechat.im_livechat_mail_channel_data_5')], 10)" model="mail.channel" name="rating_apply"/> <function eval="([ref('im_livechat.im_livechat_mail_channel_data_5')], 5)" model="mail.channel" name="rating_apply"/>
<record id="im_livechat_mail_message_25_data" model="mail.message"> <record id="im_livechat_mail_message_25_data" model="mail.message">
<field name="model">mail.channel</field> <field name="model">mail.channel</field>
<field name="res_id" ref="im_livechat.im_livechat_mail_channel_data_5"/> <field name="res_id" ref="im_livechat.im_livechat_mail_channel_data_5"/>
@@ -352,7 +352,7 @@
<field name="partner_id" ref="base.partner_demo_portal"/> <field name="partner_id" ref="base.partner_demo_portal"/>
<field eval="True" name="consumed"/> <field eval="True" name="consumed"/>
</record> </record>
<function eval="([ref('im_livechat.im_livechat_mail_channel_data_6')], 10)" model="mail.channel" name="rating_apply"/> <function eval="([ref('im_livechat.im_livechat_mail_channel_data_6')], 5)" model="mail.channel" name="rating_apply"/>
<record id="im_livechat_mail_message_29_data" model="mail.message"> <record id="im_livechat_mail_message_29_data" model="mail.message">
<field name="model">mail.channel</field> <field name="model">mail.channel</field>
<field name="res_id" ref="im_livechat.im_livechat_mail_channel_data_6"/> <field name="res_id" ref="im_livechat.im_livechat_mail_channel_data_6"/>
@@ -404,7 +404,7 @@
<field eval="False" name="partner_id"/> <field eval="False" name="partner_id"/>
<field eval="True" name="consumed"/> <field eval="True" name="consumed"/>
</record> </record>
<function eval="([ref('im_livechat.im_livechat_mail_channel_data_7')], 10)" model="mail.channel" name="rating_apply"/> <function eval="([ref('im_livechat.im_livechat_mail_channel_data_7')], 5)" model="mail.channel" name="rating_apply"/>
<record id="im_livechat_mail_message_33_data" model="mail.message"> <record id="im_livechat_mail_message_33_data" model="mail.message">
<field name="model">mail.channel</field> <field name="model">mail.channel</field>
<field name="res_id" ref="im_livechat.im_livechat_mail_channel_data_7"/> <field name="res_id" ref="im_livechat.im_livechat_mail_channel_data_7"/>
@@ -514,7 +514,7 @@
<field name="partner_id" ref="base.partner_admin"/> <field name="partner_id" ref="base.partner_admin"/>
<field name="create_date" eval="datetime.now() - timedelta(days=1, seconds=-53)"/> <field name="create_date" eval="datetime.now() - timedelta(days=1, seconds=-53)"/>
<field name="res_id" ref="im_livechat.mail_channel_livechat_1"/> <field name="res_id" ref="im_livechat.mail_channel_livechat_1"/>
<field name="rating" eval="10"/> <field name="rating" eval="5"/>
<field name="consumed" eval="True"/> <field name="consumed" eval="True"/>
</record> </record>
@@ -72,14 +72,14 @@ class ImLivechatReportChannel(models.Model):
END as is_anonymous, END as is_anonymous,
C.country_id, C.country_id,
CASE CASE
WHEN rate.rating = 10 THEN 1 WHEN rate.rating = 5 THEN 1
ELSE 0 ELSE 0
END as is_happy, END as is_happy,
Rate.rating as rating, Rate.rating as rating,
CASE CASE
WHEN Rate.rating = 1 THEN 'Unhappy' WHEN Rate.rating = 1 THEN 'Unhappy'
WHEN Rate.rating = 10 THEN 'Happy' WHEN Rate.rating = 5 THEN 'Happy'
WHEN Rate.rating = 5 THEN 'Neutral' WHEN Rate.rating = 3 THEN 'Neutral'
ELSE null ELSE null
END as rating_text, END as rating_text,
CASE CASE
@@ -525,7 +525,7 @@ var Feedback = Widget.extend({
this.$('.o_livechat_rating_choices img[data-value="'+this.rating+'"]').addClass('selected'); this.$('.o_livechat_rating_choices img[data-value="'+this.rating+'"]').addClass('selected');
// only display textearea if bad smiley selected // only display textearea if bad smiley selected
if (this.rating !== 10) { if (this.rating !== 5) {
this.$('.o_livechat_rating_reason').show(); this.$('.o_livechat_rating_reason').show();
} else { } else {
this.$('.o_livechat_rating_reason').hide(); this.$('.o_livechat_rating_reason').hide();
@@ -9,8 +9,8 @@
Did we correctly answer your question ? Did we correctly answer your question ?
</div> </div>
<div class="o_livechat_rating_choices"> <div class="o_livechat_rating_choices">
<img t-att-src="widget.server_origin + '/rating/static/src/img/rating_10.png'" alt="Good" data-value="10"/> <img t-att-src="widget.server_origin + '/rating/static/src/img/rating_5.png'" alt="Good" data-value="5"/>
<img t-att-src="widget.server_origin + '/rating/static/src/img/rating_5.png'" alt="OK" data-value="5"/> <img t-att-src="widget.server_origin + '/rating/static/src/img/rating_3.png'" alt="OK" data-value="3"/>
<img t-att-src="widget.server_origin + '/rating/static/src/img/rating_1.png'" alt="Bad" data-value="1" /> <img t-att-src="widget.server_origin + '/rating/static/src/img/rating_1.png'" alt="Bad" data-value="1" />
</div> </div>
</div> </div>
@@ -9,7 +9,6 @@ var time = require('web.time');
var _t = core._t; var _t = core._t;
var PortalChatter = portalChatter.PortalChatter; var PortalChatter = portalChatter.PortalChatter;
var qweb = core.qweb; var qweb = core.qweb;
var STAR_RATING_RATIO = 2; // conversion factor from the star (1-5) to the db rating range (1-10)
/** /**
* PortalChatter * PortalChatter
@@ -67,7 +66,7 @@ PortalChatter.include({
messages = this._super.apply(this, arguments); messages = this._super.apply(this, arguments);
if (this.options['display_rating']) { if (this.options['display_rating']) {
_.each(messages, function (m, i) { _.each(messages, function (m, i) {
m.rating_value = self.roundToHalf(m['rating_value'] / STAR_RATING_RATIO); m.rating_value = self.roundToHalf(m['rating_value']);
m.rating = self._preprocessCommentData(m.rating, i); m.rating = self._preprocessCommentData(m.rating, i);
}); });
} }
@@ -114,13 +113,13 @@ PortalChatter.include({
return; return;
} }
var ratingData = { var ratingData = {
'avg': Math.round(result['rating_stats']['avg'] / STAR_RATING_RATIO * 100) / 100, 'avg': Math.round(result['rating_stats']['avg'] * 100) / 100,
'percent': [], 'percent': [],
}; };
_.each(_.keys(result['rating_stats']['percent']).reverse(), function (rating) { _.each(_.keys(result['rating_stats']['percent']).reverse(), function (rating) {
if (rating % 2 === 0) { if (rating % 2 === 0) {
ratingData['percent'].push({ ratingData['percent'].push({
'num': rating / STAR_RATING_RATIO, 'num': rating,
'percent': utils.round_precision(result['rating_stats']['percent'][rating], 0.01), 'percent': utils.round_precision(result['rating_stats']['percent'][rating], 0.01),
}); });
} }
@@ -347,7 +346,7 @@ PortalChatter.include({
_onChangeRatingDomain: function () { _onChangeRatingDomain: function () {
var domain = []; var domain = [];
if (this.get('rating_value')) { if (this.get('rating_value')) {
domain = [['rating_value', '=', this.get('rating_value') * STAR_RATING_RATIO]]; domain = [['rating_value', '=', this.get('rating_value')]];
} }
this._changeCurrentPage(1, domain); this._changeCurrentPage(1, domain);
}, },
@@ -8,8 +8,6 @@ var _t = core._t;
var PortalComposer = portalComposer.PortalComposer; var PortalComposer = portalComposer.PortalComposer;
var STAR_RATING_RATIO = 2; // conversion factor from the star (1-5) to the db rating range (1-10)
/** /**
* PortalComposer * PortalComposer
* *
@@ -31,7 +29,7 @@ PortalComposer.include({
// apply ratio to default rating value // apply ratio to default rating value
if (options.default_rating_value) { if (options.default_rating_value) {
options.default_rating_value = parseFloat(options.default_rating_value) / STAR_RATING_RATIO; options.default_rating_value = parseFloat(options.default_rating_value);
} }
// default options // default options
@@ -66,7 +64,7 @@ PortalComposer.include({
// set the default value to trigger the display of star widget and update the hidden input value. // set the default value to trigger the display of star widget and update the hidden input value.
self.set("star_value", self.options.default_rating_value); self.set("star_value", self.options.default_rating_value);
self.$input.val(self.options.default_rating_value * STAR_RATING_RATIO); self.$input.val(self.options.default_rating_value);
}); });
}, },
@@ -97,7 +95,7 @@ PortalComposer.include({
var index = this.$('.stars i').index(ev.currentTarget); var index = this.$('.stars i').index(ev.currentTarget);
this.set("star_value", index + 1); this.set("star_value", index + 1);
this.user_click = true; this.user_click = true;
this.$input.val(this.get("star_value") * STAR_RATING_RATIO); this.$input.val(this.get("star_value"));
}, },
/** /**
* @private * @private
@@ -7,8 +7,6 @@ var portalComposer = require('portal.composer');
var PortalComposer = portalComposer.PortalComposer; var PortalComposer = portalComposer.PortalComposer;
var STAR_RATING_RATIO = 2; // conversion factor from the star (1-5) to the db rating range (1-10)
/** /**
* RatingPopupComposer * RatingPopupComposer
* *
@@ -25,7 +23,7 @@ var RatingPopupComposer = publicWidget.Widget.extend({
init: function (parent, options) { init: function (parent, options) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.rating_avg = Math.round(options['ratingAvg'] / STAR_RATING_RATIO * 100) / 100 || 0.0; this.rating_avg = Math.round(options['ratingAvg'] / 100) / 100 || 0.0;
this.rating_total = options['ratingTotal'] || 0.0; this.rating_total = options['ratingTotal'] || 0.0;
this.options = _.defaults({}, options, { this.options = _.defaults({}, options, {
@@ -4,7 +4,7 @@
Rating widget static: show 5 stars (full or empty) regarding the given rating_avg and rating_count Rating widget static: show 5 stars (full or empty) regarding the given rating_avg and rating_count
--> -->
<template id="rating_widget_stars_static" name="Rating: static star widget"> <template id="rating_widget_stars_static" name="Rating: static star widget">
<t t-set="rating_avg" t-value="round(rating_avg * 100) / 100 / 2"/> <t t-set="rating_avg" t-value="round(rating_avg * 100) / 100"/>
<t t-set="val_decimal" t-value="round(rating_avg % 1, 1)"/> <t t-set="val_decimal" t-value="round(rating_avg % 1, 1)"/>
<t t-set="val_integer" t-value="int(rating_avg)"/> <t t-set="val_integer" t-value="int(rating_avg)"/>
<t t-set="empty_star" t-value="5 - (val_integer+1) if val_decimal else 5 - val_integer"/> <t t-set="empty_star" t-value="5 - (val_integer+1) if val_decimal else 5 - val_integer"/>
-1
View File
@@ -33,7 +33,6 @@
'views/mail_activity_views.xml', 'views/mail_activity_views.xml',
'views/project_assets.xml', 'views/project_assets.xml',
'views/project_portal_templates.xml', 'views/project_portal_templates.xml',
'views/project_rating_templates.xml',
'data/digest_data.xml', 'data/digest_data.xml',
'data/project_mail_template_data.xml', 'data/project_mail_template_data.xml',
'data/project_data.xml', 'data/project_data.xml',
-1
View File
@@ -2,4 +2,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import portal from . import portal
from . import rating
+1
View File
@@ -95,6 +95,7 @@
<field name="alias_defaults">{'project_id': 1}</field> <field name="alias_defaults">{'project_id': 1}</field>
<field name="alias_force_thread_id">0</field> <field name="alias_force_thread_id">0</field>
<field name="alias_parent_model_id" ref="model_project_project"/> <field name="alias_parent_model_id" ref="model_project_project"/>
<field name="rating_status">stage</field>
</record> </record>
<record forcecreate="False" id="project_stage_data_0" model="project.task.type"> <record forcecreate="False" id="project_stage_data_0" model="project.task.type">
+2 -2
View File
@@ -324,8 +324,8 @@ Send it ASAP, its urgent.</field>
<field name="rating_template_id" ref="rating_project_request_email_template"/> <field name="rating_template_id" ref="rating_project_request_email_template"/>
</record> </record>
<function model="project.task" name="rating_apply" eval="([ref('project.project_task_3')], 10)"/> <function model="project.task" name="rating_apply" eval="([ref('project.project_task_3')], 5)"/>
<function model="project.task" name="rating_apply" eval="([ref('project.project_task_24')], 10)"/> <function model="project.task" name="rating_apply" eval="([ref('project.project_task_24')], 5)"/>
<function model="project.task" name="rating_apply" eval="([ref('project.project_task_8')], 1)"/> <function model="project.task" name="rating_apply" eval="([ref('project.project_task_8')], 1)"/>
</data> </data>
</odoo> </odoo>
@@ -60,13 +60,13 @@
<table style="width:100%;text-align:center;"> <table style="width:100%;text-align:center;">
<tr> <tr>
<td> <td>
<a href="/rating/${access_token}/10"> <a href="/rating/${access_token}/5">
<img alt="Satisfied" src="/rating/static/src/img/rating_10.png" title="Satisfied"/> <img alt="Satisfied" src="/rating/static/src/img/rating_5.png" title="Satisfied"/>
</a> </a>
</td> </td>
<td> <td>
<a href="/rating/${access_token}/5"> <a href="/rating/${access_token}/3">
<img alt="Not satisfied" src="/rating/static/src/img/rating_5.png" title="Not satisfied"/> <img alt="Not satisfied" src="/rating/static/src/img/rating_3.png" title="Not satisfied"/>
</a> </a>
</td> </td>
<td> <td>
+25 -25
View File
@@ -51,6 +51,7 @@ class ProjectTaskType(models.Model):
" * A good 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") " * A medium or a bad feedback will set the kanban state to 'blocked' (red bullet).\n")
is_closed = fields.Boolean('Closing Stage', help="Tasks in this stage are considered as closed.") is_closed = fields.Boolean('Closing Stage', help="Tasks in this stage are considered as closed.")
disabled_rating_warning = fields.Text(compute='_compute_disabled_rating_warning')
def unlink_wizard(self, stage_view=False): def unlink_wizard(self, stage_view=False):
self = self.with_context(active_test=False) self = self.with_context(active_test=False)
@@ -82,6 +83,15 @@ class ProjectTaskType(models.Model):
self.env['project.task'].search([('stage_id', 'in', self.ids)]).write({'active': False}) self.env['project.task'].search([('stage_id', 'in', self.ids)]).write({'active': False})
return super(ProjectTaskType, self).write(vals) return super(ProjectTaskType, self).write(vals)
@api.depends('project_ids', 'project_ids.rating_active')
def _compute_disabled_rating_warning(self):
for stage in self:
disabled_projects = stage.project_ids.filtered(lambda p: not p.rating_active)
if disabled_projects:
stage.disabled_rating_warning = '\n'.join('- %s' % p.name for p in disabled_projects)
else:
stage.disabled_rating_warning = False
class Project(models.Model): class Project(models.Model):
_name = "project.project" _name = "project.project"
@@ -172,11 +182,6 @@ class Project(models.Model):
def _get_default_favorite_user_ids(self): def _get_default_favorite_user_ids(self):
return [(6, 0, [self.env.uid])] return [(6, 0, [self.env.uid])]
def _get_default_rating_status(self):
if self.user_has_groups('project.group_project_rating'):
return 'stage'
return 'no'
name = fields.Char("Name", index=True, required=True, tracking=True) name = fields.Char("Name", index=True, required=True, tracking=True)
active = fields.Boolean(default=True, active = fields.Boolean(default=True,
help="If the active field is set to False, it will allow you to hide the project without removing it.") help="If the active field is set to False, it will allow you to hide the project without removing it.")
@@ -238,20 +243,22 @@ class Project(models.Model):
# rating fields # rating fields
rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True) rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True)
rating_status = fields.Selection([ rating_active = fields.Boolean('Customer Ratings', default=True)
('stage', 'Rating when changing stage'), rating_status = fields.Selection(
('periodic', 'Periodical Rating'), [('stage', 'Rating when changing stage'),
('no', 'No rating')], string='Customer Ratings', default=_get_default_rating_status, required=True, ('periodic', 'Periodical Rating')
], 'Customer Ratings Status', default="stage", required=True,
help="How to get customer feedback?\n" help="How to get customer feedback?\n"
"- Rating when changing stage: an email will be sent when a task is pulled in another stage.\n" "- Rating when changing stage: an email will be sent when a task is pulled in another stage.\n"
"- Periodical Rating: email will be sent periodically.\n\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.") "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([ rating_status_period = fields.Selection([
('daily', 'Daily'), ('weekly', 'Weekly'), ('bimonthly', 'Twice a Month'), ('daily', 'Daily'),
('monthly', 'Once a Month'), ('quarterly', 'Quarterly'), ('yearly', 'Yearly') ('weekly', 'Weekly'),
], 'Rating Frequency') ('bimonthly', 'Twice a Month'),
('monthly', 'Once a Month'),
portal_show_rating = fields.Boolean('Rating visible publicly', copy=False) ('quarterly', 'Quarterly'),
('yearly', 'Yearly')], 'Rating Frequency', required=True, default='monthly')
_sql_constraints = [ _sql_constraints = [
('project_date_greater', 'check(date >= date_start)', 'Error! project start-date must be lower than project end-date.') ('project_date_greater', 'check(date >= date_start)', 'Error! project start-date must be lower than project end-date.')
@@ -452,14 +459,7 @@ class Project(models.Model):
return action return action
def action_view_all_rating(self): def action_view_all_rating(self):
""" return the action to see all the rating of the project, and activate default filters """ """ return the action to see all the rating of the project and activate default filters"""
if self.portal_show_rating:
return {
'type': 'ir.actions.act_url',
'name': "Redirect to the Website Projcet Rating Page",
'target': 'self',
'url': "/project/rating/%s" % (self.id,)
}
action = self.env['ir.actions.act_window'].for_xml_id('project', 'rating_rating_action_view_project_rating') action = self.env['ir.actions.act_window'].for_xml_id('project', 'rating_rating_action_view_project_rating')
action['name'] = _('Ratings of %s') % (self.name,) action['name'] = _('Ratings of %s') % (self.name,)
action_context = ast.literal_eval(action['context']) if action['context'] else {} action_context = ast.literal_eval(action['context']) if action['context'] else {}
+1 -15
View File
@@ -10,18 +10,4 @@ class ResConfigSettings(models.TransientModel):
module_project_forecast = fields.Boolean(string="Planning") module_project_forecast = fields.Boolean(string="Planning")
module_hr_timesheet = fields.Boolean(string="Task Logs") module_hr_timesheet = fields.Boolean(string="Task Logs")
group_subtask_project = fields.Boolean("Sub-tasks", implied_group="project.group_subtask_project") group_subtask_project = fields.Boolean("Sub-tasks", implied_group="project.group_subtask_project")
group_project_rating = fields.Boolean("Use Rating on Project", implied_group='project.group_project_rating') group_project_rating = fields.Boolean("Customer Ratings", implied_group='project.group_project_rating')
def _get_subtasks_projects_domain(self):
return []
def execute(self):
res = super(ResConfigSettings, self).execute()
if self.group_project_rating:
# Change the rating status on existing projects from 'no' to 'stage'
self.env['project.project'].search([('rating_status', '=', 'no')]).write({
'rating_status': 'stage'})
if self.group_subtask_project:
domain = self._get_subtasks_projects_domain()
self.env['project.project'].search(domain).write({'allow_subtasks': True})
return res
+3 -3
View File
@@ -142,7 +142,7 @@ class TestProjectFlow(TestProjectCommon):
'parent_res_id': self.project_pigs.id, 'parent_res_id': self.project_pigs.id,
'rated_partner_id': self.partner_2.id, 'rated_partner_id': self.partner_2.id,
'partner_id': self.partner_2.id, 'partner_id': self.partner_2.id,
'rating': 10, 'rating': 5,
'consumed': False, 'consumed': False,
}) })
@@ -153,7 +153,7 @@ class TestProjectFlow(TestProjectCommon):
'parent_res_id': self.project_pigs.id, 'parent_res_id': self.project_pigs.id,
'rated_partner_id': self.partner_2.id, 'rated_partner_id': self.partner_2.id,
'partner_id': self.partner_2.id, 'partner_id': self.partner_2.id,
'rating': 5, 'rating': 3,
'consumed': True, 'consumed': True,
}) })
@@ -170,7 +170,7 @@ class TestProjectFlow(TestProjectCommon):
self.assertEqual(self.project_pigs.rating_percentage_satisfaction, 0) # There is a rating but not a "great" on, just an "okay". self.assertEqual(self.project_pigs.rating_percentage_satisfaction, 0) # There is a rating but not a "great" on, just an "okay".
# Consuming rating_good # Consuming rating_good
first_task.rating_apply(10, rating_good.access_token) first_task.rating_apply(5, rating_good.access_token)
# We need to invalidate cache since it is not done automatically by the ORM # We need to invalidate cache since it is not done automatically by the ORM
# Our One2Many is linked to a res_id (int) for which the orm doesn't create an inverse # Our One2Many is linked to a res_id (int) for which the orm doesn't create an inverse
+179 -155
View File
@@ -89,6 +89,161 @@
</field> </field>
</record> </record>
<!-- Task types -->
<record id="task_type_search" model="ir.ui.view">
<field name="name">project.task.type.search</field>
<field name="model">project.task.type</field>
<field name="arch" type="xml">
<search string="Tasks Stages">
<field name="name" string="Tasks Stages"/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
</search>
</field>
</record>
<record id="task_type_edit" model="ir.ui.view">
<field name="name">project.task.type.form</field>
<field name="model">project.task.type</field>
<field name="arch" type="xml">
<form string="Task Stage" delete="0">
<field name="active" invisible="1" />
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}" />
<group>
<group>
<field name="name"/>
<field name="mail_template_id"/>
<field name="rating_template_id" groups="project.group_project_rating"/>
<div class="alert alert-warning" role="alert" colspan='2' attrs="{'invisible': ['|', ('rating_template_id','=', False), ('disabled_rating_warning', '=', False)]}">
<i class="fa fa-warning" title="Customer disabled on projects"/><b> Customer Ratings</b> are disabled on the following project(s) : <br/>
<field name="disabled_rating_warning" class="mb-0" />
</div>
<field name="auto_validation_kanban_state" attrs="{'invisible': [('rating_template_id','=', False)]}" groups="project.group_project_rating"/>
</group>
<group>
<field name="fold"/>
<field name="is_closed" groups="base.group_no_one"/>
<field name="project_ids" widget="many2many_tags" groups="base.group_no_one"/>
<field name="sequence" groups="base.group_no_one"/>
</group>
</group>
<group string="Stage Description and Tooltips">
<p class="text-muted" colspan="2">
At each stage employees can block or make task/issue ready for next stage.
You can define here labels that will be displayed for the state instead
of the default labels.
</p>
<label for="legend_normal" string=" " class="o_status oe_project_kanban_legend"
title="Task in progress. Click to block or set as done."
aria-label="Task in progress. Click to block or set as done." role="img"/>
<field name="legend_normal" nolabel="1"/>
<label for="legend_blocked" string=" " class="o_status o_status_red oe_project_kanban_legend"
title="Task is blocked. Click to unblock or set as done."
aria-label="Task is blocked. Click to unblock or set as done." role="img"/>
<field name="legend_blocked" nolabel="1"/>
<label for="legend_done" string=" " class="o_status o_status_green oe_project_kanban_legend"
title="This step is done. Click to block or set in progress."
aria-label="This step is done. Click to block or set in progress." role="img"/>
<field name="legend_done" nolabel="1"/>
<p class="text-muted" colspan="2">
You can also add a description to help your coworkers understand the meaning and purpose of the stage.
</p>
<field name="description" placeholder="Add a description..." nolabel="1" colspan="2"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="task_type_tree" model="ir.ui.view">
<field name="name">project.task.type.tree</field>
<field name="model">project.task.type</field>
<field name="arch" type="xml">
<tree string="Task Stage" delete="0">
<field name="sequence" widget="handle" optional="show"/>
<field name="name"/>
<field name="fold" optional="show"/>
<field name="mail_template_id" optional="show"/>
<field name="rating_template_id" optional="show"/>
<field name="auto_validation_kanban_state" optional="hide" />
<field name="description" optional="hide"/>
<field name="project_ids" optional="show" widget="many2many_tags" options="{'color_field': 'color'}" />
</tree>
</field>
</record>
<record id="view_project_task_type_kanban" model="ir.ui.view">
<field name="name">project.task.type.kanban</field>
<field name="model">project.task.type</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="name"/>
<field name="fold"/>
<field name="description"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
<div class="row">
<div class="col-12">
<strong><t t-esc="record.name.value"/></strong>
</div>
</div>
<t t-if="record.description.value">
<hr class="mt8 mb8"/>
<t t-esc="record.description.value"/>
</t>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="open_task_type_form" model="ir.actions.act_window">
<field name="name">Stages</field>
<field name="res_model">project.task.type</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_id" ref="task_type_tree"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new stage in the task pipeline
</p><p>
Define the steps that will be used in the project from the
creation of the task, up to the closing of the task or issue.
You will use these stages in order to track the progress in
solving a task or an issue.
</p>
</field>
</record>
<record id="open_task_type_form_domain" model="ir.actions.act_window">
<field name="name">Stages</field>
<field name="res_model">project.task.type</field>
<field name="view_mode">tree,kanban,form</field>
<field name="domain">[('project_ids','=', project_id)]</field>
<field name="view_id" ref="task_type_tree"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new stage in the task pipeline
</p><p>
Define the steps that will be used in the project from the
creation of the task, up to the closing of the task or issue.
You will use these stages in order to track the progress in
solving a task or an issue.
</p>
</field>
</record>
<record id="unlink_task_type_action" model="ir.actions.server">
<field name="name">Delete</field>
<field name="model_id" ref="project.model_project_task_type"/>
<field name="binding_model_id" ref="project.model_project_task_type"/>
<field name="binding_view_types">form,list</field>
<field name="state">code</field>
<field name="code">action = records.unlink_wizard(stage_view=True)</field>
</record>
<!-- Project --> <!-- Project -->
<record id="edit_project" model="ir.ui.view"> <record id="edit_project" model="ir.ui.view">
<field name="name">project.project.form</field> <field name="name">project.project.form</field>
@@ -107,16 +262,17 @@
name="%(act_project_project_2_project_task_all)d" icon="fa-tasks"> name="%(act_project_project_2_project_task_all)d" icon="fa-tasks">
<field string="Tasks In Progress" name="task_count" widget="statinfo" options="{'label_field': 'label_tasks'}"/> <field string="Tasks In Progress" name="task_count" widget="statinfo" options="{'label_field': 'label_tasks'}"/>
</button> </button>
<button name="action_view_all_rating" type="object" attrs="{'invisible': ['|', ('rating_status', '=', 'no'), ('rating_percentage_satisfaction', '=', -1)]}" class="oe_stat_button oe_percent" icon="fa-smile-o" groups="project.group_project_rating"> <button name="action_view_all_rating" type="object" attrs="{'invisible': ['|', ('rating_active', '=', False), ('rating_percentage_satisfaction', '=', -1)]}" class="oe_stat_button oe_percent oe_read_only" icon="fa-smile-o" groups="project.group_project_rating">
<div class="o_field_widget o_stat_info"> <div class="o_field_widget o_stat_info">
<span class="o_stat_value"> <span class="o_stat_value">
<field name="rating_percentage_satisfaction" nolabel="1"/> <field name="rating_percentage_satisfaction" nolabel="1"/>
%
</span> </span>
<span class="o_stat_text"> <span class="o_stat_text">
% On <field readonly="1" name="label_tasks" options="{'label_field': 'label_tasks'}" /> Customer Satisfaction
</span> </span>
</div> </div>
</button> </button>
</div> </div>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/> <widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<div class="oe_title"> <div class="oe_title">
@@ -151,23 +307,24 @@
</group> </group>
<div class="row mt16 o_settings_container"> <div class="row mt16 o_settings_container">
<div id="rating_settings" class="col-lg-6 o_setting_box" groups="project.group_project_rating"> <div id="rating_settings" class="col-lg-6 o_setting_box" groups="project.group_project_rating">
<div class="o_setting_left_pane">
<field name="rating_active"/>
</div>
<div class="o_setting_right_pane"> <div class="o_setting_right_pane">
<label for="rating_status"/> <label for="rating_active" />
<div class="text-muted"> <div class="text-muted">
Get customer feedback Get customer feedback
</div> </div>
<div class="mt16"> <div class="mt16" attrs="{'invisible':[('rating_active','==',False)]}">
<field name="rating_status" widget="radio"/> <field name="rating_status" widget="radio" />
<p attrs="{'invisible': [('rating_status','not in',('periodic','stage'))]}" class="text-muted oe_edit_only">
Edit project's stages and set an email template on the stages on which you want to activate the rating.
</p>
<div attrs="{'required': [('rating_status','=','periodic')], 'invisible': [('rating_status','!=','periodic')]}"> <div attrs="{'required': [('rating_status','=','periodic')], 'invisible': [('rating_status','!=','periodic')]}">
<label for="rating_status_period"/> <label for="rating_status_period"/>
<field name="rating_status_period" class="oe_inline"/> <field name="rating_status_period"/>
</div> </div>
<div attrs="{'invisible': [('rating_status','==','no')]}"> <div class="content-group">
<label for="portal_show_rating"/> <div class="mt8">
<field name="portal_show_rating"/> <button name="%(project.open_task_type_form_domain)d" context="{'project_id':id}" icon="fa-arrow-right" type="action" string="Set a Rating Email Template on Stages" class="btn-link"/>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -352,6 +509,7 @@
<field name="is_favorite"/> <field name="is_favorite"/>
<field name="rating_percentage_satisfaction"/> <field name="rating_percentage_satisfaction"/>
<field name="rating_status"/> <field name="rating_status"/>
<field name="rating_active" />
<field name="analytic_account_id"/> <field name="analytic_account_id"/>
<templates> <templates>
<t t-name="kanban-box"> <t t-name="kanban-box">
@@ -368,16 +526,13 @@
<div t-if="record.alias_name.value and record.alias_domain.value"> <div t-if="record.alias_name.value and record.alias_domain.value">
<span><i class="fa fa-envelope" role="img" aria-label="Domain Alias" title="Domain Alias"></i> <t t-esc="record.alias_id.value"/></span> <span><i class="fa fa-envelope" role="img" aria-label="Domain Alias" title="Domain Alias"></i> <t t-esc="record.alias_id.value"/></span>
</div> </div>
<div t-if="record.rating_status.raw_value != 'no'" class="mt8 text-primary" title="Percentage of happy ratings over the past 30 days. Get rating details from the More menu." groups="project.group_project_rating"> <div t-if="record.rating_active.raw_value" class="mt8 text-primary" title="Percentage of happy ratings over the past 30 days." groups="project.group_project_rating">
<b> <b>
<t t-if="record.rating_percentage_satisfaction.value == -1"> <a name="action_view_all_rating" type="object" context="{'search_default_rating_last_30_days':1}">
<i class="fa fa-smile-o"/> No rating yet <t t-if="record.rating_percentage_satisfaction.value != -1">
</t>
<t t-if="record.rating_percentage_satisfaction.value != -1">
<a name="action_view_all_rating" type="object" context="{'search_default_rating_last_30_days':1}">
<i class="fa fa-smile-o" role="img" aria-label="Percentage of satisfaction" title="Percentage of satisfaction"/> <t t-esc="record.rating_percentage_satisfaction.value"/>% <i class="fa fa-smile-o" role="img" aria-label="Percentage of satisfaction" title="Percentage of satisfaction"/> <t t-esc="record.rating_percentage_satisfaction.value"/>%
</a> </t>
</t> </a>
</b> </b>
</div> </div>
</div> </div>
@@ -390,9 +545,6 @@
<div role="menuitem"> <div role="menuitem">
<a type="edit">Edit</a> <a type="edit">Edit</a>
</div> </div>
<div role="menuitem" t-if="record.rating_status.raw_value != 'no'">
<a name="action_view_all_rating" type="object">Customer Ratings</a>
</div>
</div> </div>
<div role="menuitem" aria-haspopup="true" class="o_no_padding_kanban_colorpicker"> <div role="menuitem" aria-haspopup="true" class="o_no_padding_kanban_colorpicker">
<ul class="oe_kanban_colorpicker" data-field="color" role="popup"/> <ul class="oe_kanban_colorpicker" data-field="color" role="popup"/>
@@ -674,8 +826,8 @@
<span role="alert" class='oe_kanban_mail_new' title='Unread Messages'><i class='fa fa-comments' role="img" aria-label="Unread Messages"/><t t-raw="record.message_needaction_counter.raw_value"/></span> <span role="alert" class='oe_kanban_mail_new' title='Unread Messages'><i class='fa fa-comments' role="img" aria-label="Unread Messages"/><t t-raw="record.message_needaction_counter.raw_value"/></span>
</t> </t>
<b t-if="record.rating_ids.raw_value.length"> <b t-if="record.rating_ids.raw_value.length">
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-smile-o text-success" t-if="record.rating_last_value.value == 10" title="Latest Rating: Satisfied" role="img" aria-label="Happy face"/> <span style="font-weight:bold;" class="fa fa-fw mt4 fa-smile-o text-success" t-if="record.rating_last_value.value == 5" title="Latest Rating: Satisfied" role="img" aria-label="Happy face"/>
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-meh-o text-warning" t-if="record.rating_last_value.value == 5" title="Latest Rating: Not Satisfied" role="img" aria-label="Neutral face"/> <span style="font-weight:bold;" class="fa fa-fw mt4 fa-meh-o text-warning" t-if="record.rating_last_value.value == 3" title="Latest Rating: Not Satisfied" role="img" aria-label="Neutral face"/>
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-frown-o text-danger" t-if="record.rating_last_value.value == 1" title="Latest Rating: Higly Dissatisfied" role="img" aria-label="Sad face"/> <span style="font-weight:bold;" class="fa fa-fw mt4 fa-frown-o text-danger" t-if="record.rating_last_value.value == 1" title="Latest Rating: Higly Dissatisfied" role="img" aria-label="Sad face"/>
</b> </b>
<!-- formating of the date --> <!-- formating of the date -->
@@ -874,135 +1026,7 @@
<field name="context">{'project_id':active_id}</field> <field name="context">{'project_id':active_id}</field>
</record> </record>
<!-- Task types --> <!-- Menu item for project -->
<record id="task_type_search" model="ir.ui.view">
<field name="name">project.task.type.search</field>
<field name="model">project.task.type</field>
<field name="arch" type="xml">
<search string="Tasks Stages">
<field name="name" string="Tasks Stages"/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
</search>
</field>
</record>
<record id="task_type_edit" model="ir.ui.view">
<field name="name">project.task.type.form</field>
<field name="model">project.task.type</field>
<field name="arch" type="xml">
<form string="Task Stage" delete="0">
<field name="active" invisible="1" />
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}" />
<group>
<group>
<field name="name"/>
<field name="mail_template_id"/>
<field name="rating_template_id" groups="project.group_project_rating"/>
<field name="auto_validation_kanban_state" attrs="{'invisible': [('rating_template_id','=', False)]}" groups="project.group_project_rating"/>
</group>
<group>
<field name="fold"/>
<field name="is_closed" groups="base.group_no_one"/>
<field name="project_ids" widget="many2many_tags" groups="base.group_no_one"/>
<field name="sequence" groups="base.group_no_one"/>
</group>
</group>
<group string="Stage Description and Tooltips">
<p class="text-muted" colspan="2">
At each stage employees can block or make task/issue ready for next stage.
You can define here labels that will be displayed for the state instead
of the default labels.
</p>
<label for="legend_normal" string=" " class="o_status oe_project_kanban_legend"
title="Task in progress. Click to block or set as done."
aria-label="Task in progress. Click to block or set as done." role="img"/>
<field name="legend_normal" nolabel="1"/>
<label for="legend_blocked" string=" " class="o_status o_status_red oe_project_kanban_legend"
title="Task is blocked. Click to unblock or set as done."
aria-label="Task is blocked. Click to unblock or set as done." role="img"/>
<field name="legend_blocked" nolabel="1"/>
<label for="legend_done" string=" " class="o_status o_status_green oe_project_kanban_legend"
title="This step is done. Click to block or set in progress."
aria-label="This step is done. Click to block or set in progress." role="img"/>
<field name="legend_done" nolabel="1"/>
<p class="text-muted" colspan="2">
You can also add a description to help your coworkers understand the meaning and purpose of the stage.
</p>
<field name="description" placeholder="Add a description..." nolabel="1" colspan="2"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="task_type_tree" model="ir.ui.view">
<field name="name">project.task.type.tree</field>
<field name="model">project.task.type</field>
<field name="arch" type="xml">
<tree string="Task Stage" delete="0">
<field name="sequence" widget="handle" groups="base.group_no_one"/>
<field name="name"/>
<field name="fold"/>
<field name="description"/>
</tree>
</field>
</record>
<record id="view_project_task_type_kanban" model="ir.ui.view">
<field name="name">project.task.type.kanban</field>
<field name="model">project.task.type</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="name"/>
<field name="fold"/>
<field name="description"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
<div class="row">
<div class="col-12">
<strong><t t-esc="record.name.value"/></strong>
</div>
</div>
<t t-if="record.description.value">
<hr class="mt8 mb8"/>
<t t-esc="record.description.value"/>
</t>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="open_task_type_form" model="ir.actions.act_window">
<field name="name">Stages</field>
<field name="res_model">project.task.type</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_id" ref="task_type_tree"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new stage in the task pipeline
</p><p>
Define the steps that will be used in the project from the
creation of the task, up to the closing of the task or issue.
You will use these stages in order to track the progress in
solving a task or an issue.
</p>
</field>
</record>
<record id="unlink_task_type_action" model="ir.actions.server">
<field name="name">Delete</field>
<field name="model_id" ref="project.model_project_task_type"/>
<field name="binding_model_id" ref="project.model_project_task_type"/>
<field name="binding_view_types">form,list</field>
<field name="state">code</field>
<field name="code">action = records.unlink_wizard(stage_view=True)</field>
</record>
<menuitem id="menu_tasks_config" name="GTD" parent="menu_project_config" sequence="2"/> <menuitem id="menu_tasks_config" name="GTD" parent="menu_project_config" sequence="2"/>
<menuitem action="open_task_type_form" id="menu_project_config_project" name="Stages" parent="menu_project_config" sequence="3" groups="base.group_no_one"/> <menuitem action="open_task_type_form" id="menu_project_config_project" name="Stages" parent="menu_project_config" sequence="3" groups="base.group_no_one"/>
+2 -3
View File
@@ -12,8 +12,7 @@
</xpath> </xpath>
<xpath expr="/search" position="inside"> <xpath expr="/search" position="inside">
<filter string="Last 30 Days" name="rating_last_30_days" domain="[ <filter string="Last 30 Days" name="rating_last_30_days" domain="[
('create_date', '>=', (datetime.datetime.combine(context_today() + relativedelta(days=-30), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S')), ('create_date', '>=', (datetime.datetime.combine(context_today() + relativedelta(days=-30), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))]"
('create_date', '&lt;', (datetime.datetime.combine(context_today(), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))]"
/> />
<separator/> <separator/>
</xpath> </xpath>
@@ -28,7 +27,7 @@
<field name="search_view_id" ref="rating_rating_view_search_project"/> <field name="search_view_id" ref="rating_rating_view_search_project"/>
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_empty_folder"> <p class="o_view_nocontent_empty_folder">
There are no ratings for this object at the moment There are no ratings for this project at the moment
</p> </p>
</field> </field>
</record> </record>
@@ -44,7 +44,7 @@
</div> </div>
<div class="content-group" attrs="{'invisible': [('group_project_rating', '=', False)]}"> <div class="content-group" attrs="{'invisible': [('group_project_rating', '=', False)]}">
<div class="mt8"> <div class="mt8">
<button name="%(project.open_task_type_form)d" icon="fa-arrow-right" type="action" string="Set Email Template to Stages" class="btn-link"/> <button name="%(project.open_task_type_form)d" icon="fa-arrow-right" type="action" string="Set a Rating Email Template on Stages" class="btn-link"/>
</div> </div>
</div> </div>
</div> </div>
+4 -4
View File
@@ -11,13 +11,13 @@ class Rating(http.Controller):
@http.route('/rating/<string:token>/<int:rate>', type='http', auth="public", website=True) @http.route('/rating/<string:token>/<int:rate>', type='http', auth="public", website=True)
def open_rating(self, token, rate, **kwargs): def open_rating(self, token, rate, **kwargs):
assert rate in (1, 5, 10), "Incorrect rating" assert rate in (1, 3, 5), "Incorrect rating"
rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)]) rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)])
if not rating: if not rating:
return request.not_found() return request.not_found()
rate_names = { rate_names = {
10: _("satisfied"), 5: _("satisfied"),
5: _("not satisfied"), 3: _("not satisfied"),
1: _("highly dissatisfied") 1: _("highly dissatisfied")
} }
rating.write({'rating': rate, 'consumed': True}) rating.write({'rating': rate, 'consumed': True})
@@ -30,7 +30,7 @@ class Rating(http.Controller):
@http.route(['/rating/<string:token>/submit_feedback'], type="http", auth="public", methods=['post'], website=True) @http.route(['/rating/<string:token>/submit_feedback'], type="http", auth="public", methods=['post'], website=True)
def submit_rating(self, token, **kwargs): def submit_rating(self, token, **kwargs):
rate = int(kwargs.get('rate')) rate = int(kwargs.get('rate'))
assert rate in (1, 5, 10), "Incorrect rating" assert rate in (1, 3, 5), "Incorrect rating"
rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)]) rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)])
if not rating: if not rating:
return request.not_found() return request.not_found()
+18 -16
View File
@@ -7,7 +7,7 @@ from odoo import api, fields, models
from odoo.modules.module import get_resource_path from odoo.modules.module import get_resource_path
RATING_LIMIT_SATISFIED = 7 RATING_LIMIT_SATISFIED = 5
RATING_LIMIT_OK = 3 RATING_LIMIT_OK = 3
RATING_LIMIT_MIN = 1 RATING_LIMIT_MIN = 1
@@ -18,7 +18,7 @@ class Rating(models.Model):
_order = 'write_date desc' _order = 'write_date desc'
_rec_name = 'res_name' _rec_name = 'res_name'
_sql_constraints = [ _sql_constraints = [
('rating_range', 'check(rating >= 0 and rating <= 10)', 'Rating should be between 0 to 10'), ('rating_range', 'check(rating >= 0 and rating <= 5)', 'Rating should be between 0 to 5'),
] ]
@api.depends('res_model', 'res_id') @api.depends('res_model', 'res_id')
@@ -41,7 +41,7 @@ class Rating(models.Model):
parent_res_id = fields.Integer('Parent Document', index=True) parent_res_id = fields.Integer('Parent Document', index=True)
rated_partner_id = fields.Many2one('res.partner', string="Rated person", help="Owner of the rated resource") rated_partner_id = fields.Many2one('res.partner', string="Rated person", help="Owner of the rated resource")
partner_id = fields.Many2one('res.partner', string='Customer', help="Author of the rating") partner_id = fields.Many2one('res.partner', string='Customer', help="Author of the rating")
rating = fields.Float(string="Rating Number", group_operator="avg", default=0, help="Rating value: 0=Unhappy, 10=Happy") rating = fields.Float(string="Rating Number", group_operator="avg", default=0, help="Rating value: 0=Unhappy, 5=Happy")
rating_image = fields.Binary('Image', compute='_compute_rating_image') rating_image = fields.Binary('Image', compute='_compute_rating_image')
rating_text = fields.Selection([ rating_text = fields.Selection([
('satisfied', 'Satisfied'), ('satisfied', 'Satisfied'),
@@ -66,21 +66,23 @@ class Rating(models.Model):
name = name and name[0][1] or ('%s/%s') % (rating.parent_res_model, rating.parent_res_id) name = name and name[0][1] or ('%s/%s') % (rating.parent_res_model, rating.parent_res_id)
rating.parent_res_name = name rating.parent_res_name = name
@api.depends('rating') def _get_rating_image_filename(self):
self.ensure_one()
if self.rating >= RATING_LIMIT_SATISFIED:
rating_int = 5
elif self.rating >= RATING_LIMIT_OK:
rating_int = 3
elif self.rating >= RATING_LIMIT_MIN:
rating_int = 1
else:
rating_int = 0
return 'rating_%s.png' % rating_int
def _compute_rating_image(self): def _compute_rating_image(self):
# Due to some new widgets, we may have ratings different from 0/1/5/10 (e.g. slide.channel review)
# Let us have some custom rounding while finding a better solution for images.
for rating in self: for rating in self:
rating_for_img = 0
if rating.rating >= 8:
rating_for_img = 10
elif rating.rating > 3:
rating_for_img = 5
elif rating.rating >= 1:
rating_for_img = 1
try: try:
image_path = get_resource_path('rating', 'static/src/img', 'rating_%s.png' % rating_for_img) image_path = get_resource_path('rating', 'static/src/img', rating._get_rating_image_filename())
rating.rating_image = base64.b64encode(open(image_path, 'rb').read()) rating.rating_image = base64.b64encode(open(image_path, 'rb').read()) if image_path else False
except (IOError, OSError): except (IOError, OSError):
rating.rating_image = False rating.rating_image = False
@@ -89,7 +91,7 @@ class Rating(models.Model):
for rating in self: for rating in self:
if rating.rating >= RATING_LIMIT_SATISFIED: if rating.rating >= RATING_LIMIT_SATISFIED:
rating.rating_text = 'satisfied' rating.rating_text = 'satisfied'
elif rating.rating > RATING_LIMIT_OK: elif rating.rating >= RATING_LIMIT_OK:
rating.rating_text = 'not_satisfied' rating.rating_text = 'not_satisfied'
elif rating.rating >= RATING_LIMIT_MIN: elif rating.rating >= RATING_LIMIT_MIN:
rating.rating_text = 'highly_dissatisfied' rating.rating_text = 'highly_dissatisfied'
+3 -3
View File
@@ -199,9 +199,9 @@ class RatingMixin(models.AbstractModel):
author_id=rating.partner_id and rating.partner_id.id or None # None will set the default author in mail_thread.py 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 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: if rating.rating > 2:
self.write({'kanban_state': 'done'}) self.write({'kanban_state': 'done'})
if rating.rating < 5: else:
self.write({'kanban_state': 'blocked'}) self.write({'kanban_state': 'blocked'})
return rating return rating
@@ -222,7 +222,7 @@ class RatingMixin(models.AbstractModel):
base_domain += domain base_domain += domain
data = self.env['rating.rating'].read_group(base_domain, ['rating'], ['rating', 'res_id']) data = self.env['rating.rating'].read_group(base_domain, ['rating'], ['rating', 'res_id'])
# init dict with all posible rate value, except 0 (no value for the rating) # init dict with all posible rate value, except 0 (no value for the rating)
values = dict.fromkeys(range(1, 11), 0) values = dict.fromkeys(range(1, 6), 0)
values.update((d['rating'], d['rating_count']) for d in data) values.update((d['rating'], d['rating_count']) for d in data)
# add other stats # add other stats
if add_stats: if add_stats:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

+3 -3
View File
@@ -51,14 +51,14 @@ class TestAccessRating(common.SavepointCase):
'res_model_id': self.env['ir.model'].sudo().search([('model', '=', 'res.partner')], limit=1).id, 'res_model_id': self.env['ir.model'].sudo().search([('model', '=', 'res.partner')], limit=1).id,
'res_model': 'res.partner', 'res_model': 'res.partner',
'res_id': self.partner_to_rate.id, 'res_id': self.partner_to_rate.id,
'rating': 2 'rating': 1
}) })
with self.assertRaises(AccessError): with self.assertRaises(AccessError):
self.env['rating.rating'].with_user(self.user_public).create({ self.env['rating.rating'].with_user(self.user_public).create({
'res_model_id': self.env['ir.model'].sudo().search([('model', '=', 'res.partner')], limit=1).id, 'res_model_id': self.env['ir.model'].sudo().search([('model', '=', 'res.partner')], limit=1).id,
'res_model': 'res.partner', 'res_model': 'res.partner',
'res_id': self.partner_to_rate.id, 'res_id': self.partner_to_rate.id,
'rating': 5 'rating': 3
}) })
# No error with employee # No error with employee
@@ -66,7 +66,7 @@ class TestAccessRating(common.SavepointCase):
'res_model_id': self.env['ir.model'].sudo().search([('model', '=', 'res.partner')], limit=1).id, 'res_model_id': self.env['ir.model'].sudo().search([('model', '=', 'res.partner')], limit=1).id,
'res_model': 'res.partner', 'res_model': 'res.partner',
'res_id': self.partner_to_rate.id, 'res_id': self.partner_to_rate.id,
'rating': 5 'rating': 3
}) })
with self.assertRaises(AccessError): with self.assertRaises(AccessError):
@@ -4,9 +4,5 @@
<record id="website.default_website" model="website"> <record id="website.default_website" model="website">
<field name="channel_id" ref="im_livechat.im_livechat_channel_data"></field> <field name="channel_id" ref="im_livechat.im_livechat_channel_data"></field>
</record> </record>
<record id="im_livechat.im_livechat_channel_data" model="im_livechat.channel">
<field name="is_published" eval="True"/>
</record>
</data> </data>
</odoo> </odoo>
@@ -132,7 +132,7 @@ var closeStep = [{
var goodRatingStep = [{ var goodRatingStep = [{
content: "Send Good Rating", content: "Send Good Rating",
trigger: "div.o_livechat_rating_choices > img[data-value=10]", trigger: "div.o_livechat_rating_choices > img[data-value=5]",
}, { }, {
content: "Check if feedback has been sent", content: "Check if feedback has been sent",
trigger: "div.o_thread_window_header.feedback_sent", trigger: "div.o_thread_window_header.feedback_sent",
@@ -143,7 +143,7 @@ var goodRatingStep = [{
var okRatingStep = [{ var okRatingStep = [{
content: "Send ok Rating", content: "Send ok Rating",
trigger: "div.o_livechat_rating_choices > img[data-value=5]", trigger: "div.o_livechat_rating_choices > img[data-value=3]",
}]; }];
var sadRatingStep = [{ var sadRatingStep = [{
+1 -1
View File
@@ -69,7 +69,7 @@ class TestLivechatCommon(tests.TransactionCase):
def _send_rating(self, channel, visitor, rating_value, reason=False): def _send_rating(self, channel, visitor, rating_value, reason=False):
channel_messages_count = len(channel.message_ids) channel_messages_count = len(channel.message_ids)
rating_to_emoji = {1: "😞", 5: "😐", 10: "😊"} rating_to_emoji = {1: "😞", 3: "😐", 5: "😊"}
self.opener.post(url=self.send_feedback_url, json={'params': { self.opener.post(url=self.send_feedback_url, json={'params': {
'uuid': channel.uuid, 'uuid': channel.uuid,
'rate': rating_value, 'rate': rating_value,
@@ -13,7 +13,7 @@ class TestLivechatRequestHttpCase(tests.HttpCase, TestLivechatCommon):
# Send first chat request - Open chat from operator side # Send first chat request - Open chat from operator side
channel_1 = self._common_chat_request_flow() channel_1 = self._common_chat_request_flow()
# Visitor Rates the conversation (Good) # Visitor Rates the conversation (Good)
self._send_rating(channel_1, self.visitor, 10) self._send_rating(channel_1, self.visitor, 5)
# Operator Re-Send a chat request # Operator Re-Send a chat request
channel_2 = self._common_chat_request_flow() channel_2 = self._common_chat_request_flow()
@@ -80,7 +80,7 @@
<div class="col-lg-4 d-flex justify-content-end flex-column"> <div class="col-lg-4 d-flex justify-content-end flex-column">
<div class="card bg-success text-white" t-attf-style="height: #{160 + int(percentage['great'])}px;"> <div class="card bg-success text-white" t-attf-style="height: #{160 + int(percentage['great'])}px;">
<div class="card-body text-center"> <div class="card-body text-center">
<img src="/rating/static/src/img/rating_10.png" style="height:40px" alt="Happy face"/> <img src="/rating/static/src/img/rating_5.png" style="height:40px" alt="Happy face"/>
</div> </div>
<div class="card-body text-center"> <div class="card-body text-center">
<h2 style="margin: 0"> <h2 style="margin: 0">
@@ -95,7 +95,7 @@
<div class="col-lg-4 d-flex justify-content-end flex-column"> <div class="col-lg-4 d-flex justify-content-end flex-column">
<div class="card bg-warning text-white" t-attf-style="height: #{160 + int(percentage['okay'])}px;"> <div class="card bg-warning text-white" t-attf-style="height: #{160 + int(percentage['okay'])}px;">
<div class="card-body text-center"> <div class="card-body text-center">
<img src="/rating/static/src/img/rating_5.png" style="height:40px" alt="Neutral face"/> <img src="/rating/static/src/img/rating_3.png" style="height:40px" alt="Neutral face"/>
</div> </div>
<div class="card-body text-center"> <div class="card-body text-center">
<h2 style="margin: 0"> <h2 style="margin: 0">
@@ -131,7 +131,7 @@
<div> <div>
<t t-foreach="ratings" t-as="rating"> <t t-foreach="ratings" t-as="rating">
<img t-attf-src='/rating/static/src/img/rating_#{int(rating.rating)}.png' t-att-alt="rating.res_name" width="48px" height="48px"/> <img t-attf-src='/rating/static/src/img/rating_#{int(rating.rating)}.png' t-att-alt="rating.res_name" width="48px" height="48px"/>
<t t-if="(rating_index+1) % 10 == 0"> <t t-if="(rating_index+1) % 5 == 0">
<br/> <br/>
</t> </t>
</t> </t>
@@ -161,11 +161,11 @@
<div class="row"> <div class="row">
<t t-if="user.partner_id.id in ratings_per_user"> <t t-if="user.partner_id.id in ratings_per_user">
<div class="col-lg-4 pl-0 pr-0"> <div class="col-lg-4 pl-0 pr-0">
<img t-attf-src='/rating/static/src/img/rating_10.png' alt="Great" width="16px" height="16px"/> <img t-attf-src='/rating/static/src/img/rating_5.png' alt="Great" width="16px" height="16px"/>
<span class="align-middle"><t t-esc="ratings_per_user[user.partner_id.id]['great']"/>%</span> <span class="align-middle"><t t-esc="ratings_per_user[user.partner_id.id]['great']"/>%</span>
</div> </div>
<div class="col-lg-4 pl-0 pr-0"> <div class="col-lg-4 pl-0 pr-0">
<img t-attf-src='/rating/static/src/img/rating_5.png' alt="Okay" width="16px" height="16px"/> <img t-attf-src='/rating/static/src/img/rating_3.png' alt="Okay" width="16px" height="16px"/>
<span class="align-middle"><t t-esc="ratings_per_user[user.partner_id.id]['okay']"/>%</span> <span class="align-middle"><t t-esc="ratings_per_user[user.partner_id.id]['okay']"/>%</span>
</div> </div>
<div class="col-lg-4 pl-0 pr-0"> <div class="col-lg-4 pl-0 pr-0">
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
+18
View File
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Website Project',
'category': 'Website/Website',
'summary': 'Publish your project ratings',
'version': '1.0',
'description': """
Allows to publish all ratings for a particular project.""",
'depends': ['website', 'project'],
'data': [
'views/project_rating_templates.xml',
'views/project_project_views.xml',
],
'installable': True,
'auto_install': True,
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import rating
@@ -11,9 +11,11 @@ class RatingProject(http.Controller):
@http.route(['/project/rating'], type='http', auth="public", website=True, sitemap=True) @http.route(['/project/rating'], type='http', auth="public", website=True, sitemap=True)
def index(self, **kw): def index(self, **kw):
projects = request.env['project.project'].sudo().search([('rating_status', '!=', 'no'), ('portal_show_rating', '=', True)]) projects = request.env['project.project'].sudo().search([
('rating_active', '=', True),
('is_published', '=', True)])
values = {'projects': projects} values = {'projects': projects}
return request.render('project.rating_index', values) return request.render('website_project.rating_index', values)
def _calculate_period_partner_stats(self, project_id): def _calculate_period_partner_stats(self, project_id):
# get raw data: number of rating by rated partner, by rating value, by period # get raw data: number of rating by rated partner, by rating value, by period
@@ -36,7 +38,7 @@ class RatingProject(http.Controller):
AND res_model = 'project.task' AND res_model = 'project.task'
AND rated_partner_id IS NOT NULL AND rated_partner_id IS NOT NULL
AND write_date >= current_date - interval '90' day AND write_date >= current_date - interval '90' day
AND rating IN (1,5,10) AND rating IN (1, 3, 5)
GROUP BY GROUP BY
rated_partner_id, rating, period rated_partner_id, rating, period
""", (project_id, )) """, (project_id, ))
@@ -44,7 +46,7 @@ class RatingProject(http.Controller):
raw_data = request.env.cr.dictfetchall() raw_data = request.env.cr.dictfetchall()
# periodical statistics # periodical statistics
default_period_dict = {'rating_10': 0, 'rating_5': 0, 'rating_1': 0, 'total': 0} default_period_dict = {'rating_5': 0, 'rating_3': 0, 'rating_1': 0, 'total': 0}
period_statistics = { period_statistics = {
'days_06': dict(default_period_dict), 'days_06': dict(default_period_dict),
'days_15': dict(default_period_dict), 'days_15': dict(default_period_dict),
@@ -58,7 +60,7 @@ class RatingProject(http.Controller):
period_statistics[period_statistics_key]['total'] += row['rating_count'] period_statistics[period_statistics_key]['total'] += row['rating_count']
# partner statistics # partner statistics
default_partner_dict = {'rating_10': 0, 'rating_5': 0, 'rating_1': 0, 'total': 0, 'rated_partner': None, 'percentage_happy': 0.0} default_partner_dict = {'rating_5': 0, 'rating_3': 0, 'rating_1': 0, 'total': 0, 'rated_partner': None, 'percentage_happy': 0.0}
partner_statistics = {} partner_statistics = {}
for row in raw_data: for row in raw_data:
if row['period'] <= 'days_15': if row['period'] <= 'days_15':
@@ -69,7 +71,7 @@ class RatingProject(http.Controller):
partner_statistics[row['rated_partner_id']]['total'] += row['rating_count'] partner_statistics[row['rated_partner_id']]['total'] += row['rating_count']
for partner_id, stat_values in partner_statistics.items(): for partner_id, stat_values in partner_statistics.items():
stat_values['percentage_happy'] = (stat_values['rating_10'] / float(stat_values['total'])) * 100 if stat_values['total'] else 0.0 stat_values['percentage_happy'] = (stat_values['rating_5'] / float(stat_values['total'])) * 100 if stat_values['total'] else 0.0
return { return {
'partner_statistics': partner_statistics, 'partner_statistics': partner_statistics,
@@ -82,10 +84,10 @@ class RatingProject(http.Controller):
project = request.env['project.project'].sudo().browse(project_id) 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 # 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) # 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.rating_status != 'no') and project.portal_show_rating) and not user.with_user(user).has_group('project.group_project_manager'): if not (project.rating_active and project.is_published) and not user.with_user(user).has_group('project.group_project_manager'):
raise NotFound() raise NotFound()
return request.render('project.rating_project_rating_page', { return request.render('website_project.rating_project_rating_page', {
'project': project, 'project': project,
'ratings': request.env['rating.rating'].sudo().search([ 'ratings': request.env['rating.rating'].sudo().search([
('consumed', '=', True), ('consumed', '=', True),
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import project
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class Project(models.Model):
_name = 'project.project'
_inherit = ['project.project', 'website.published.mixin']
def action_view_all_rating(self):
if self.website_published:
return self.open_website_url()
return super(Project, self).action_view_all_rating()
def open_website_url(self):
""" return the action to see all the rating of the project, and activate default filters """
return {
'type': 'ir.actions.act_url',
'name': "Redirect to the Website Project Rating Page",
'target': 'self',
'url': "/project/rating/%s" % (self.id)
}
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="project_project_view_form" model="ir.ui.view">
<field name="name">project.project.form</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.edit_project"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<field name="is_published"
widget="website_redirect_button"
groups="project.group_project_rating"
attrs="{'invisible': [('rating_active', '=', False)]}"/>
</xpath>
</field>
</record>
</odoo>
@@ -58,15 +58,15 @@
<!-- Page : Project Rating Page (and widgets) --> <!-- Page : Project Rating Page (and widgets) -->
<template id="portal_project_rating_progressbar"> <template id="portal_project_rating_progressbar">
<div class="progress o_rating_progress"> <div class="progress o_rating_progress">
<t t-set="percent_rating_10" t-value="(stats['rating_10'] / float(stats['total'])) * 100 if stats['total'] else 0.0"/> <t t-set="percent_rating_5" t-value="int(round((stats['rating_5'] / float(stats['total'])) * 100)) if stats['total'] else 0.0"/>
<div class="progress-bar bg-success" t-attf-style="width: #{percent_rating_10}%;" title="Happy"> <div class="progress-bar bg-success" t-attf-style="width: #{percent_rating_5}%;" title="Happy">
<t t-esc="percent_rating_10"/>%
</div>
<t t-set="percent_rating_5" t-value="(stats['rating_5'] / float(stats['total'])) * 100 if stats['total'] else 0.0"/>
<div class="progress-bar bg-warning" t-attf-style="width: #{percent_rating_5}%;" title="Average">
<t t-esc="percent_rating_5"/>% <t t-esc="percent_rating_5"/>%
</div> </div>
<t t-set="percent_rating_1" t-value="(stats['rating_1'] / float(stats['total'])) * 100 if stats['total'] else 0.0"/> <t t-set="percent_rating_3" t-value="int(round((stats['rating_3'] / float(stats['total'])) * 100)) if stats['total'] else 0.0"/>
<div class="progress-bar bg-warning" t-attf-style="width: #{percent_rating_3}%;" title="Average">
<t t-esc="percent_rating_3"/>%
</div>
<t t-set="percent_rating_1" t-value="int(round((stats['rating_1'] / float(stats['total'])) * 100)) if stats['total'] else 0.0"/>
<div class="progress-bar bg-danger" t-attf-style="width: #{percent_rating_1}%;" title="Bad"> <div class="progress-bar bg-danger" t-attf-style="width: #{percent_rating_1}%;" title="Bad">
<t t-esc="percent_rating_1"/>% <t t-esc="percent_rating_1"/>%
</div> </div>
@@ -84,26 +84,26 @@
<span class="ml8" t-esc="partner_stat['rated_partner'].name"/> <span class="ml8" t-esc="partner_stat['rated_partner'].name"/>
</div> </div>
<div class="col-md-4 o_smiley_no_padding_right"> <div class="col-md-4 o_smiley_no_padding_right">
<div class="col-md-4 o_smiley_no_padding_right o_smiley_no_padding_left">
<span t-if="partner_stat['rating_10']">
<img title="happy" alt="Happy face" class="o_top_partner_rating_image" src='/rating/static/src/img/rating_10.png'/>
<span class="o_rating_count" t-esc="partner_stat['rating_10']"/>
</span>
<span t-if="not partner_stat['rating_10']">
<img title="happy" alt="Happy face" class="o_top_partner_rating_image o_lighter_smileys" src='/rating/static/src/img/rating_10.png'/>
<span class="o_rating_count o_lighter_smileys" t-esc="partner_stat['rating_10']"/>
</span>
</div>
<div class="col-md-4 o_smiley_no_padding_right o_smiley_no_padding_left"> <div class="col-md-4 o_smiley_no_padding_right o_smiley_no_padding_left">
<span t-if="partner_stat['rating_5']"> <span t-if="partner_stat['rating_5']">
<img title="average" alt="Neutral face" class="o_top_partner_rating_image" src='/rating/static/src/img/rating_5.png'/> <img title="happy" alt="Happy face" class="o_top_partner_rating_image" src='/rating/static/src/img/rating_5.png'/>
<span class="o_rating_count" t-esc="partner_stat['rating_5']"/> <span class="o_rating_count" t-esc="partner_stat['rating_5']"/>
</span> </span>
<span t-if="not partner_stat['rating_5']"> <span t-if="not partner_stat['rating_5']">
<img title="average" alt="Neutral face" class="o_top_partner_rating_image o_lighter_smileys" src='/rating/static/src/img/rating_5.png'/> <img title="happy" alt="Happy face" class="o_top_partner_rating_image o_lighter_smileys" src='/rating/static/src/img/rating_5.png'/>
<span class="o_rating_count o_lighter_smileys" t-esc="partner_stat['rating_5']"/> <span class="o_rating_count o_lighter_smileys" t-esc="partner_stat['rating_5']"/>
</span> </span>
</div> </div>
<div class="col-md-4 o_smiley_no_padding_right o_smiley_no_padding_left">
<span t-if="partner_stat['rating_3']">
<img title="average" alt="Neutral face" class="o_top_partner_rating_image" src='/rating/static/src/img/rating_3.png'/>
<span class="o_rating_count" t-esc="partner_stat['rating_3']"/>
</span>
<span t-if="not partner_stat['rating_3']">
<img title="average" alt="Neutral face" class="o_top_partner_rating_image o_lighter_smileys" src='/rating/static/src/img/rating_3.png'/>
<span class="o_rating_count o_lighter_smileys" t-esc="partner_stat['rating_3']"/>
</span>
</div>
<div class="col-md-4 o_smiley_no_padding_right o_smiley_no_padding_left"> <div class="col-md-4 o_smiley_no_padding_right o_smiley_no_padding_left">
<span t-if="partner_stat['rating_1']"> <span t-if="partner_stat['rating_1']">
<img title="bad" alt="Sad face" class="o_top_partner_rating_image" src='/rating/static/src/img/rating_1.png'/> <img title="bad" alt="Sad face" class="o_top_partner_rating_image" src='/rating/static/src/img/rating_1.png'/>
@@ -116,7 +116,7 @@
</div> </div>
</div> </div>
<div class="col-md-3 o_smiley_no_padding_left"> <div class="col-md-3 o_smiley_no_padding_left">
<strong class="float-right"><t t-esc="partner_stat['percentage_happy']"/>%</strong> <strong class="float-right"><t t-esc="int(round(partner_stat['percentage_happy']))"/>%</strong>
</div> </div>
</div> </div>
</t> </t>
@@ -125,11 +125,11 @@
<template id="portal_project_rating_popover"> <template id="portal_project_rating_popover">
<div t-attf-id="rating_#{rating.id}" class="container d-none"> <div t-attf-id="rating_#{rating.id}" class="container d-none">
<div class="row"> <div class="row">
<div class="col-lg-2"> <div class="col-lg-4">
<img t-if="rating.partner_id.image_1024" class="o_top_partner_image" t-att-src="image_data_uri(rating.partner_id.image_1024)" alt="Rated partner"/> <img t-if="rating.partner_id.image_1024" class="img-fluid" t-att-src="image_data_uri(rating.partner_id.image_1024)" alt="Rated partner"/>
<img t-if="not rating.partner_id.image_1024" class="o_top_partner_image" src='/web/static/src/img/placeholder.png' alt="Placeholder"/> <img t-if="not rating.partner_id.image_1024" class="img-fluid" src='/web/static/src/img/placeholder.png' alt="Placeholder"/>
</div> </div>
<div class="col-lg-10"> <div class="col-lg-8">
<div class="mt4"> <div class="mt4">
<strong><t t-esc="rating.rated_partner_id.name"/></strong> - <span class="rating_timeduration"><t t-esc="rating.create_date"/></span> <strong><t t-esc="rating.rated_partner_id.name"/></strong> - <span class="rating_timeduration"><t t-esc="rating.create_date"/></span>
</div> </div>
@@ -149,42 +149,52 @@
<div id="wrap" class="row mt4"> <div id="wrap" class="row mt4">
<div class="oe_structure" id="oe_structure_project_rating_page_1"/> <div class="oe_structure" id="oe_structure_project_rating_page_1"/>
<div class="container o_project o_portal_project_rating"> <div class="container o_project o_portal_project_rating">
<br/>
<h2 t-esc="project.name"/> <h2 t-esc="project.name"/>
<br/>
<div class="row mb32"> <div class="row mb32">
<div class="col-lg-12"> <div class="col-lg-12">
<h3 class="o_page_header">Customer Ratings</h3> <div class="o_page_header">
<div class="justify-content-end" groups="project.group_project_manager">
<t t-call="website.publish_management">
<t t-set="object" t-value="project"/>
<t t-set="publish_edit" t-value="True"/>
</t>
</div>
<h3>Customer Ratings</h3>
</div>
<div class="row"> <div class="row">
<div class="col-lg-7"> <div class="col-lg-7">
<t t-foreach="ratings" t-as="rating"> <t t-foreach="ratings" t-as="rating">
<img t-attf-src='/rating/static/src/img/rating_#{int(rating.rating)}.png' <img t-attf-src='/rating/static/src/img/#{rating._get_rating_image_filename()}'
class="mt4 o_rating_image" class="mt4 o_rating_image"
t-att-alt="rating.res_name" t-att-alt="rating.res_name"
t-att-data-id="rating.id" t-att-data-id="rating.id"
t-att-data-rating-date="rating.write_date"/> t-att-data-rating-date="rating.write_date"/>
<t t-call="project.portal_project_rating_popover"/> <t t-call="website_project.portal_project_rating_popover"/>
</t> </t>
</div> </div>
<div class="col-lg-5 o_vertical_separator"> <div class="col-lg-5 o_vertical_separator">
<div t-if="statistics['period_statistics']['days_06']"> <div t-if="statistics['period_statistics']['days_06']">
<h5>Last 7 days</h5> <h5>Last 7 days</h5>
<t t-call="project.portal_project_rating_progressbar"> <t t-call="website_project.portal_project_rating_progressbar">
<t t-set="stats" t-value="statistics['period_statistics']['days_06']"/> <t t-set="stats" t-value="statistics['period_statistics']['days_06']"/>
</t> </t>
</div> </div>
<div t-if="statistics['period_statistics']['days_30']"> <div t-if="statistics['period_statistics']['days_30']">
<h5>Last 30 days</h5> <h5>Last 30 days</h5>
<t t-call="project.portal_project_rating_progressbar"> <t t-call="website_project.portal_project_rating_progressbar">
<t t-set="stats" t-value="statistics['period_statistics']['days_30']"/> <t t-set="stats" t-value="statistics['period_statistics']['days_30']"/>
</t> </t>
</div> </div>
<div t-if="statistics['period_statistics']['days_90']"> <div t-if="statistics['period_statistics']['days_90']">
<h5>Last 3 months</h5> <h5>Last 3 months</h5>
<t t-call="project.portal_project_rating_progressbar"> <t t-call="website_project.portal_project_rating_progressbar">
<t t-set="stats" t-value="statistics['period_statistics']['days_90']"/> <t t-set="stats" t-value="statistics['period_statistics']['days_90']"/>
</t> </t>
</div> </div>
<div> <div>
<t t-call="project.portal_project_rating_partner_stat"> <t t-call="website_project.portal_project_rating_partner_stat">
<t t-set="partner_stats" t-value="statistics['partner_statistics']"/> <t t-set="partner_stats" t-value="statistics['partner_statistics']"/>
</t> </t>
</div> </div>
@@ -52,7 +52,7 @@
<field name="partner_id" ref="base.partner_admin"/> <field name="partner_id" ref="base.partner_admin"/>
<field name="consumed" eval="True"/> <field name="consumed" eval="True"/>
<field name="feedback">I fear beginners could be lost... Isn't it a bit harsh for a "basics" course ?</field> <field name="feedback">I fear beginners could be lost... Isn't it a bit harsh for a "basics" course ?</field>
<field name="rating">6</field> <field name="rating">3</field>
<field name="message_id" ref="website_slides.message_channel_0_admin"/> <field name="message_id" ref="website_slides.message_channel_0_admin"/>
</record> </record>
<record id="message_channel_0_demo" model="mail.message"> <record id="message_channel_0_demo" model="mail.message">
@@ -68,7 +68,7 @@
<field name="res_id" ref="website_slides.slide_channel_demo_0_gard_0"/> <field name="res_id" ref="website_slides.slide_channel_demo_0_gard_0"/>
<field name="partner_id" ref="base.partner_demo"/> <field name="partner_id" ref="base.partner_demo"/>
<field name="consumed" eval="True"/> <field name="consumed" eval="True"/>
<field name="rating">10</field> <field name="rating">5</field>
<field name="feedback">Back to basics and interesting. Just WOW !</field> <field name="feedback">Back to basics and interesting. Just WOW !</field>
<field name="message_id" ref="website_slides.message_channel_0_demo"/> <field name="message_id" ref="website_slides.message_channel_0_demo"/>
</record> </record>
@@ -140,7 +140,7 @@
<field name="partner_id" ref="base.partner_admin"/> <field name="partner_id" ref="base.partner_admin"/>
<field name="consumed" eval="True"/> <field name="consumed" eval="True"/>
<field name="feedback">Very good course.</field> <field name="feedback">Very good course.</field>
<field name="rating">10</field> <field name="rating">5</field>
<field name="message_id" ref="website_slides.message_channel_1_admin"/> <field name="message_id" ref="website_slides.message_channel_1_admin"/>
</record> </record>
<record id="message_channel_1_demo" model="mail.message"> <record id="message_channel_1_demo" model="mail.message">
@@ -156,7 +156,7 @@
<field name="res_id" ref="website_slides.slide_channel_demo_1_gard1"/> <field name="res_id" ref="website_slides.slide_channel_demo_1_gard1"/>
<field name="partner_id" ref="base.partner_demo"/> <field name="partner_id" ref="base.partner_demo"/>
<field name="consumed" eval="True"/> <field name="consumed" eval="True"/>
<field name="rating">8</field> <field name="rating">4</field>
<field name="feedback">Interesting !</field> <field name="feedback">Interesting !</field>
<field name="message_id" ref="website_slides.message_channel_1_demo"/> <field name="message_id" ref="website_slides.message_channel_1_demo"/>
</record> </record>
@@ -173,7 +173,7 @@
<field name="res_id" ref="website_slides.slide_channel_demo_1_gard1"/> <field name="res_id" ref="website_slides.slide_channel_demo_1_gard1"/>
<field name="partner_id" ref="base.partner_demo_portal"/> <field name="partner_id" ref="base.partner_demo_portal"/>
<field name="consumed" eval="True"/> <field name="consumed" eval="True"/>
<field name="rating">6</field> <field name="rating">3</field>
<field name="feedback">Interesting. Could be great to include more examples.</field> <field name="feedback">Interesting. Could be great to include more examples.</field>
<field name="message_id" ref="website_slides.message_channel_1_portal"/> <field name="message_id" ref="website_slides.message_channel_1_portal"/>
<field name="publisher_comment">Thanks for the feedback! We just updated first lessons to better include newcomers.</field> <field name="publisher_comment">Thanks for the feedback! We just updated first lessons to better include newcomers.</field>
@@ -260,7 +260,7 @@ class Channel(models.Model):
def _compute_rating_stats(self): def _compute_rating_stats(self):
super(Channel, self)._compute_rating_stats() super(Channel, self)._compute_rating_stats()
for record in self: for record in self:
record.rating_avg_stars = record.rating_avg / 2 record.rating_avg_stars = record.rating_avg
@api.depends('slide_partner_ids', 'total_slides') @api.depends('slide_partner_ids', 'total_slides')
@api.depends_context('uid') @api.depends_context('uid')
@@ -12,7 +12,7 @@
<field name="partner_id"/> <field name="partner_id"/>
<templates> <templates>
<t t-name="kanban-box"> <t t-name="kanban-box">
<t t-set="val_stars" t-value="record.rating.raw_value / 2"/> <t t-set="val_stars" t-value="record.rating.raw_value"/>
<t t-set="val_integer" t-value="Math.floor(val_stars)"/> <t t-set="val_integer" t-value="Math.floor(val_stars)"/>
<t t-set="val_decimal" t-value="val_stars - val_integer"/> <t t-set="val_decimal" t-value="val_stars - val_integer"/>
<t t-set="empty_star" t-value="5 - (val_integer + Math.ceil(val_decimal))"/> <t t-set="empty_star" t-value="5 - (val_integer + Math.ceil(val_decimal))"/>
+2 -2
View File
@@ -1030,8 +1030,8 @@ links to the rating object. A very basic email template could look like this:
<p>Hi,</p> <p>Hi,</p>
<p>How satsified are you?</p> <p>How satsified are you?</p>
<ul> <ul>
<li><a href="/rating/${access_token}/10">Satisfied</a></li> <li><a href="/rating/${access_token}/5">Satisfied</a></li>
<li><a href="/rating/${access_token}/5">Not satisfied</a></li> <li><a href="/rating/${access_token}/3">Not satisfied</a></li>
<li><a href="/rating/${access_token}/1">Very unsatisfied</a></li> <li><a href="/rating/${access_token}/1">Very unsatisfied</a></li>
</ul> </ul>
]]></field> ]]></field>