[REF,IMP] project,project_issue,rating: Handle ratings more efficiently
Purpose: For our internal project management, we would like to track the customer satisfaction on the open projects. We send them an email every one or 2 weeks allowing them to give a feedback by clicking on one of 3 smileys: Happy, Average, Angry. They can also put a additional explanation. We already have something similar which is working on livechat with a unusable reporting, and on issues but unusable. Our project are managed by tasks. Specification: - On the project : Selection fields : (Periodical Rating or Rating on Stage) + Fields to choose the period if periodical - On the stage : email_template_id field. - IF : ---> Periodical : Send an email to all the customers for the tasks on this stage periodically ---> On Stage : Send an email to the customer's tasks when the task reaches the stage - That way, it's impossible to send a satisfaction request both periodically and sequentially. FP request - Who is the customer : The customer on the task OR the customer on the related sales order OR the customer on the projet OR nobody - The last feedback is displayed on the task kanban card (Thumb up, down, or neutral) - On the project kanban card, the customer satisfaction is displayed. This is the simple mean of all the previous ratings.
This commit is contained in:
committed by
Yannick Tivisse
parent
5560ac4a04
commit
0b1a27a3c7
@@ -20,11 +20,9 @@
|
||||
<a href="https://www.youtube.com/watch?v=PWKez0gn6Ts" target="_blank"> More Info </a>
|
||||
</div>
|
||||
</div>
|
||||
</group>
|
||||
<group name="timesheets"/>
|
||||
<group string="Helpdesk & Support">
|
||||
<field name="module_rating_project" widget="radio"/>
|
||||
</group>
|
||||
<group name="timesheets"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -30,6 +30,7 @@ It allows the manager to quickly check the issues, assign them and decide on the
|
||||
'views/res_partner_view.xml',
|
||||
'views/project_issue_menu.xml',
|
||||
'views/project_dashboard.xml',
|
||||
'views/project_config_settings_views.xml',
|
||||
],
|
||||
'demo': ['data/project_issue_demo.xml'],
|
||||
'application': True,
|
||||
|
||||
@@ -12,3 +12,8 @@ class ProjectIssueConfiguration(models.TransientModel):
|
||||
], string="Timesheets on Issues",
|
||||
help='Provides timesheet support for the issues/bugs management in project.\n'
|
||||
'-This installs the module project_issue_sheet.')
|
||||
module_rating_project_issue = fields.Selection([
|
||||
(0, "No customer rating"),
|
||||
(1, 'Track customer satisfaction on issues')
|
||||
], "Rating on issue",
|
||||
help="This allows customers to give rating on issue")
|
||||
|
||||
@@ -10,7 +10,7 @@ class ResPartner(models.Model):
|
||||
issue_count = fields.Integer(compute='_compute_issue_count', string='# Issues')
|
||||
|
||||
def _compute_issue_count(self):
|
||||
Issue = self.pool['project.issue']
|
||||
partners = {id: self.search([('id', 'child_of', self.ids)]) for id in self.ids}
|
||||
for partner_id in partners.keys():
|
||||
partner.issue_count = Issue.search_count([('partner_id', 'in', partners[partner_id])])
|
||||
Issue = self.env['project.issue']
|
||||
partners = {id: self.search([('id', 'child_of', self.ids)]).ids for id in self.ids}
|
||||
for partner in self:
|
||||
partner.issue_count = Issue.search_count([('partner_id', 'in', partners[partner.id])])
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="project_config_settings_view_form_inherit_project_issue" model="ir.ui.view">
|
||||
<field name="name">project.config.settings.view.form.inherit.project.issue</field>
|
||||
<field name="model">project.config.settings</field>
|
||||
<field name="inherit_id" ref="project.view_config_settings"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='timesheets']" position="after">
|
||||
<group string="Helpdesk & Support">
|
||||
<field name="module_rating_project_issue" widget="radio"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -8,16 +8,24 @@ from openerp.http import request
|
||||
class Rating(http.Controller):
|
||||
|
||||
@http.route('/rating/<string:token>/<int:rate>', type='http', auth="public")
|
||||
def add_rating(self, token, rate, **kwargs):
|
||||
Rating = request.env['rating.rating']
|
||||
rating = Rating.search([('access_token', '=', token)])
|
||||
if rating:
|
||||
is_rated = bool(rating.rating != -1)
|
||||
if not is_rated:
|
||||
request.env['rating.rating'].apply_rating(rate, token=token)
|
||||
# redirect to the form view if logged person
|
||||
if request.session.uid and not is_rated:
|
||||
record = request.env[rating.res_model].browse(rating.res_id)
|
||||
return werkzeug.utils.redirect('/web#model=%s&id=%s&view_type=form' % (record._name, record.id))
|
||||
return request.render('rating.rating_external_page_view', {'rating': rate, 'is_rated': is_rated})
|
||||
def open_rating(self, token, rate, **kwargs):
|
||||
rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)])
|
||||
if not rating.consumed:
|
||||
return request.render('rating.rating_external_page_submit', {'rating': rate, 'token': token})
|
||||
else:
|
||||
return request.render('rating.rating_external_page_view', {'is_rated': True})
|
||||
return request.not_found()
|
||||
|
||||
@http.route(['/rating/<string:token>/<int:rate>/submit_feedback'], type="http", auth="public", method=['post'])
|
||||
def submit_rating(self, token, rate, **kwargs):
|
||||
rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)])
|
||||
if not rating.consumed:
|
||||
record_sudo = request.env[rating.res_model].sudo().browse(rating.res_id)
|
||||
record_sudo.rating_apply(rate, token=token, feedback=kwargs.get('feedback'))
|
||||
# redirect to the form view if logged person
|
||||
if request.session.uid:
|
||||
return werkzeug.utils.redirect('/web#model=%s&id=%s&view_type=form' % (record_sudo._name, record_sudo.id))
|
||||
return request.render('rating.rating_external_page_view', {'is_public': True})
|
||||
else:
|
||||
return request.render('rating.rating_external_page_view', {'is_rated': True})
|
||||
return request.not_found()
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import uuid
|
||||
from openerp import api, fields, models, _
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class Rating(models.Model):
|
||||
|
||||
_name = "rating.rating"
|
||||
_description = "Rating"
|
||||
_order = 'create_date desc'
|
||||
_order = 'write_date desc'
|
||||
_rec_name = 'res_name'
|
||||
_sql_constraints = [
|
||||
('rating_range', 'check(rating >= -1 and rating <= 10)', 'Rating should be between -1 to 10'),
|
||||
('rating_range', 'check(rating >= 0 and rating <= 10)', 'Rating should be between 0 to 10'),
|
||||
]
|
||||
|
||||
@api.one
|
||||
@@ -27,99 +28,101 @@ class Rating(models.Model):
|
||||
res_id = fields.Integer(string='Document ID', required=True, help="Identifier of the rated object", index=True)
|
||||
rated_partner_id = fields.Many2one('res.partner', string="Rated Partner", help="Owner of the rated resource")
|
||||
partner_id = fields.Many2one('res.partner', string='Customer', help="Author of the rating")
|
||||
rating = fields.Float(string="Rating", group_operator="avg", default=-1, help="Rating value")
|
||||
rating = fields.Float(string="Rating", group_operator="avg", default=0, help="Rating value")
|
||||
feedback = fields.Text('Feedback reason', help="Reason of the rating")
|
||||
access_token = fields.Char(string='Security Token', default=new_access_token, help="Access token to set the rating of the value")
|
||||
|
||||
message_id = fields.Many2one('mail.message', string="Linked message", help="Associated message when posting a review. Mainly used in website addons.", index=True)
|
||||
|
||||
@api.model
|
||||
def apply_rating(self, rate, res_model=None, res_id=None, token=None):
|
||||
""" apply a rating for given res_model/res_id or token. If the res_model is a mail.thread
|
||||
object, a message will be posted in the chatter.
|
||||
:param rate : the rating value to apply
|
||||
:type rate : float
|
||||
:param res_id : id of the rated object.
|
||||
:param res_model : name of model.
|
||||
:param token : access token
|
||||
:returns rating.rating record
|
||||
"""
|
||||
domain = [('access_token', '=', token)] if token else [('res_model', '=', res_model), ('res_id', '=', res_id)]
|
||||
rating = self.search(domain, limit=1)
|
||||
if rating:
|
||||
if token:
|
||||
rating = rating.sudo()
|
||||
rating.write({'rating' : rate})
|
||||
if hasattr(self.env[rating.res_model], 'message_post'):
|
||||
record = self.env[rating.res_model].sudo().browse(rating.res_id)
|
||||
record.sudo().message_post(
|
||||
body="%s %s <br/><img src='/rating/static/src/img/rating_%s.png' style='width:20px;height:20px'/>"
|
||||
% (rating.sudo().partner_id.name, _('rated it'), rate),
|
||||
subtype='mail.mt_comment',
|
||||
author_id=rating.partner_id and rating.partner_id.id or None # None will set the default author in mail_thread.py
|
||||
)
|
||||
return rating
|
||||
access_token = fields.Char('Security Token', default=new_access_token, help="Access token to set the rating of the value")
|
||||
consumed = fields.Boolean(string="Filled Rating", help="Enabled if the rating has been filled.")
|
||||
|
||||
@api.multi
|
||||
def reset(self):
|
||||
for record in self:
|
||||
record.write({
|
||||
'rating': -1,
|
||||
'rating': 0,
|
||||
'access_token': record.new_access_token(),
|
||||
'feedback' : False
|
||||
'feedback': False,
|
||||
'consumed': False,
|
||||
})
|
||||
|
||||
|
||||
class RatingMixin(models.AbstractModel):
|
||||
|
||||
_name = 'rating.mixin'
|
||||
_description = "Rating Mixin"
|
||||
|
||||
rating_ids = fields.One2many('rating.rating', 'res_id', string='Rating', domain=lambda self: [('res_model', '=', self._name)])
|
||||
rating_last_value = fields.Float('Last Value', related='rating_ids.rating', store=True)
|
||||
rating_count = fields.Integer('Rating count', compute="_compute_rating_count")
|
||||
|
||||
@api.multi
|
||||
def rating_send_request(self, template, partner_id, rated_partner_id, reuse_rating=True):
|
||||
""" This method create (empty) rating objects for the current recordsets
|
||||
and send this request by mail (given email template) with the given
|
||||
rated_partner_id and partner_id as recipient and sender of the email.
|
||||
:param template : the email template to send. The res_model of the
|
||||
template must be 'rating.rating'.
|
||||
:type template : mail.template
|
||||
:param res_model : model name of the object to rated_partner_id
|
||||
:type res_model : string
|
||||
:param res_id : id the record to rate
|
||||
:type res_id : int
|
||||
:param partner_id : the recipient partner
|
||||
:type partner : res.partner
|
||||
:param rated_partner_id : the sender partner
|
||||
:type rated_partner_id : res.partner
|
||||
:param reuse_rating : if True, the rating of the current objects will
|
||||
be reset. Otherwise a new rating will be create
|
||||
:type reuse_rating : boolean
|
||||
"""
|
||||
if not rated_partner_id.email or not partner_id.email:
|
||||
return False
|
||||
Rating = self.env['rating.rating']
|
||||
res_model = self._name
|
||||
def _compute_rating_count(self):
|
||||
read_group_res = self.env['rating.rating'].read_group(
|
||||
[('res_model', '=', self._name), ('res_id', 'in', self.ids), ('consumed', '=', True)],
|
||||
['res_id'], groupby=['res_id'])
|
||||
result = dict.fromkeys(self.ids, 0)
|
||||
for data in read_group_res:
|
||||
result[data['res_id']] += data['res_id_count']
|
||||
for record in self:
|
||||
res_id = record.id
|
||||
values = {
|
||||
'res_model': res_model,
|
||||
'res_id': res_id,
|
||||
'partner_id': partner_id.id,
|
||||
'rated_partner_id': rated_partner_id.id
|
||||
}
|
||||
if reuse_rating:
|
||||
# search the existing rating for the given res_model/res_id
|
||||
rating = Rating.search([('res_id', '=', res_id), ('res_model', '=', res_model), ('partner_id', '=', partner_id.id)], limit=1)
|
||||
if rating: # reset the rating
|
||||
rating.reset()
|
||||
else: # create a new one
|
||||
rating = Rating.create(values)
|
||||
else:
|
||||
rating = Rating.create(values)
|
||||
# send the mail
|
||||
template.send_mail(rating.id, force_send=True)
|
||||
record.rating_count = result[record.id]
|
||||
|
||||
def rating_get_partner_id(self):
|
||||
if hasattr(self, 'partner_id') and self.partner_id:
|
||||
return self.partner_id
|
||||
return self.env['res.partner']
|
||||
|
||||
def rating_get_rated_partner_id(self):
|
||||
if hasattr(self, 'user_id') and self.user_id.partner_id:
|
||||
return self.user_id.partner_id
|
||||
return self.env['res.partner']
|
||||
|
||||
def rating_get_access_token(self, partner=None):
|
||||
if not partner:
|
||||
partner = self.rating_get_partner_id()
|
||||
ratings = self.rating_ids.filtered(lambda x: x.partner_id == partner.id and not x.consumed)
|
||||
if not ratings:
|
||||
rating = self.env['rating.rating'].create({'partner_id': partner.id, 'res_model': self._name, 'res_id': self.id})
|
||||
else:
|
||||
rating = ratings[0]
|
||||
return rating.access_token
|
||||
|
||||
@api.multi
|
||||
def rating_send_request(self, template, partner=None, rated_partner=None, reuse_rating=True, force_send=True):
|
||||
""" This method send rating request by email, using a template given
|
||||
in parameter. """
|
||||
for record in self:
|
||||
template.send_mail(record.id, force_send=force_send)
|
||||
|
||||
@api.multi
|
||||
def rating_apply(self, rate, token=None, feedback=None, subtype=None):
|
||||
""" Apply a rating given a token. If the current model inherits from
|
||||
mail.thread mixing, a message is posted on its chatter.
|
||||
:param rate : the rating value to apply
|
||||
:type rate : float
|
||||
:param token : access token
|
||||
:param feedback : additional feedback
|
||||
:type feedback : string
|
||||
:param subtype : subtype for mail
|
||||
:type subtype : string
|
||||
:returns rating.rating record
|
||||
"""
|
||||
Rating, rating = self.env['rating.rating'], None
|
||||
if token:
|
||||
rating = self.env['rating.rating'].search([('access_token', '=', token), ('consumed', '=', False)], limit=1)
|
||||
else:
|
||||
rating = Rating.search([('res_model', '=', self._name), ('res_id', '=', self.ids[0]), ('consumed', '=', False)], limit=1)
|
||||
if rating:
|
||||
rating.write({'rating': rate, 'feedback': feedback, 'consumed': True})
|
||||
if hasattr(self, 'message_post'):
|
||||
self.message_post(
|
||||
body="<img src='/rating/static/src/img/rating_%s.png' style='width:20px;height:20px'/>%s"
|
||||
% (rate, '<br/>' + feedback if feedback else ''),
|
||||
subtype=subtype or "mail.mt_comment",
|
||||
author_id=rating.partner_id and rating.partner_id.id or None # None will set the default author in mail_thread.py
|
||||
)
|
||||
if hasattr(self, 'stage_id') and self.stage_id and hasattr(self.stage_id, 'auto_validation_kanban_state') and self.stage_id.auto_validation_kanban_state:
|
||||
if rating.rating > 5:
|
||||
self.write({'kanban_state': 'done'})
|
||||
else:
|
||||
self.write({'kanban_state': 'blocked'})
|
||||
return rating
|
||||
|
||||
@api.multi
|
||||
def rating_get_repartition(self, add_stats=False, domain=None):
|
||||
@@ -134,20 +137,20 @@ class RatingMixin(models.AbstractModel):
|
||||
otherwise, key is the value of the information (string) : either stat name (avg, total, ...) or 'repartition'
|
||||
containing the same dict if add_stats was False.
|
||||
"""
|
||||
base_domain = [('res_model', '=', self._name), ('res_id', 'in', self.ids), ('rating', '>=', 0)]
|
||||
base_domain = [('res_model', '=', self._name), ('res_id', 'in', self.ids), ('rating', '>=', 1), ('consumed', '=', True)]
|
||||
if domain:
|
||||
base_domain += domain
|
||||
data = self.env['rating.rating'].read_group(base_domain, ['rating'], ['rating', 'res_id'])
|
||||
# init dict with all posible rate value, except -1 (no value for the rating)
|
||||
values = dict.fromkeys(range(11), 0)
|
||||
# init dict with all posible rate value, except 0 (no value for the rating)
|
||||
values = dict.fromkeys(range(1, 11), 0)
|
||||
values.update((d['rating'], d['rating_count']) for d in data)
|
||||
# add other stats
|
||||
if add_stats:
|
||||
rating_number = sum(values.values())
|
||||
result = {
|
||||
'repartition': values,
|
||||
'avg': sum([float(key*values[key]) for key in values])/rating_number if rating_number > 0 else 0,
|
||||
'total': reduce(lambda x, y: y['rating_count']+x, data, 0),
|
||||
'avg': sum([float(key * values[key]) for key in values]) / rating_number if rating_number > 0 else 0,
|
||||
'total': reduce(lambda x, y: y['rating_count'] + x, data, 0),
|
||||
}
|
||||
return result
|
||||
return values
|
||||
@@ -185,7 +188,7 @@ class RatingMixin(models.AbstractModel):
|
||||
result = {
|
||||
'avg': data['avg'],
|
||||
'total': data['total'],
|
||||
'percent': dict.fromkeys(range(11), 0),
|
||||
'percent': dict.fromkeys(range(1, 11), 0),
|
||||
}
|
||||
for rate in data['repartition']:
|
||||
result['percent'][rate] = (data['repartition'][rate] * 100) / data['total'] if data['total'] > 0 else 0
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
@@ -98,16 +98,44 @@
|
||||
<template id="rating_external_page_view">
|
||||
<head>
|
||||
<link rel='stylesheet' href='/web/static/lib/bootstrap/css/bootstrap.css'/>
|
||||
<link rel='stylesheet' href='/web/static/lib/fontawesome/css/font-awesome.css'/>
|
||||
</head>
|
||||
<div class='well' t-if="not is_rated">
|
||||
<h1>Thanks for voting !</h1>
|
||||
<div class="oe_right">
|
||||
<p>Your vote has been taken into account.</p>
|
||||
<p>It was <img t-attf-src='../../../rating/static/src/img/rating_#{rating}.png'/></p>
|
||||
<div class="container">
|
||||
<div clas="row" t-if="is_rated">
|
||||
<h1 class="text-center"><i class="fa fa-frown-o fa-2x text-danger" /></h1>
|
||||
<h1 class="text-center">Sorry you have already voted!</h1>
|
||||
</div>
|
||||
<div clas="row" t-if="is_public">
|
||||
<h1 class="text-center"><i class="fa fa-check-circle fa-2x text-success" /></h1>
|
||||
<h1 class="text-center">Thank you for submitting your feedback!</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class='well' t-if="is_rated">
|
||||
<h1>Sorry you have already voted !</h1>
|
||||
</template>
|
||||
|
||||
<!-- External page: rate and submit feedback -->
|
||||
<template id="rating_external_page_submit">
|
||||
<head>
|
||||
<link rel='stylesheet' href='/web/static/lib/bootstrap/css/bootstrap.css'/>
|
||||
</head>
|
||||
<div class="container">
|
||||
<div clas="row">
|
||||
<h1 class="text-center">Please submit your rating</h1>
|
||||
<h4 class="text-center">You selected <img t-attf-src='/rating/static/src/img/rating_#{rating}.png'/></h4>
|
||||
<form class="form-horizontal" t-attf-action="/rating/#{token}/#{rating}/submit_feedback" method="post">
|
||||
<div class="form-group">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h5>Do you want to give additional explanation? (Optional)</h5>
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<textarea class="form-control" name="feedback" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -7,25 +7,19 @@
|
||||
<field name="arch" type="xml">
|
||||
<kanban create="false" class="oe_background_grey">
|
||||
<field name="rating"/>
|
||||
<field name="res_id"/>
|
||||
<field name="res_model"/>
|
||||
<field name="res_name"/>
|
||||
<field name="create_date"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="rated_partner_id"/>
|
||||
<field name="feedback"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<a t-att-href="_.str.sprintf('/web?#id=%s&view_type=form&model=%s', record.res_id.raw_value, record.res_model.raw_value)">
|
||||
<img t-attf-src="rating/static/src/img/rating_#{record.rating.raw_value}.png"
|
||||
t-attf-title="Name : #{ record.res_name.raw_value } <br>
|
||||
#{ record.partner_id.value ? 'Customer : ' + record.partner_id.value + '<br>' : '' }
|
||||
#{ record.rated_partner_id.value ? 'Responsible : ' + record.rated_partner_id.value+'<br>' : '' }
|
||||
Date : #{ moment(record.create_date.raw_value).format('L') }
|
||||
#{ record.feedback.raw_value ? '<br>'+'Reason : ' + record.feedback.raw_value : '' }"
|
||||
data-html="true" data-placement="bottom"/>
|
||||
</a>
|
||||
<div t-att-title="record.feedback.raw_value ? 'Feedback:\n' + record.feedback.raw_value : ''">
|
||||
<img t-attf-src="/rating/static/src/img/rating_#{record.rating.raw_value}.png"
|
||||
data-html="true" data-placement="bottom" class="pull-left"/>
|
||||
<div style="margin-left: 80px">
|
||||
<div><b><field name="rated_partner_id"/></b></div>
|
||||
<div><span class="text-muted">by </span><field name="partner_id"/></div>
|
||||
<div t-if="record.res_name.raw_value"><span class="text-muted">for </span><field name="res_name"/></div>
|
||||
<div><span class="text-muted">on </span><field name="create_date"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -38,7 +32,8 @@
|
||||
<field name="model">rating.rating</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Rating Average" display_quantity="true">
|
||||
<field name="res_name" type="row"/>
|
||||
<field name="rated_partner_id" type="row"/>
|
||||
<field name="create_date" type="col"/>
|
||||
<field name="rating" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
@@ -49,7 +44,7 @@
|
||||
<field name="model">rating.rating</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Rating Average" type="bar">
|
||||
<field name="res_name" type="row"/>
|
||||
<field name="create_date" type="row"/>
|
||||
<field name="rating" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
@@ -67,9 +62,11 @@
|
||||
<field name="res_id"/>
|
||||
<filter string="Happy" domain="[('rating', '>=', 7)]"/>
|
||||
<filter string="Okay" domain="[('rating', '>', 3), ('rating', '<', 7)]"/>
|
||||
<filter string="Unhappy" domain="[('rating', '>=', 0), ('rating', '<=',3)]"/>
|
||||
<filter string="Unhappy" domain="[('rating', '>=', 1), ('rating', '<=',3)]"/>
|
||||
<separator/>
|
||||
<filter string="My Ratings" domain="[('rated_partner_id.user_ids', 'in', [uid])]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Responsible" name="Responsible" context="{'group_by':'rated_partner_id'}"/>
|
||||
<filter string="Rated User" name="Responsible" context="{'group_by':'rated_partner_id'}"/>
|
||||
<filter string="Customer" name="customer" context="{'group_by':'partner_id'}"/>
|
||||
<filter string="Rating" name="rating" context="{'group_by':'rating'}"/>
|
||||
<filter string="Resource" name="resource" context="{'group_by':'res_name'}"/>
|
||||
@@ -84,7 +81,8 @@
|
||||
<record id="action_view_rating" model="ir.actions.act_window">
|
||||
<field name="name">Rating</field>
|
||||
<field name="res_model">rating.rating</field>
|
||||
<field name="view_mode">graph,pivot,kanban</field>
|
||||
<field name="view_mode">kanban,graph,pivot</field>
|
||||
<field name="domain">[('consumed','=',True)]</field>
|
||||
<field name="help" type="html">
|
||||
<p>
|
||||
There is no rating for this object at the moment.
|
||||
|
||||
@@ -1,51 +1,109 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="ir_cron_rating_project" model="ir.cron">
|
||||
<field name="name">Send Customer Rating Forms</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="function">_send_rating_all</field>
|
||||
</record>
|
||||
|
||||
<record id="mt_task_rating" model="mail.message.subtype">
|
||||
<field name="name">Task Rating</field>
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="description">Ratings</field>
|
||||
</record>
|
||||
|
||||
<record id="mt_project_task_rating" model="mail.message.subtype">
|
||||
<field name="name">Task Rating</field>
|
||||
<field name="sequence">14</field>
|
||||
<field name="res_model">project.project</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="parent_id" eval="ref('mt_task_rating')"/>
|
||||
<field name="relation_field">project_id</field>
|
||||
</record>
|
||||
|
||||
<record id="rating_project_request_email_template" model="mail.template">
|
||||
<field name="name">Rating Project Request</field>
|
||||
<field name="email_from">${object.rated_partner_id.email or ''}</field>
|
||||
<field name="subject">${object.res_name}</field>
|
||||
<field name="model_id" ref="rating.model_rating_rating"/>
|
||||
<field name="email_to" >${object.partner_id.email}</field>
|
||||
<field name="name">Task: Rating Request</field>
|
||||
<field name="email_from">${object.rating_get_rated_partner_id().email or '' | safe}</field>
|
||||
<field name="subject">${object.project_id.company_id.name}: Service Rating Request</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="partner_to" >${object.rating_get_partner_id().id}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
<field name="body_html"><![CDATA[
|
||||
<body>
|
||||
<div style="max-width:600px; height:auto; margin-left:30px;">
|
||||
<div style="margin-left:30px;align=center;">
|
||||
Dear ${object.partner_id.name},
|
||||
</div><br>
|
||||
<div style="margin-left:30px;align=center;" width="60%">
|
||||
We hope the services provided have met your expectations.
|
||||
<br>If you have any more questions or comments, do not hesitate to reply to this e-mail.
|
||||
<br><br>Please, help us to improve ourself by rating the service we provide.
|
||||
</div><br>
|
||||
<table style="margin-left:50px;" width="70%">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/rating/${object.access_token}/10">
|
||||
<img alt="Great" src="rating/static/src/img/rating_10.png" title="It was great"/>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/rating/${object.access_token}/5">
|
||||
<img alt="Okay" src="rating/static/src/img/rating_5.png" title="It was okay"/>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/rating/${object.access_token}/0">
|
||||
<img alt="Bad" src="rating/static/src/img/rating_0.png" title="It wasn't good"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table><br>
|
||||
<h4 style="margin-left:30px;font-weight:normal;">Thank you </h4>
|
||||
<h4 style="margin-left:30px;font-weight:normal;">${object.rated_partner_id.name or ''} </h4>
|
||||
</div>
|
||||
</body>
|
||||
]]>
|
||||
</field>
|
||||
% set access_token = object.rating_get_access_token()
|
||||
<div style="background:#F0F0F0;color:#515166;padding:10px 0px;font-family:Arial,Helvetica,sans-serif;font-size:14px;">
|
||||
<table style="width:600px;margin:5px auto;">
|
||||
<tbody>
|
||||
<tr><td>
|
||||
<a href="/"><img src="/web/binary/company_logo" style="vertical-align:baseline;max-width:100px;" /></a>
|
||||
</td><td style="text-align:right;vertical-align:middle;">
|
||||
Satisfaction Survey
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table style="width:600px;margin:0px auto;background:white;border:1px solid #e1e1e1;">
|
||||
<tbody>
|
||||
<tr><td style="padding:15px 20px 10px 20px;">
|
||||
<p>Hello,</p>
|
||||
<p>Please take a moment to rate our services related to the task "<strong>${object.name}</strong>"
|
||||
assigned to <strong>${object.rating_get_rated_partner_id().name}</strong>.</p>
|
||||
<p>We appreciate your feedback. It helps us to improve continuously.</p>
|
||||
</td></tr>
|
||||
<tr><td style="padding:10px 20px">
|
||||
<table style="width:100%;border-top:1px solid #e1e1e1;">
|
||||
<tr>
|
||||
<td style="text-align:center;">
|
||||
<h2 style="font-weight:300;font-size:18px;">
|
||||
Tell us how you feel about our service
|
||||
</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 10px;">
|
||||
<table style="width:100%;text-align:center;">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/rating/${access_token}/10">
|
||||
<img alt="Great" src="/rating/static/src/img/rating_10.png" title="It was great"/>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/rating/${access_token}/5">
|
||||
<img alt="Okay" src="/rating/static/src/img/rating_5.png" title="It was okay"/>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/rating/${access_token}/1">
|
||||
<img alt="Bad" src="/rating/static/src/img/rating_1.png" title="It wasn't good"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td style="padding:15px 20px 10px 20px;">${object.user_id.signature | safe}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table style="width:600px;margin:auto;text-align:center;font-size:12px;">
|
||||
<tbody>
|
||||
<tr><td style="padding-top:10px;color:#afafaf;">
|
||||
% if object.project_id.rating_status == 'stage':
|
||||
<p>This customer survey has been sent because your task has been moved to the stage <b>${object.stage_id.name}</b></p>
|
||||
% endif
|
||||
% if object.project_id.rating_status == 'periodic':
|
||||
<p>This customer survey has been sent <b>${object.project_id.rating_status_period}</b> as long as the task is in the <b>${object.stage_id.name}</b> stage.
|
||||
% endif
|
||||
<p>Email automatically sent by <a href="https://www.odoo.com/page/project-management" style="color:#a24689;text-decoration:none;">Odoo Project</a> for <a href="${object.project_id.company_id.website}" style="color:#a24689;text-decoration:none;">${object.project_id.company_id.name}</a></p>
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
]]></field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -3,22 +3,32 @@
|
||||
<data noupdate="1">
|
||||
<record id="rating_task_1" model="rating.rating">
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="rated_partner_id" ref="base.partner_root"/>
|
||||
<field name="partner_id" ref="portal.demo_user0_res_partner"/>
|
||||
<field name="res_id" ref="project.project_task_3"/>
|
||||
</record>
|
||||
<record id="rating_task_2" model="rating.rating">
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="rated_partner_id" ref="base.partner_demo"/>
|
||||
<field name="partner_id" ref="base.partner_demo"/>
|
||||
<field name="res_id" ref="project.project_task_8"/>
|
||||
</record>
|
||||
<record id="rating_task_3" model="rating.rating">
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="rated_partner_id" ref="base.partner_root"/>
|
||||
<field name="partner_id" ref="base.partner_root"/>
|
||||
<field name="res_id" ref="project.project_task_15"/>
|
||||
</record>
|
||||
<record id="rating_task_4" model="rating.rating">
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="res_id" ref="project.project_task_18"/>
|
||||
<field name="rated_partner_id" ref="base.partner_root"/>
|
||||
<field name="partner_id" ref="base.partner_root"/>
|
||||
<field name="res_id" ref="project.project_task_18"/>
|
||||
</record>
|
||||
<record id="rating_task_5" model="rating.rating">
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="rated_partner_id" ref="base.partner_root"/>
|
||||
<field name="partner_id" ref="base.res_partner_4"/>
|
||||
<field name="res_id" ref="project.project_task_24"/>
|
||||
</record>
|
||||
|
||||
@@ -26,11 +36,14 @@
|
||||
<record id="project.project_stage_2" model="project.task.type">
|
||||
<field name="rating_template_id" ref="rating_project_request_email_template"/>
|
||||
</record>
|
||||
<record id="project.project_project_3" model="project.project">
|
||||
<field name="rating_status">stage</field>
|
||||
</record>
|
||||
|
||||
<function model="rating.rating" name="apply_rating" eval="(10, 'project.task', ref('project.project_task_3'))"/>
|
||||
<function model="rating.rating" name="apply_rating" eval="(0, 'project.task', ref('project.project_task_8'))"/>
|
||||
<function model="rating.rating" name="apply_rating" eval="(5, 'project.task', ref('project.project_task_15'))"/>
|
||||
<function model="rating.rating" name="apply_rating" eval="(10, 'project.task', ref('project.project_task_18'))"/>
|
||||
<function model="rating.rating" name="apply_rating" eval="(10, 'project.task', ref('project.project_task_24'))"/>
|
||||
<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_18')], 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_8')], 1)"/>
|
||||
<function model="project.task" name="rating_apply" eval="([ref('project.project_task_15')], 5)"/>
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from openerp import models, fields, api
|
||||
from datetime import timedelta
|
||||
from openerp import api, fields, models, _
|
||||
|
||||
|
||||
class ProjectTaskType(models.Model):
|
||||
@@ -9,38 +10,62 @@ class ProjectTaskType(models.Model):
|
||||
rating_template_id = fields.Many2one(
|
||||
'mail.template',
|
||||
string='Rating Email Template',
|
||||
domain=[('model', '=', 'rating.rating')],
|
||||
domain=[('model', '=', 'project.task')],
|
||||
help="Select an email template. An email will be sent to the customer when the task reach this step.")
|
||||
auto_validation_kanban_state = fields.Boolean('Auto Kanban state validation', default=False,
|
||||
auto_validation_kanban_state = fields.Boolean('Automatic kanban status', default=False,
|
||||
help="Automatically modify the kanban state when the customer reply to the feedback for this stage.\n"
|
||||
" * A great feedback from the customer will update the kanban state to 'ready for the new stage' (green bullet).\n"
|
||||
" * A good feedback from the customer will update the kanban state to 'ready for the new stage' (green bullet).\n"
|
||||
" * A medium or a bad feedback will set the kanban state to 'blocked' (red bullet).\n")
|
||||
|
||||
class Task(models.Model):
|
||||
|
||||
class Task(models.Model):
|
||||
_name = 'project.task'
|
||||
_inherit = ['project.task', 'rating.mixin']
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
res = super(Task, self).write(values)
|
||||
if 'stage_id' in values and values.get('stage_id'):
|
||||
template = self.env['project.task.type'].browse(values.get('stage_id')).rating_template_id
|
||||
if template:
|
||||
rated_partner_id = self.user_id.partner_id
|
||||
partner_id = self.partner_id
|
||||
if partner_id and rated_partner_id:
|
||||
self.rating_send_request(template, partner_id, rated_partner_id)
|
||||
return super(Task, self).write(values)
|
||||
self.filtered(lambda x: x.project_id.rating_status == 'stage')._send_task_rating_mail()
|
||||
return res
|
||||
|
||||
def _send_task_rating_mail(self):
|
||||
for task in self:
|
||||
rating_template = task.stage_id.rating_template_id
|
||||
if rating_template:
|
||||
task.rating_send_request(rating_template, reuse_rating=False)
|
||||
|
||||
def _rating_get_partner_id(self):
|
||||
res = super(Task, self)._rating_get_partner_id()
|
||||
if not res and self.project_id.partner_id:
|
||||
return self.project_id.partner_id
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def rating_apply(self, rate, token=None, feedback=None, subtype=None):
|
||||
return super(Task, self).rating_apply(rate, token=token, feedback=feedback, subtype="rating_project.mt_task_rating")
|
||||
|
||||
class Project(models.Model):
|
||||
|
||||
_inherit = "project.project"
|
||||
|
||||
@api.one
|
||||
# This method should be called once a day by the scheduler
|
||||
@api.model
|
||||
def _send_rating_all(self):
|
||||
projects = self.search([('rating_status', '=', 'periodic'), ('rating_request_deadline', '<=', fields.Datetime.now())])
|
||||
projects._send_rating_mail()
|
||||
projects._compute_rating_request_deadline()
|
||||
|
||||
def _send_rating_mail(self):
|
||||
for project in self:
|
||||
project.task_ids._send_task_rating_mail()
|
||||
|
||||
@api.depends('percentage_satisfaction_task')
|
||||
def _compute_percentage_satisfaction_project(self):
|
||||
self.percentage_satisfaction_project = self.percentage_satisfaction_task
|
||||
domain = [('create_date', '>=', fields.Datetime.to_string(fields.datetime.now() - timedelta(days=30)))]
|
||||
for project in self:
|
||||
activity = project.tasks.rating_get_grades(domain)
|
||||
project.percentage_satisfaction_project = activity['great'] * 100 / sum(activity.values()) if sum(activity.values()) else -1
|
||||
|
||||
@api.one
|
||||
@api.depends('tasks.rating_ids.rating')
|
||||
@@ -52,38 +77,29 @@ class Project(models.Model):
|
||||
compute='_compute_percentage_satisfaction_task', string="Happy % on Task", store=True, default=-1)
|
||||
percentage_satisfaction_project = fields.Integer(
|
||||
compute="_compute_percentage_satisfaction_project", string="Happy % on Project", store=True, default=-1)
|
||||
is_visible_happy_customer = fields.Boolean(string="Customer Satisfaction", default=False,
|
||||
help="Display information about rating of the project on kanban and form view. This buttons will only be displayed if at least a rating exists.")
|
||||
rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True)
|
||||
rating_status = fields.Selection([('stage', 'Rating on Stage'), ('periodic', 'Periodical Rating')], 'Customer(s) Ratings', help="How to get the customer's feedbacks?\n"
|
||||
"- Rating on stage: Email will be sent when a task/issue is pulled in another stage\n"
|
||||
"- Periodical Rating: Email will be sent periodically\n\n"
|
||||
"Don't forget to set up the mail templates on the stages for which you want to get the customer's feedbacks.")
|
||||
rating_status_period = fields.Selection([
|
||||
('daily', 'Daily'), ('weekly', 'Weekly'), ('bimonthly', 'Twice a Month'),
|
||||
('monthly', 'Once a Month'), ('quarterly', 'Quarterly'), ('yearly', 'Yearly')
|
||||
], 'Rating Frequency')
|
||||
|
||||
@api.depends('rating_status', 'rating_status_period')
|
||||
def _compute_rating_request_deadline(self):
|
||||
periods = {'daily': 1, 'weekly': 7, 'bimonthly': 15, 'monthly': 30, 'quarterly': 90, 'yearly': 365}
|
||||
for project in self:
|
||||
project.rating_request_deadline = fields.datetime.now() + timedelta(days=periods.get(project.rating_status_period, 0))
|
||||
|
||||
@api.multi
|
||||
def action_view_task_rating(self):
|
||||
""" return the action to see all the rating about the tasks of the project """
|
||||
action = self.env['ir.actions.act_window'].for_xml_id('rating', 'action_view_rating')
|
||||
return dict(action, domain=[('rating', '!=', -1), ('res_id', 'in', self.tasks.ids), ('res_model', '=', 'project.task')])
|
||||
return dict(action, domain=[('res_id', 'in', self.tasks.ids), ('res_model', '=', 'project.task')])
|
||||
|
||||
@api.multi
|
||||
def action_view_all_rating(self):
|
||||
""" return the action to see all the rating about the all sort of activity of the project (tasks, issues, ...) """
|
||||
return self.action_view_task_rating()
|
||||
|
||||
|
||||
|
||||
class Rating(models.Model):
|
||||
|
||||
_inherit = "rating.rating"
|
||||
|
||||
@api.model
|
||||
def apply_rating(self, rate, res_model=None, res_id=None, token=None):
|
||||
""" check if the auto_validation_kanban_state is activated. If so, apply the modification of the
|
||||
kanban state according to the given rating.
|
||||
"""
|
||||
rating = super(Rating, self).apply_rating(rate, res_model, res_id, token)
|
||||
if rating.res_model == 'project.task':
|
||||
task = self.env[rating.res_model].sudo().browse(rating.res_id)
|
||||
if task.stage_id.auto_validation_kanban_state:
|
||||
if rating.rating > 5:
|
||||
task.write({'kanban_state' : 'done'})
|
||||
else:
|
||||
task.write({'kanban_state' : 'blocked'})
|
||||
return rating
|
||||
|
||||
@@ -6,15 +6,26 @@
|
||||
<field name="arch" type="xml">
|
||||
<field name="use_tasks" position="after">
|
||||
<field name="percentage_satisfaction_project" invisible="1"/>
|
||||
<field name="is_visible_happy_customer" invisible="1"/>
|
||||
<field name="rating_status" invisible="1"/>
|
||||
</field>
|
||||
|
||||
<xpath expr="//div[contains(@class, 'o_kanban_card_content')]" position="inside">
|
||||
<div t-if="record.is_visible_happy_customer.raw_value and record.percentage_satisfaction_project.raw_value >= 0" class="row text-center o_project_kanban_bugs">
|
||||
<a name="action_view_all_rating" type="object" title="Percentage of happy people about this project" class="pull-right">
|
||||
<h5 t-attf-class="label #{record.percentage_satisfaction_project.raw_value >= 50 ? 'label-success' : 'label-warning'}">
|
||||
<i t-attf-class="fa #{record.percentage_satisfaction_project.raw_value >= 50 ? 'fa-smile-o' : 'fa-frown-o'}"/> <t t-raw="record.percentage_satisfaction_project.raw_value"/> %
|
||||
</h5>
|
||||
<xpath expr="//div[contains(@class, 'o_kanban_primary_left')]" position="inside">
|
||||
<div t-if="record.rating_status.raw_value" class="mt8 text-primary" title="Percentage of happy ratings over the past 30 days. Get rating details from the More menu.">
|
||||
<b>
|
||||
<i class="fa fa-smile-o"/>
|
||||
<t t-if="record.percentage_satisfaction_project.value == -1">
|
||||
No rating
|
||||
</t>
|
||||
<t t-if="record.percentage_satisfaction_project.value != -1">
|
||||
<t t-esc="record.percentage_satisfaction_project.value"/>%
|
||||
</t>
|
||||
</b>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//div[contains(@class, 'o_kanban_card_manage_section')]" position="inside">
|
||||
<div t-if="record.rating_status.raw_value">
|
||||
<a name="action_view_all_rating" type="object">
|
||||
Customers Ratings
|
||||
</a>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
@@ -1,6 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="rating_rating_action_task" model="ir.actions.act_window">
|
||||
<field name="name">Customer Ratings</field>
|
||||
<field name="res_model">rating.rating</field>
|
||||
<field name="view_mode">kanban,pivot,graph</field>
|
||||
<field name="domain">[('res_model', '=', 'project.task'), ('res_id', '=', active_id), ('consumed', '=', True)]</field>
|
||||
<field name="help" type="html">
|
||||
<p>
|
||||
Customer ratings on tasks. If you have no rating, change your project Settings to activate it.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_task_view_form_inherit_rating" model="ir.ui.view">
|
||||
<field name="name">project.task.view.form.inherit.rating</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="%(rating_rating_action_task)d" type="action"
|
||||
class="oe_stat_button" icon="fa-smile-o">
|
||||
<field name="rating_count" string="Rating" widget="statinfo"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_task_view_kanban_inherit_rating" model="ir.ui.view">
|
||||
<field name="name">project.task.view.kanban.inherit.rating</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='color']" position="after">
|
||||
<field name="rating_last_value"/>
|
||||
<field name="rating_ids"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@class='oe_kanban_bottom_left']" position="inside">
|
||||
<b t-if="record.rating_ids.raw_value.length">
|
||||
<span class="fa fa-fw mt4 fa-thumbs-up text-success" t-if="record.rating_last_value.value == 10" title="Latest Rating: Happy"/>
|
||||
<span class="fa fa-fw mt4 fa-thumbs-down text-warning" t-if="record.rating_last_value.value == 5" title="Latest Rating: Average"/>
|
||||
<span class="fa fa-fw mt4 fa-thumbs-down text-danger" t-if="record.rating_last_value.value == 1" title="Latest Rating: Not Happy"/>
|
||||
</b>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- project.project form view with rating satisfaction state button -->
|
||||
<record id="view_project_project_rating_form" model="ir.ui.view">
|
||||
<field name="name">project.project.rating.form.view</field>
|
||||
@@ -10,15 +56,21 @@
|
||||
<field name="arch" type="xml">
|
||||
<div name="button_box" position="inside">
|
||||
<button name="action_view_task_rating"
|
||||
attrs="{'invisible': ['|', '|', ('is_visible_happy_customer', '=', False), ('use_tasks','=', False), ('percentage_satisfaction_task', '=', -1)]}"
|
||||
attrs="{'invisible': ['|', '|', ('rating_status', '=', False), ('use_tasks','=', False), ('percentage_satisfaction_task', '=', -1)]}"
|
||||
class="oe_stat_button oe_percent"
|
||||
type="object"
|
||||
icon="fa-smile-o">
|
||||
<field string="On tasks" name="percentage_satisfaction_task" widget="statinfo"/>
|
||||
<field string="% On tasks" name="percentage_satisfaction_task" widget="statinfo"/>
|
||||
</button>
|
||||
</div>
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="is_visible_happy_customer"/>
|
||||
<label for="rating_status"/>
|
||||
<div>
|
||||
<field name="rating_status" class="oe_inline"/>
|
||||
<span attrs="{'invisible': [('rating_status','!=','periodic')]}"> -
|
||||
<field name="rating_status_period" attrs="{'required': [('rating_status','=','periodic')]}" class="oe_inline"/>
|
||||
</span>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
@@ -37,5 +89,18 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="rating_rating_action_project" model="ir.actions.act_window">
|
||||
<field name="name">Customers Ratings</field>
|
||||
<field name="res_model">rating.rating</field>
|
||||
<field name="view_mode">kanban,pivot,graph</field>
|
||||
<field name="domain">[('res_model','in',['project.task', 'project.issue']), ('consumed', '=', True)]</field>
|
||||
<field name="help" type="html">
|
||||
<p>
|
||||
Customer ratings on tasks/issues. If you have no rating, change your project Settings to activate it.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<menuitem id="rating_rating_menu_project" action="rating_rating_action_project" parent="base.menu_project_report" sequence="5"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -12,6 +12,7 @@ This module allows a customer to give rating on Project Issue.
|
||||
'project_issue'
|
||||
],
|
||||
'data': [
|
||||
'data/project_issue_data.xml',
|
||||
'views/project_issue_view.xml',
|
||||
],
|
||||
'demo': [
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="mt_issue_rating" model="mail.message.subtype">
|
||||
<field name="name">Issue Rating</field>
|
||||
<field name="res_model">project.issue</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="description">Ratings</field>
|
||||
</record>
|
||||
|
||||
<record id="mt_project_issue_rating" model="mail.message.subtype">
|
||||
<field name="name">Issue Rating</field>
|
||||
<field name="sequence">24</field>
|
||||
<field name="res_model">project.project</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="parent_id" eval="ref('mt_issue_rating')"/>
|
||||
<field name="relation_field">project_id</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -3,22 +3,25 @@
|
||||
<data noupdate="1">
|
||||
<record id="rating_issue_1" model="rating.rating">
|
||||
<field name="res_model">project.issue</field>
|
||||
<field name="rated_partner_id" ref="base.partner_demo"/>
|
||||
<field name="partner_id" ref="base.partner_demo"/>
|
||||
<field name="res_id" ref="project_issue.crm_case_outputincorrect0"/>
|
||||
</record>
|
||||
<record id="rating_issue_2" model="rating.rating">
|
||||
<field name="res_model">project.issue</field>
|
||||
<field name="rated_partner_id" ref="base.partner_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field name="res_id" ref="project_issue.crm_case_addmenustothemodule0"/>
|
||||
</record>
|
||||
<record id="rating_issue_3" model="rating.rating">
|
||||
<field name="res_model">project.issue</field>
|
||||
<field name="rated_partner_id" ref="base.partner_root"/>
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="res_id" ref="project_issue.crm_case_improvereportsinhrms0"/>
|
||||
</record>
|
||||
|
||||
<function model="rating.rating" name="apply_rating" eval="(10, 'project.issue',
|
||||
ref('project_issue.crm_case_outputincorrect0'))"/>
|
||||
<function model="rating.rating" name="apply_rating" eval="(0, 'project.issue',
|
||||
ref('project_issue.crm_case_addmenustothemodule0'))"/>
|
||||
<function model="rating.rating" name="apply_rating" eval="(5, 'project.issue',
|
||||
ref('project_issue.crm_case_improvereportsinhrms0'))"/>
|
||||
<function model="project.issue" name="rating_apply" eval="([ref('project_issue.crm_case_outputincorrect0')], 10)"/>
|
||||
<function model="project.issue" name="rating_apply" eval="([ref('project_issue.crm_case_addmenustothemodule0')], 1)"/>
|
||||
<function model="project.issue" name="rating_apply" eval="([ref('project_issue.crm_case_improvereportsinhrms0')], 5)"/>
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -1,65 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import timedelta
|
||||
from openerp import api, fields, models
|
||||
|
||||
|
||||
class ProjectIssue(models.Model):
|
||||
|
||||
_name = "project.issue"
|
||||
_inherit = ['project.issue', 'rating.mixin']
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
res = super(ProjectIssue, self).write(values)
|
||||
if 'stage_id' in values and values.get('stage_id'):
|
||||
template = self.env['project.task.type'].browse(values.get('stage_id')).rating_template_id
|
||||
if template:
|
||||
rated_partner_id = self.user_id.partner_id
|
||||
partner_id = self.partner_id
|
||||
if partner_id and rated_partner_id:
|
||||
self.rating_send_request(template, partner_id, rated_partner_id)
|
||||
return super(ProjectIssue, self).write(values)
|
||||
self.filtered(lambda x: x.project_id.rating_status == 'stage')._send_issue_rating_mail()
|
||||
return res
|
||||
|
||||
def _send_issue_rating_mail(self):
|
||||
for issue in self:
|
||||
rating_template = issue.stage_id.rating_template_id
|
||||
if rating_template:
|
||||
issue.rating_send_request(rating_template, reuse_rating=False)
|
||||
|
||||
@api.multi
|
||||
def rating_apply(self, rate, token=None, feedback=None, subtype=None):
|
||||
return super(ProjectIssue, self).rating_apply(rate, token=token, feedback=feedback, subtype="rating_project_issue.mt_issue_rating")
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
|
||||
_inherit = "project.project"
|
||||
|
||||
@api.multi
|
||||
def _send_rating_mail(self):
|
||||
super(Project, self)._send_rating_mail()
|
||||
for project in self:
|
||||
project.issue_ids._send_issue_rating_mail()
|
||||
|
||||
@api.depends('percentage_satisfaction_task', 'percentage_satisfaction_issue')
|
||||
def _compute_percentage_satisfaction_project(self):
|
||||
super(Project, self)._compute_percentage_satisfaction_project()
|
||||
Rating = self.env['rating.rating']
|
||||
Issue = self.env['project.issue']
|
||||
for record in self.filtered(lambda record: record.use_tasks or record.use_issues):
|
||||
if record.use_tasks or record.use_issues:
|
||||
# built the domain according the project parameters (use tasks and/or issues)
|
||||
res_models = []
|
||||
domain = []
|
||||
if record.use_tasks:
|
||||
res_models.append('project.task')
|
||||
domain += ['&', ('res_model', '=', 'project.task'), ('res_id', 'in', record.tasks.ids)]
|
||||
if record.use_issues:
|
||||
# TODO: if performance issue, compute the satisfaction with a custom request joining rating and task/issue.
|
||||
issues = Issue.search([('project_id', '=', record.id)])
|
||||
res_models.append('project.issue')
|
||||
domain += ['&', ('res_model', '=', 'project.issue'), ('res_id', 'in', issues.ids)]
|
||||
if len(res_models) == 2:
|
||||
domain = ['|'] + domain
|
||||
domain = ['&', ('rating', '>=', 0)] + domain
|
||||
# get the number of rated tasks and issues with a read_group (more perfomant !)
|
||||
grouped_data = Rating.read_group(domain, ['res_model'], ['res_model'])
|
||||
# compute the number of each model and total number
|
||||
res = dict.fromkeys(res_models, 0)
|
||||
for data in grouped_data:
|
||||
res[data['res_model']] += data['res_model_count']
|
||||
nbr_rated_task = res.get('project.task', 0)
|
||||
nbr_rated_issue = res.get('project.issue', 0)
|
||||
nbr_project_rating = nbr_rated_issue + nbr_rated_task
|
||||
# compute the weighted arithmetic average
|
||||
ratio_task = float(nbr_rated_task) / float(nbr_project_rating) if nbr_project_rating else 0
|
||||
ratio_issue = float(nbr_rated_issue) / float(nbr_project_rating) if nbr_project_rating else 0
|
||||
record.percentage_satisfaction_project = round((ratio_task*record.percentage_satisfaction_task)+(ratio_issue*record.percentage_satisfaction_issue)) if nbr_project_rating else -1
|
||||
else:
|
||||
record.percentage_satisfaction_project = -1
|
||||
for project in self:
|
||||
domain = [('create_date', '>=', fields.Datetime.to_string(fields.datetime.now() - timedelta(days=30)))]
|
||||
activity_great, activity_sum = 0, 0
|
||||
if project.use_tasks:
|
||||
activity_task = project.tasks.rating_get_grades(domain)
|
||||
activity_great = activity_task['great']
|
||||
activity_sum = sum(activity_task.values())
|
||||
if project.use_issues:
|
||||
activity_issue = project.issue_ids.rating_get_grades(domain)
|
||||
activity_great += activity_issue['great']
|
||||
activity_sum += sum(activity_issue.values())
|
||||
project.percentage_satisfaction_project = activity_great * 100 / activity_sum if activity_sum else -1
|
||||
|
||||
@api.one
|
||||
@api.depends('issue_ids.rating_ids.rating')
|
||||
@@ -80,7 +69,7 @@ class Project(models.Model):
|
||||
@api.multi
|
||||
def action_view_all_rating(self):
|
||||
action = super(Project, self).action_view_all_rating()
|
||||
task_domain = action['domain'][1:] # remove the (rating != -1) condition
|
||||
task_domain = action['domain']
|
||||
domain = []
|
||||
if self.use_tasks: # add task domain, if neeeded
|
||||
domain = ['&'] + task_domain
|
||||
@@ -89,26 +78,4 @@ class Project(models.Model):
|
||||
domain = domain + ['&', ('res_id', 'in', issues.ids), ('res_model', '=', 'project.issue')]
|
||||
if self.use_tasks and self.use_issues:
|
||||
domain = ['|'] + domain
|
||||
domain = [('rating', '!=', -1)] + domain # prepend the condition to avoid empty rating
|
||||
return dict(action, domain=domain)
|
||||
|
||||
|
||||
|
||||
class Rating(models.Model):
|
||||
|
||||
_inherit = "rating.rating"
|
||||
|
||||
@api.model
|
||||
def apply_rating(self, rate, res_model=None, res_id=None, token=None):
|
||||
""" check if the auto_validation_kanban_state is activated. If so, apply the modification of the
|
||||
kanban state according to the given rating.
|
||||
"""
|
||||
rating = super(Rating, self).apply_rating(rate, res_model, res_id, token)
|
||||
if rating.res_model == 'project.issue':
|
||||
issue = self.env[rating.res_model].sudo().browse(rating.res_id)
|
||||
if issue.stage_id.auto_validation_kanban_state:
|
||||
if rating.rating > 5:
|
||||
issue.write({'kanban_state' : 'done'})
|
||||
else:
|
||||
issue.write({'kanban_state' : 'blocked'})
|
||||
return rating
|
||||
|
||||
@@ -1,6 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="rating_rating_action_issue" model="ir.actions.act_window">
|
||||
<field name="name">Customer Ratings</field>
|
||||
<field name="res_model">rating.rating</field>
|
||||
<field name="view_mode">kanban,pivot,graph</field>
|
||||
<field name="domain">[('res_model','=','project.issue'),('res_id','=',active_id), ('consumed', '=', True)]</field>
|
||||
<field name="help" type="html">
|
||||
<p>
|
||||
Customer ratings on issues. If you have no rating, change your project Settings to activate it.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_issue_view_form_inherit_rating" model="ir.ui.view">
|
||||
<field name="name">project.issue.view.form.inherit.rating</field>
|
||||
<field name="model">project.issue</field>
|
||||
<field name="inherit_id" ref="project_issue.project_issue_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="%(rating_rating_action_issue)d" type="action"
|
||||
class="oe_stat_button" icon="fa-smile-o">
|
||||
<field name="rating_count" string="Rating" widget="statinfo"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_issue_view_kanban_inherit_rating" model="ir.ui.view">
|
||||
<field name="name">project.issue.view.kanban.inherit.rating</field>
|
||||
<field name="model">project.issue</field>
|
||||
<field name="inherit_id" ref="project_issue.project_issue_kanban_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='color']" position="after">
|
||||
<field name="rating_last_value"/>
|
||||
<field name="rating_ids"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@class='oe_kanban_bottom_left']" position="inside">
|
||||
<b t-if="record.rating_ids.raw_value.length">
|
||||
<span class="fa fa-fw mt4 fa-thumbs-up text-success" t-if="record.rating_last_value.value == 10" title="Latest Rating: Happy"/>
|
||||
<span class="fa fa-fw mt4 fa-thumbs-down text-warning" t-if="record.rating_last_value.value == 5" title="Latest Rating: Average"/>
|
||||
<span class="fa fa-fw mt4 fa-thumbs-down text-danger" t-if="record.rating_last_value.value == 1" title="Latest Rating: Not Happy"/>
|
||||
</b>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- inherit get state button on project -->
|
||||
<record id="project_issue_view_rating" model="ir.ui.view">
|
||||
<field name="name">project.issue.view.rating</field>
|
||||
@@ -10,11 +56,11 @@
|
||||
<field name="arch" type="xml">
|
||||
<div name="button_box" position="inside">
|
||||
<button name="action_view_issue_rating"
|
||||
attrs="{'invisible': ['|', '|', ('use_issues','=', False), ('is_visible_happy_customer','=', False), ('percentage_satisfaction_issue', '=', -1)]}"
|
||||
attrs="{'invisible': ['|', '|', ('use_issues','=', False), ('rating_status', '=', False), ('percentage_satisfaction_issue', '=', -1)]}"
|
||||
class="oe_stat_button oe_percent"
|
||||
type="object"
|
||||
icon="fa-smile-o">
|
||||
<field string="On issues" name="percentage_satisfaction_issue" widget="statinfo"/>
|
||||
<field string="% On issues" name="percentage_satisfaction_issue" widget="statinfo"/>
|
||||
</button>
|
||||
</div>
|
||||
</field>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import models
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Sale Service Rating',
|
||||
'category': 'Hidden',
|
||||
'description': """
|
||||
This module allows a customer to give rating on task which are created from sale order.
|
||||
""",
|
||||
'depends': [
|
||||
'rating_project',
|
||||
'sale_timesheet'
|
||||
],
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import project
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class Task(models.Model):
|
||||
_inherit = 'project.task'
|
||||
|
||||
def rating_get_partner_id(self):
|
||||
if self.partner_id or self.self.order_id.partner_id:
|
||||
return self.partner_id or self.sale_line_id.order_id.partner_id
|
||||
return super(Task, self).rating_get_partner_id()
|
||||
@@ -88,7 +88,7 @@
|
||||
<div class="col-md-3">
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-body text-center bg-danger">
|
||||
<img src="/rating/static/src/img/rating_0.png" style="height:40px"/>
|
||||
<img src="/rating/static/src/img/rating_1.png" style="height:40px"/>
|
||||
</div>
|
||||
<div class="panel-body text-center bg-danger">
|
||||
<h2 style="margin: 0">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from openerp import _
|
||||
from openerp.addons.web import http
|
||||
from openerp.addons.web.http import request
|
||||
|
||||
@@ -11,8 +10,8 @@ class WebsiteRatingProject(http.Controller):
|
||||
|
||||
@http.route(['/project/rating/'], type='http', auth="public", website=True)
|
||||
def index(self, **kw):
|
||||
projects = request.env['project.project'].sudo().search([('is_visible_happy_customer', '=', True), ('website_published', '=', True)])
|
||||
values = {'projects' : projects}
|
||||
projects = request.env['project.project'].sudo().search([('rating_status', '!=', False), ('website_published', '=', True)])
|
||||
values = {'projects': projects}
|
||||
return request.website.render('website_rating_project_issue.index', values)
|
||||
|
||||
@http.route(['/project/rating/<int:project_id>'], type='http', auth="public", website=True)
|
||||
@@ -21,25 +20,29 @@ class WebsiteRatingProject(http.Controller):
|
||||
project = request.env['project.project'].sudo().browse(project_id)
|
||||
# to avoid giving any access rights on projects to the public user, let's use sudo
|
||||
# and check if the user should be able to view the project (project managers only if it's unpublished or has no rating)
|
||||
if not (project.is_visible_happy_customer and project.website_published) and not user.sudo(user).has_group('project.group_project_manager'):
|
||||
if not (project.rating_status and project.website_published) and not user.sudo(user).has_group('project.group_project_manager'):
|
||||
raise NotFound()
|
||||
issues = request.env['project.issue'].sudo().search([('project_id', '=', project.id)])
|
||||
domain = [('res_model', '=', 'project.issue'), ('res_id', 'in', issues.ids)]
|
||||
ratings = request.env['rating.rating'].search(domain, order="id desc", limit=100)
|
||||
|
||||
yesterday = (datetime.date.today()-datetime.timedelta(days=-1)).strftime('%Y-%m-%d 23:59:59')
|
||||
stats = {}
|
||||
for x in (7, 30, 90):
|
||||
todate = (datetime.date.today()-datetime.timedelta(days=x)).strftime('%Y-%m-%d 00:00:00')
|
||||
domdate = domain + [('create_date', '<=', yesterday), ('create_date', '>=', todate)]
|
||||
stats[x] = {0: 0, 5: 0, 10: 0}
|
||||
rating_stats = request.env['rating.rating'].read_group(domdate, [], ['rating'])
|
||||
total = reduce(lambda x, y: y['rating_count']+x, rating_stats, 0)
|
||||
for rate in rating_stats:
|
||||
stats[x][rate['rating']] = (rate['rating_count'] * 100) / total
|
||||
values = {
|
||||
'project': project,
|
||||
'ratings': ratings,
|
||||
'stats': stats,
|
||||
'task_data': self._calculate_rating(project.id, "project.task"),
|
||||
'issue_data': self._calculate_rating(project.id, "project.issue"),
|
||||
}
|
||||
return request.website.render('website_rating_project_issue.project_rating_page', values)
|
||||
|
||||
def _calculate_rating(self, project_id, model_name):
|
||||
# Calculate rating for Tasks and Issues
|
||||
records = request.env[model_name].sudo().search([('project_id', '=', project_id)])
|
||||
domain = [('res_model', '=', model_name), ('res_id', 'in', records.ids), ('consumed', '=', True)]
|
||||
ratings = request.env['rating.rating'].search(domain, order="id desc", limit=100)
|
||||
|
||||
yesterday = (datetime.date.today() - datetime.timedelta(days=-1)).strftime('%Y-%m-%d 23:59:59')
|
||||
stats = {}
|
||||
for x in (7, 30, 90):
|
||||
todate = (datetime.date.today() - datetime.timedelta(days=x)).strftime('%Y-%m-%d 00:00:00')
|
||||
domdate = domain + [('create_date', '<=', yesterday), ('create_date', '>=', todate)]
|
||||
stats[x] = {1: 0, 5: 0, 10: 0}
|
||||
rating_stats = request.env['rating.rating'].read_group(domdate, [], ['rating'])
|
||||
total = reduce(lambda x, y: y['rating_count'] + x, rating_stats, 0)
|
||||
for rate in rating_stats:
|
||||
stats[x][rate['rating']] = float("%.2f" % (rate['rating_count'] * 100.0 / total))
|
||||
return {'ratings': ratings, 'stats': stats}
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
.oe_website_rating_project .oe_rate{
|
||||
clear: both;
|
||||
}
|
||||
.oe_website_rating_project .oe_rate img {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.oe_website_rating_project .thumbnail{
|
||||
height: 355px;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr='//div[@name="button_box"]/*[1]' position='before'>
|
||||
<button class="oe_stat_button" name="website_publish_button"
|
||||
type="object" icon="fa-globe" attrs="{'invisible': [('is_visible_happy_customer', '=', False)]}">
|
||||
type="object" icon="fa-globe" attrs="{'invisible': [('rating_status', '=', False)]}">
|
||||
<field name="website_published" widget="website_button"/>
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
@@ -71,30 +71,48 @@
|
||||
|
||||
<!-- Page : Rating of a particular project -->
|
||||
<template id="project_rating_data" name="Project Issue Rating Page">
|
||||
<div class="oe_rate">
|
||||
<img src='/rating/static/src/img/rating_10.png' alt="Happy Feedback" class="pull-left" style="width:24px"/>
|
||||
<div class="progress mt4">
|
||||
<div t-attf-class="progress-bar progress-bar-success" role="progressbar" t-attf-style="width: #{stats[duration][10]}%;">
|
||||
<t t-esc="stats[duration][10]"/>%%
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" t-attf-style="width: #{stats[duration][10]}%;" title="Happy">
|
||||
<t t-esc="int(stats[duration][10])"/>%
|
||||
</div>
|
||||
</div><div class="oe_rate mt16">
|
||||
<img src='/rating/static/src/img/rating_5.png' alt="Normal Feedback" class="pull-left" style="width:24px"/>
|
||||
<div class="progress mt4">
|
||||
<div t-attf-class="progress-bar progress-bar-warning" role="progressbar" t-attf-style="width: #{stats[duration][5]}%;">
|
||||
<t t-esc="stats[duration][5]"/>%%
|
||||
</div>
|
||||
<div class="progress-bar progress-bar-warning" t-attf-style="width: #{stats[duration][5]}%;" title="Average">
|
||||
<t t-esc="int(stats[duration][5])"/>%
|
||||
</div>
|
||||
</div><div class="oe_rate mt16">
|
||||
<img src='/rating/static/src/img/rating_0.png' alt="Bad Feedback" class="pull-left" style="width:24px"/>
|
||||
<div class="progress mt4">
|
||||
<div t-attf-class="progress-bar progress-bar-danger" role="progressbar" t-attf-style="width: #{stats[duration][0]}%;">
|
||||
<t t-esc="stats[duration][0]"/>%%
|
||||
</div>
|
||||
<div class="progress-bar progress-bar-danger" t-attf-style="width: #{stats[duration][1]}%;" title="Bad">
|
||||
<t t-esc="int(stats[duration][1])"/>%
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="project_data_satisfaction">
|
||||
<h3 class="page-header">Customer Satisfaction of <t t-esc="type"/></h3>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<h5>Last 7 days</h5>
|
||||
<t t-set="duration" t-value="7"/>
|
||||
<t t-set="stats" t-value="data['stats']"/>
|
||||
<t t-call="website_rating_project_issue.project_rating_data"/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<h5>Last 30 days</h5>
|
||||
<t t-set="duration" t-value="30"/>
|
||||
<t t-set="stats" t-value="data['stats']"/>
|
||||
<t t-call="website_rating_project_issue.project_rating_data"/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<h5>Last 3 months</h5>
|
||||
<t t-set="duration" t-value="90"/>
|
||||
<t t-set="stats" t-value="data['stats']"/>
|
||||
<t t-call="website_rating_project_issue.project_rating_data"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<h5 class="page-header">Latest Feedbacks</h5>
|
||||
<t t-foreach="data['ratings']" t-as="rating" class="media">
|
||||
<img t-attf-src='/rating/static/src/img/rating_#{int(rating.rating)}.png' t-att-alt="rating.res_name" t-att-title="rating.res_name"/>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="project_rating_page" name="Project Issue Rating Page">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
@@ -110,29 +128,12 @@
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<h3 class="page-header">Customer Satisfaction</h3>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<h4>Last 7 days</h4>
|
||||
<t t-set="duration" t-value="7"/>
|
||||
<t t-call="website_rating_project_issue.project_rating_data"/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<h4>Last 30 days</h4>
|
||||
<t t-set="duration" t-value="30"/>
|
||||
<t t-call="website_rating_project_issue.project_rating_data"/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<h4>Last 3 months</h4>
|
||||
<t t-set="duration" t-value="90"/>
|
||||
<t t-call="website_rating_project_issue.project_rating_data"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<h3 class="page-header">Latest Feedbacks</h3>
|
||||
<t t-foreach="ratings" t-as="rating" class="media">
|
||||
<img t-attf-src='/rating/static/src/img/rating_#{int(rating.rating)}.png' t-att-alt="rating.res_name" t-att-title="rating.res_name"/>
|
||||
</t>
|
||||
<t t-set="type" t-value="'Tasks'"/>
|
||||
<t t-set="data" t-value="task_data"/>
|
||||
<t t-call="website_rating_project_issue.project_data_satisfaction"/>
|
||||
<t t-set="type" t-value="'Issues'"/>
|
||||
<t t-set="data" t-value="issue_data"/>
|
||||
<t t-call="website_rating_project_issue.project_data_satisfaction"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user