[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
@@ -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"/>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -2,4 +2,3 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import portal
|
||||
from . import rating
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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', '<', (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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -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 = [{
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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))"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||