[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:
jat-odoo
2016-07-01 14:05:03 +02:00
committed by Yannick Tivisse
parent 5560ac4a04
commit 0b1a27a3c7
31 changed files with 658 additions and 368 deletions
+1 -3
View File
@@ -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 &amp; Support">
<field name="module_rating_project" widget="radio"/>
</group>
<group name="timesheets"/>
</form>
</field>
</record>
+1
View File
@@ -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")
+4 -4
View File
@@ -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 &amp; Support">
<field name="module_rating_project_issue" widget="radio"/>
</group>
</xpath>
</field>
</record>
</odoo>
+20 -12
View File
@@ -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()
+87 -84
View File
@@ -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

+35 -7
View File
@@ -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>
+18 -20
View File
@@ -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&amp;view_type=form&amp;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 } &lt;br&gt;
#{ record.partner_id.value ? 'Customer : ' + record.partner_id.value + '&lt;br&gt;' : '' }
#{ record.rated_partner_id.value ? 'Responsible : ' + record.rated_partner_id.value+'&lt;br&gt;' : '' }
Date : #{ moment(record.create_date.raw_value).format('L') }
#{ record.feedback.raw_value ? '&lt;br&gt;'+'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', '&gt;=', 7)]"/>
<filter string="Okay" domain="[('rating', '&gt;', 3), ('rating', '&lt;', 7)]"/>
<filter string="Unhappy" domain="[('rating', '&gt;=', 0), ('rating', '&lt;=',3)]"/>
<filter string="Unhappy" domain="[('rating', '&gt;=', 1), ('rating', '&lt;=',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.
+98 -40
View File
@@ -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>
+19 -6
View File
@@ -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>
+54 -38
View File
@@ -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 &gt;= 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 &gt;= 50 ? 'label-success' : 'label-warning'}">
<i t-attf-class="fa #{record.percentage_satisfaction_project.raw_value &gt;= 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>
+68 -3
View File
@@ -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>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import models
+15
View File
@@ -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>