[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="True" name="consumed"/>
</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">
<field name="model">mail.channel</field>
<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="True" name="consumed"/>
</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">
<field name="model">mail.channel</field>
<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 eval="True" name="consumed"/>
</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">
<field name="model">mail.channel</field>
<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="True" name="consumed"/>
</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">
<field name="model">mail.channel</field>
<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 eval="True" name="consumed"/>
</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">
<field name="model">mail.channel</field>
<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="True" name="consumed"/>
</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">
<field name="model">mail.channel</field>
<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="create_date" eval="datetime.now() - timedelta(days=1, seconds=-53)"/>
<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"/>
</record>
@@ -72,14 +72,14 @@ class ImLivechatReportChannel(models.Model):
END as is_anonymous,
C.country_id,
CASE
WHEN rate.rating = 10 THEN 1
WHEN rate.rating = 5 THEN 1
ELSE 0
END as is_happy,
Rate.rating as rating,
CASE
WHEN Rate.rating = 1 THEN 'Unhappy'
WHEN Rate.rating = 10 THEN 'Happy'
WHEN Rate.rating = 5 THEN 'Neutral'
WHEN Rate.rating = 5 THEN 'Happy'
WHEN Rate.rating = 3 THEN 'Neutral'
ELSE null
END as rating_text,
CASE
@@ -525,7 +525,7 @@ var Feedback = Widget.extend({
this.$('.o_livechat_rating_choices img[data-value="'+this.rating+'"]').addClass('selected');
// only display textearea if bad smiley selected
if (this.rating !== 10) {
if (this.rating !== 5) {
this.$('.o_livechat_rating_reason').show();
} else {
this.$('.o_livechat_rating_reason').hide();
@@ -9,8 +9,8 @@
Did we correctly answer your question ?
</div>
<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="OK" data-value="5"/>
<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_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" />
</div>
</div>
@@ -9,7 +9,6 @@ var time = require('web.time');
var _t = core._t;
var PortalChatter = portalChatter.PortalChatter;
var qweb = core.qweb;
var STAR_RATING_RATIO = 2; // conversion factor from the star (1-5) to the db rating range (1-10)
/**
* PortalChatter
@@ -67,7 +66,7 @@ PortalChatter.include({
messages = this._super.apply(this, arguments);
if (this.options['display_rating']) {
_.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);
});
}
@@ -114,13 +113,13 @@ PortalChatter.include({
return;
}
var ratingData = {
'avg': Math.round(result['rating_stats']['avg'] / STAR_RATING_RATIO * 100) / 100,
'avg': Math.round(result['rating_stats']['avg'] * 100) / 100,
'percent': [],
};
_.each(_.keys(result['rating_stats']['percent']).reverse(), function (rating) {
if (rating % 2 === 0) {
ratingData['percent'].push({
'num': rating / STAR_RATING_RATIO,
'num': rating,
'percent': utils.round_precision(result['rating_stats']['percent'][rating], 0.01),
});
}
@@ -347,7 +346,7 @@ PortalChatter.include({
_onChangeRatingDomain: function () {
var domain = [];
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);
},
@@ -8,8 +8,6 @@ var _t = core._t;
var PortalComposer = portalComposer.PortalComposer;
var STAR_RATING_RATIO = 2; // conversion factor from the star (1-5) to the db rating range (1-10)
/**
* PortalComposer
*
@@ -31,7 +29,7 @@ PortalComposer.include({
// apply ratio to 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
@@ -66,7 +64,7 @@ PortalComposer.include({
// 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.$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);
this.set("star_value", index + 1);
this.user_click = true;
this.$input.val(this.get("star_value") * STAR_RATING_RATIO);
this.$input.val(this.get("star_value"));
},
/**
* @private
@@ -7,8 +7,6 @@ var portalComposer = require('portal.composer');
var PortalComposer = portalComposer.PortalComposer;
var STAR_RATING_RATIO = 2; // conversion factor from the star (1-5) to the db rating range (1-10)
/**
* RatingPopupComposer
*
@@ -25,7 +23,7 @@ var RatingPopupComposer = publicWidget.Widget.extend({
init: function (parent, options) {
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.options = _.defaults({}, options, {
@@ -4,7 +4,7 @@
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">
<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_integer" t-value="int(rating_avg)"/>
<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/project_assets.xml',
'views/project_portal_templates.xml',
'views/project_rating_templates.xml',
'data/digest_data.xml',
'data/project_mail_template_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.
from . import portal
from . import rating
+1
View File
@@ -95,6 +95,7 @@
<field name="alias_defaults">{'project_id': 1}</field>
<field name="alias_force_thread_id">0</field>
<field name="alias_parent_model_id" ref="model_project_project"/>
<field name="rating_status">stage</field>
</record>
<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"/>
</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_24')], 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')], 5)"/>
<function model="project.task" name="rating_apply" eval="([ref('project.project_task_8')], 1)"/>
</data>
</odoo>
@@ -60,13 +60,13 @@
<table style="width:100%;text-align:center;">
<tr>
<td>
<a href="/rating/${access_token}/10">
<img alt="Satisfied" src="/rating/static/src/img/rating_10.png" title="Satisfied"/>
<a href="/rating/${access_token}/5">
<img alt="Satisfied" src="/rating/static/src/img/rating_5.png" title="Satisfied"/>
</a>
</td>
<td>
<a href="/rating/${access_token}/5">
<img alt="Not satisfied" src="/rating/static/src/img/rating_5.png" title="Not satisfied"/>
<a href="/rating/${access_token}/3">
<img alt="Not satisfied" src="/rating/static/src/img/rating_3.png" title="Not satisfied"/>
</a>
</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 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.")
disabled_rating_warning = fields.Text(compute='_compute_disabled_rating_warning')
def unlink_wizard(self, stage_view=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})
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):
_name = "project.project"
@@ -172,11 +182,6 @@ class Project(models.Model):
def _get_default_favorite_user_ids(self):
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)
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.")
@@ -238,20 +243,22 @@ class Project(models.Model):
# rating fields
rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True)
rating_status = fields.Selection([
('stage', 'Rating when changing stage'),
('periodic', 'Periodical Rating'),
('no', 'No rating')], string='Customer Ratings', default=_get_default_rating_status, required=True,
rating_active = fields.Boolean('Customer Ratings', default=True)
rating_status = fields.Selection(
[('stage', 'Rating when changing stage'),
('periodic', 'Periodical Rating')
], 'Customer Ratings Status', default="stage", required=True,
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"
"- 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 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"
"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')
portal_show_rating = fields.Boolean('Rating visible publicly', copy=False)
('daily', 'Daily'),
('weekly', 'Weekly'),
('bimonthly', 'Twice a Month'),
('monthly', 'Once a Month'),
('quarterly', 'Quarterly'),
('yearly', 'Yearly')], 'Rating Frequency', required=True, default='monthly')
_sql_constraints = [
('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
def action_view_all_rating(self):
""" 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,)
}
""" return the action to see all the rating of the project and activate default filters"""
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_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_hr_timesheet = fields.Boolean(string="Task Logs")
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')
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
group_project_rating = fields.Boolean("Customer Ratings", implied_group='project.group_project_rating')
+3 -3
View File
@@ -142,7 +142,7 @@ class TestProjectFlow(TestProjectCommon):
'parent_res_id': self.project_pigs.id,
'rated_partner_id': self.partner_2.id,
'partner_id': self.partner_2.id,
'rating': 10,
'rating': 5,
'consumed': False,
})
@@ -153,7 +153,7 @@ class TestProjectFlow(TestProjectCommon):
'parent_res_id': self.project_pigs.id,
'rated_partner_id': self.partner_2.id,
'partner_id': self.partner_2.id,
'rating': 5,
'rating': 3,
'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".
# 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
# 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>
</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 -->
<record id="edit_project" model="ir.ui.view">
<field name="name">project.project.form</field>
@@ -107,16 +262,17 @@
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'}"/>
</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">
<span class="o_stat_value">
<field name="rating_percentage_satisfaction" nolabel="1"/>
%
</span>
<span class="o_stat_text">
% On <field readonly="1" name="label_tasks" options="{'label_field': 'label_tasks'}" />
Customer Satisfaction
</span>
</div>
</button>
</button>
</div>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<div class="oe_title">
@@ -151,23 +307,24 @@
</group>
<div class="row mt16 o_settings_container">
<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">
<label for="rating_status"/>
<label for="rating_active" />
<div class="text-muted">
Get customer feedback
</div>
<div class="mt16">
<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 class="mt16" attrs="{'invisible':[('rating_active','==',False)]}">
<field name="rating_status" widget="radio" />
<div attrs="{'required': [('rating_status','=','periodic')], 'invisible': [('rating_status','!=','periodic')]}">
<label for="rating_status_period"/>
<field name="rating_status_period" class="oe_inline"/>
<field name="rating_status_period"/>
</div>
<div attrs="{'invisible': [('rating_status','==','no')]}">
<label for="portal_show_rating"/>
<field name="portal_show_rating"/>
<div class="content-group">
<div class="mt8">
<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>
@@ -352,6 +509,7 @@
<field name="is_favorite"/>
<field name="rating_percentage_satisfaction"/>
<field name="rating_status"/>
<field name="rating_active" />
<field name="analytic_account_id"/>
<templates>
<t t-name="kanban-box">
@@ -368,16 +526,13 @@
<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>
</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>
<t t-if="record.rating_percentage_satisfaction.value == -1">
<i class="fa fa-smile-o"/> No rating yet
</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}">
<a name="action_view_all_rating" type="object" context="{'search_default_rating_last_30_days':1}">
<t t-if="record.rating_percentage_satisfaction.value != -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"/>%
</a>
</t>
</t>
</a>
</b>
</div>
</div>
@@ -390,9 +545,6 @@
<div role="menuitem">
<a type="edit">Edit</a>
</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 role="menuitem" aria-haspopup="true" class="o_no_padding_kanban_colorpicker">
<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>
</t>
<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-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-smile-o text-success" t-if="record.rating_last_value.value == 5" title="Latest Rating: Satisfied" role="img" aria-label="Happy face"/>
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-meh-o text-warning" t-if="record.rating_last_value.value == 3" title="Latest Rating: 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"/>
</b>
<!-- formating of the date -->
@@ -874,135 +1026,7 @@
<field name="context">{'project_id':active_id}</field>
</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"/>
<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>
<!-- Menu item for project -->
<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"/>
+2 -3
View File
@@ -12,8 +12,7 @@
</xpath>
<xpath expr="/search" position="inside">
<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', '&lt;', (datetime.datetime.combine(context_today(), 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'))]"
/>
<separator/>
</xpath>
@@ -28,7 +27,7 @@
<field name="search_view_id" ref="rating_rating_view_search_project"/>
<field name="help" type="html">
<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>
</field>
</record>
@@ -44,7 +44,7 @@
</div>
<div class="content-group" attrs="{'invisible': [('group_project_rating', '=', False)]}">
<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>
+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)
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)])
if not rating:
return request.not_found()
rate_names = {
10: _("satisfied"),
5: _("not satisfied"),
5: _("satisfied"),
3: _("not satisfied"),
1: _("highly dissatisfied")
}
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)
def submit_rating(self, token, **kwargs):
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)])
if not rating:
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
RATING_LIMIT_SATISFIED = 7
RATING_LIMIT_SATISFIED = 5
RATING_LIMIT_OK = 3
RATING_LIMIT_MIN = 1
@@ -18,7 +18,7 @@ class Rating(models.Model):
_order = 'write_date desc'
_rec_name = 'res_name'
_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')
@@ -41,7 +41,7 @@ class Rating(models.Model):
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")
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_text = fields.Selection([
('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)
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):
# 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:
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:
image_path = get_resource_path('rating', 'static/src/img', 'rating_%s.png' % rating_for_img)
rating.rating_image = base64.b64encode(open(image_path, 'rb').read())
image_path = get_resource_path('rating', 'static/src/img', rating._get_rating_image_filename())
rating.rating_image = base64.b64encode(open(image_path, 'rb').read()) if image_path else False
except (IOError, OSError):
rating.rating_image = False
@@ -89,7 +91,7 @@ class Rating(models.Model):
for rating in self:
if rating.rating >= RATING_LIMIT_SATISFIED:
rating.rating_text = 'satisfied'
elif rating.rating > RATING_LIMIT_OK:
elif rating.rating >= RATING_LIMIT_OK:
rating.rating_text = 'not_satisfied'
elif rating.rating >= RATING_LIMIT_MIN:
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
)
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'})
if rating.rating < 5:
else:
self.write({'kanban_state': 'blocked'})
return rating
@@ -222,7 +222,7 @@ class RatingMixin(models.AbstractModel):
base_domain += domain
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)
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)
# add other 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': 'res.partner',
'res_id': self.partner_to_rate.id,
'rating': 2
'rating': 1
})
with self.assertRaises(AccessError):
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': 'res.partner',
'res_id': self.partner_to_rate.id,
'rating': 5
'rating': 3
})
# 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': 'res.partner',
'res_id': self.partner_to_rate.id,
'rating': 5
'rating': 3
})
with self.assertRaises(AccessError):
@@ -4,9 +4,5 @@
<record id="website.default_website" model="website">
<field name="channel_id" ref="im_livechat.im_livechat_channel_data"></field>
</record>
<record id="im_livechat.im_livechat_channel_data" model="im_livechat.channel">
<field name="is_published" eval="True"/>
</record>
</data>
</odoo>
@@ -132,7 +132,7 @@ var closeStep = [{
var goodRatingStep = [{
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",
trigger: "div.o_thread_window_header.feedback_sent",
@@ -143,7 +143,7 @@ var goodRatingStep = [{
var okRatingStep = [{
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 = [{
+1 -1
View File
@@ -69,7 +69,7 @@ class TestLivechatCommon(tests.TransactionCase):
def _send_rating(self, channel, visitor, rating_value, reason=False):
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': {
'uuid': channel.uuid,
'rate': rating_value,
@@ -13,7 +13,7 @@ class TestLivechatRequestHttpCase(tests.HttpCase, TestLivechatCommon):
# Send first chat request - Open chat from operator side
channel_1 = self._common_chat_request_flow()
# 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
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="card bg-success text-white" t-attf-style="height: #{160 + int(percentage['great'])}px;">
<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 class="card-body text-center">
<h2 style="margin: 0">
@@ -95,7 +95,7 @@
<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-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 class="card-body text-center">
<h2 style="margin: 0">
@@ -131,7 +131,7 @@
<div>
<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"/>
<t t-if="(rating_index+1) % 10 == 0">
<t t-if="(rating_index+1) % 5 == 0">
<br/>
</t>
</t>
@@ -161,11 +161,11 @@
<div class="row">
<t t-if="user.partner_id.id in ratings_per_user">
<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>
</div>
<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>
</div>
<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)
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}
return request.render('project.rating_index', values)
return request.render('website_project.rating_index', values)
def _calculate_period_partner_stats(self, project_id):
# 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 rated_partner_id IS NOT NULL
AND write_date >= current_date - interval '90' day
AND rating IN (1,5,10)
AND rating IN (1, 3, 5)
GROUP BY
rated_partner_id, rating, period
""", (project_id, ))
@@ -44,7 +46,7 @@ class RatingProject(http.Controller):
raw_data = request.env.cr.dictfetchall()
# 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 = {
'days_06': 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']
# 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 = {}
for row in raw_data:
if row['period'] <= 'days_15':
@@ -69,7 +71,7 @@ class RatingProject(http.Controller):
partner_statistics[row['rated_partner_id']]['total'] += row['rating_count']
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 {
'partner_statistics': partner_statistics,
@@ -82,10 +84,10 @@ class RatingProject(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.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()
return request.render('project.rating_project_rating_page', {
return request.render('website_project.rating_project_rating_page', {
'project': project,
'ratings': request.env['rating.rating'].sudo().search([
('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) -->
<template id="portal_project_rating_progressbar">
<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"/>
<div class="progress-bar bg-success" t-attf-style="width: #{percent_rating_10}%;" 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-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_5}%;" title="Happy">
<t t-esc="percent_rating_5"/>%
</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">
<t t-esc="percent_rating_1"/>%
</div>
@@ -84,26 +84,26 @@
<span class="ml8" t-esc="partner_stat['rated_partner'].name"/>
</div>
<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">
<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>
<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>
</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">
<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'/>
@@ -116,7 +116,7 @@
</div>
</div>
<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>
</t>
@@ -125,11 +125,11 @@
<template id="portal_project_rating_popover">
<div t-attf-id="rating_#{rating.id}" class="container d-none">
<div class="row">
<div class="col-lg-2">
<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="not rating.partner_id.image_1024" class="o_top_partner_image" src='/web/static/src/img/placeholder.png' alt="Placeholder"/>
<div class="col-lg-4">
<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="img-fluid" src='/web/static/src/img/placeholder.png' alt="Placeholder"/>
</div>
<div class="col-lg-10">
<div class="col-lg-8">
<div class="mt4">
<strong><t t-esc="rating.rated_partner_id.name"/></strong> - <span class="rating_timeduration"><t t-esc="rating.create_date"/></span>
</div>
@@ -149,42 +149,52 @@
<div id="wrap" class="row mt4">
<div class="oe_structure" id="oe_structure_project_rating_page_1"/>
<div class="container o_project o_portal_project_rating">
<br/>
<h2 t-esc="project.name"/>
<br/>
<div class="row mb32">
<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="col-lg-7">
<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"
t-att-alt="rating.res_name"
t-att-data-id="rating.id"
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>
</div>
<div class="col-lg-5 o_vertical_separator">
<div t-if="statistics['period_statistics']['days_06']">
<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>
</div>
<div t-if="statistics['period_statistics']['days_30']">
<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>
</div>
<div t-if="statistics['period_statistics']['days_90']">
<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>
</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>
</div>
@@ -52,7 +52,7 @@
<field name="partner_id" ref="base.partner_admin"/>
<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="rating">6</field>
<field name="rating">3</field>
<field name="message_id" ref="website_slides.message_channel_0_admin"/>
</record>
<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="partner_id" ref="base.partner_demo"/>
<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="message_id" ref="website_slides.message_channel_0_demo"/>
</record>
@@ -140,7 +140,7 @@
<field name="partner_id" ref="base.partner_admin"/>
<field name="consumed" eval="True"/>
<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"/>
</record>
<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="partner_id" ref="base.partner_demo"/>
<field name="consumed" eval="True"/>
<field name="rating">8</field>
<field name="rating">4</field>
<field name="feedback">Interesting !</field>
<field name="message_id" ref="website_slides.message_channel_1_demo"/>
</record>
@@ -173,7 +173,7 @@
<field name="res_id" ref="website_slides.slide_channel_demo_1_gard1"/>
<field name="partner_id" ref="base.partner_demo_portal"/>
<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="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>
@@ -260,7 +260,7 @@ class Channel(models.Model):
def _compute_rating_stats(self):
super(Channel, self)._compute_rating_stats()
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_context('uid')
@@ -12,7 +12,7 @@
<field name="partner_id"/>
<templates>
<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_decimal" t-value="val_stars - val_integer"/>
<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>How satsified are you?</p>
<ul>
<li><a href="/rating/${access_token}/10">Satisfied</a></li>
<li><a href="/rating/${access_token}/5">Not satisfied</a></li>
<li><a href="/rating/${access_token}/5">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>
</ul>
]]></field>