summaryrefslogtreecommitdiff
path: root/addons/website_project
diff options
context:
space:
mode:
authorBenjamin Frantzen (bfr) <bfr@odoo.com>2019-12-18 13:45:38 +0000
committerYannick Tivisse <yti@odoo.com>2020-05-08 12:17:20 +0000
commite4a4ffb974ccde1842e1b2389450718f8576ad5b (patch)
tree6159873058286c65a3a3bfb5a48f2ef5ffbf7565 /addons/website_project
parentf427a38c4e0dcd63d086706ad66a3e5570fbf0aa (diff)
[IMP] customer ratings flow improvements
- Rename the 'Use Rating on Project' feature into 'Customer Ratings' - Rename the 'Set Email Template to Stages' link to 'Set a Rating Email Template on Stages' - Add an optional list view for the Stages menu - display warning if the rating_template_id field is set and if one of the selected project_ids doesn't have the rating_status field set to true - Project form view revamp - rename the '% on tasks' stat button into 'Customer Satisfaction' - Remove the 'no option' for the rating frequency field because it is required - project form : Add a 'Go to Website' stat button - Project dashboard: remove the 'Customer Ratings' menu item in more - Ratings page: the 'Last 30 days' filter include ratings from today - remove the Appointment / Helpdesk Customer Satisfaction / Live Support menu items TASK ID : 1251
Diffstat (limited to 'addons/website_project')
-rw-r--r--addons/website_project/__init__.py5
-rw-r--r--addons/website_project/__manifest__.py18
-rw-r--r--addons/website_project/controllers/__init__.py4
-rw-r--r--addons/website_project/controllers/rating.py98
-rw-r--r--addons/website_project/models/__init__.py4
-rw-r--r--addons/website_project/models/project.py23
-rw-r--r--addons/website_project/views/project_project_views.xml16
-rw-r--r--addons/website_project/views/project_rating_templates.xml212
8 files changed, 380 insertions, 0 deletions
diff --git a/addons/website_project/__init__.py b/addons/website_project/__init__.py
new file mode 100644
index 00000000000..7d34c7c054a
--- /dev/null
+++ b/addons/website_project/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import controllers
+from . import models
diff --git a/addons/website_project/__manifest__.py b/addons/website_project/__manifest__.py
new file mode 100644
index 00000000000..cb467db65e6
--- /dev/null
+++ b/addons/website_project/__manifest__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': 'Website Project',
+ 'category': 'Website/Website',
+ 'summary': 'Publish your project ratings',
+ 'version': '1.0',
+ 'description': """
+Allows to publish all ratings for a particular project.""",
+ 'depends': ['website', 'project'],
+ 'data': [
+ 'views/project_rating_templates.xml',
+ 'views/project_project_views.xml',
+ ],
+ 'installable': True,
+ 'auto_install': True,
+}
diff --git a/addons/website_project/controllers/__init__.py b/addons/website_project/controllers/__init__.py
new file mode 100644
index 00000000000..7367c6c8192
--- /dev/null
+++ b/addons/website_project/controllers/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import rating
diff --git a/addons/website_project/controllers/rating.py b/addons/website_project/controllers/rating.py
new file mode 100644
index 00000000000..7e0c3d5dda2
--- /dev/null
+++ b/addons/website_project/controllers/rating.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from werkzeug.exceptions import NotFound
+
+from odoo import http
+from odoo.http import request
+
+
+class RatingProject(http.Controller):
+
+ @http.route(['/project/rating'], type='http', auth="public", website=True, sitemap=True)
+ def index(self, **kw):
+ projects = request.env['project.project'].sudo().search([
+ ('rating_active', '=', True),
+ ('is_published', '=', True)])
+ values = {'projects': projects}
+ return request.render('website_project.rating_index', values)
+
+ def _calculate_period_partner_stats(self, project_id):
+ # get raw data: number of rating by rated partner, by rating value, by period
+ request.env.cr.execute("""
+ SELECT
+ rated_partner_id,
+ rating,
+ COUNT(rating) as rating_count,
+ CASE
+ WHEN now()::date - write_date::date BETWEEN 0 AND 6 Then 'days_06'
+ WHEN now()::date - write_date::date BETWEEN 0 AND 15 Then 'days_15'
+ WHEN now()::date - write_date::date BETWEEN 0 AND 30 Then 'days_30'
+ WHEN now()::date - write_date::date BETWEEN 0 AND 90 Then 'days_90'
+ END AS period
+ FROM
+ rating_rating
+ WHERE
+ parent_res_model = 'project.project'
+ AND parent_res_id = %s
+ AND res_model = 'project.task'
+ AND rated_partner_id IS NOT NULL
+ AND write_date >= current_date - interval '90' day
+ AND rating IN (1, 3, 5)
+ GROUP BY
+ rated_partner_id, rating, period
+ """, (project_id, ))
+
+ raw_data = request.env.cr.dictfetchall()
+
+ # periodical statistics
+ default_period_dict = {'rating_5': 0, 'rating_3': 0, 'rating_1': 0, 'total': 0}
+ period_statistics = {
+ 'days_06': dict(default_period_dict),
+ 'days_15': dict(default_period_dict),
+ 'days_30': dict(default_period_dict),
+ 'days_90': dict(default_period_dict),
+ }
+ for period_statistics_key in period_statistics.keys():
+ for row in raw_data:
+ if row['period'] <= period_statistics_key:
+ period_statistics[period_statistics_key]['rating_%s' % (int(row['rating']),)] += row['rating_count']
+ period_statistics[period_statistics_key]['total'] += row['rating_count']
+
+ # partner statistics
+ default_partner_dict = {'rating_5': 0, 'rating_3': 0, 'rating_1': 0, 'total': 0, 'rated_partner': None, 'percentage_happy': 0.0}
+ partner_statistics = {}
+ for row in raw_data:
+ if row['period'] <= 'days_15':
+ if row['rated_partner_id'] not in partner_statistics:
+ partner_statistics[row['rated_partner_id']] = dict(default_partner_dict)
+ partner_statistics[row['rated_partner_id']]['rated_partner'] = request.env['res.partner'].sudo().browse(row['rated_partner_id'])
+ partner_statistics[row['rated_partner_id']]['rating_%s' % (int(row['rating']),)] += row['rating_count']
+ partner_statistics[row['rated_partner_id']]['total'] += row['rating_count']
+
+ for partner_id, stat_values in partner_statistics.items():
+ stat_values['percentage_happy'] = (stat_values['rating_5'] / float(stat_values['total'])) * 100 if stat_values['total'] else 0.0
+
+ return {
+ 'partner_statistics': partner_statistics,
+ 'period_statistics': period_statistics,
+ }
+
+ @http.route(['/project/rating/<int:project_id>'], type='http', auth="public", website=True)
+ def page(self, project_id=None, **kw):
+ user = request.env.user
+ project = request.env['project.project'].sudo().browse(project_id)
+ # to avoid giving any access rights on projects to the public user, let's use sudo
+ # and check if the user should be able to view the project (project managers only if it's unpublished or has no rating)
+ if not (project.rating_active and project.is_published) and not user.with_user(user).has_group('project.group_project_manager'):
+ raise NotFound()
+
+ return request.render('website_project.rating_project_rating_page', {
+ 'project': project,
+ 'ratings': request.env['rating.rating'].sudo().search([
+ ('consumed', '=', True),
+ ('parent_res_model', '=', 'project.project'),
+ ('parent_res_id', '=', project_id)
+ ], order='write_date DESC', limit=50),
+ 'statistics': self._calculate_period_partner_stats(project_id),
+ })
diff --git a/addons/website_project/models/__init__.py b/addons/website_project/models/__init__.py
new file mode 100644
index 00000000000..527e2954c06
--- /dev/null
+++ b/addons/website_project/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import project
diff --git a/addons/website_project/models/project.py b/addons/website_project/models/project.py
new file mode 100644
index 00000000000..ded92b699a2
--- /dev/null
+++ b/addons/website_project/models/project.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models
+
+
+class Project(models.Model):
+ _name = 'project.project'
+ _inherit = ['project.project', 'website.published.mixin']
+
+ def action_view_all_rating(self):
+ if self.website_published:
+ return self.open_website_url()
+ return super(Project, self).action_view_all_rating()
+
+ def open_website_url(self):
+ """ return the action to see all the rating of the project, and activate default filters """
+ return {
+ 'type': 'ir.actions.act_url',
+ 'name': "Redirect to the Website Project Rating Page",
+ 'target': 'self',
+ 'url': "/project/rating/%s" % (self.id)
+ }
diff --git a/addons/website_project/views/project_project_views.xml b/addons/website_project/views/project_project_views.xml
new file mode 100644
index 00000000000..354adc30d5b
--- /dev/null
+++ b/addons/website_project/views/project_project_views.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <record id="project_project_view_form" model="ir.ui.view">
+ <field name="name">project.project.form</field>
+ <field name="model">project.project</field>
+ <field name="inherit_id" ref="project.edit_project"/>
+ <field name="arch" type="xml">
+ <xpath expr="//div[@name='button_box']" position="inside">
+ <field name="is_published"
+ widget="website_redirect_button"
+ groups="project.group_project_rating"
+ attrs="{'invisible': [('rating_active', '=', False)]}"/>
+ </xpath>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/website_project/views/project_rating_templates.xml b/addons/website_project/views/project_rating_templates.xml
new file mode 100644
index 00000000000..197bf781b30
--- /dev/null
+++ b/addons/website_project/views/project_rating_templates.xml
@@ -0,0 +1,212 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <!-- Page : List of project -->
+ <template id="rating_index" name="Project Rating List">
+ <t t-call="portal.frontend_layout">
+ <t t-set="additional_title">Project Satisfaction</t>
+ <div id="wrap" class="row mt4">
+ <div class="oe_structure" id="oe_structure_project_rating_index_1"/>
+ <div class="container o_portal_project_rating">
+ <h3>Projects</h3>
+ <div class="oe_structure" id="oe_structure_project_rating_index_2"/>
+ <div class="row">
+ <t t-if="not projects">
+ <div class="mt64 text-center">
+ No project rating published for now.
+ </div>
+ </t>
+ <t t-foreach="projects" t-as="project">
+ <div t-if="project.rating_percentage_satisfaction != -1" class="col-md-6 col-4 col-lg-4 col-xl-4">
+ <div class="card">
+ <div class="card-body">
+ <div class="caption">
+ <span class="badge badge-secondary float-right"><t t-esc="project.privacy_visibility"/></span>
+ <h4><t t-esc="project.name"/></h4>
+ <p t-if="project.date" class="text-muted">
+ <i class="fa fa-calendar"/> End date : <t t-esc="project.date"/>
+ </p>
+ <p t-if="project.alias_name and project.alias_domain" class="text-muted">
+ <i class="fa fa-envelope"/> Email : <t t-esc="project.alias_name"/>@<t t-esc="project.alias_domain"/>
+ </p>
+ <div class="row">
+ <div class="col-lg-6 text-center">
+ <h2><t t-esc="len(project.task_ids)"/></h2>
+ <p><t t-esc="project.label_tasks"/></p>
+ </div>
+ </div>
+ </div>
+ <div class="caption">
+ <p>
+ <a role="button" t-att-href="'/project/rating/%s' % project.id" class="btn btn-primary btn-lg btn-block">
+ <i class="fa fa-arrow-circle-right "/> See the feedbacks
+ </a>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </t>
+ </div>
+ </div>
+ <div class="oe_structure" id="oe_structure_project_rating_index_3"/>
+ </div>
+ </t>
+ </template>
+
+ <!-- Page : Project Rating Page (and widgets) -->
+ <template id="portal_project_rating_progressbar">
+ <div class="progress o_rating_progress">
+ <t t-set="percent_rating_5" t-value="int(round((stats['rating_5'] / float(stats['total'])) * 100)) if stats['total'] else 0.0"/>
+ <div class="progress-bar bg-success" t-attf-style="width: #{percent_rating_5}%;" title="Happy">
+ <t t-esc="percent_rating_5"/>%
+ </div>
+ <t t-set="percent_rating_3" t-value="int(round((stats['rating_3'] / float(stats['total'])) * 100)) if stats['total'] else 0.0"/>
+ <div class="progress-bar bg-warning" t-attf-style="width: #{percent_rating_3}%;" title="Average">
+ <t t-esc="percent_rating_3"/>%
+ </div>
+ <t t-set="percent_rating_1" t-value="int(round((stats['rating_1'] / float(stats['total'])) * 100)) if stats['total'] else 0.0"/>
+ <div class="progress-bar bg-danger" t-attf-style="width: #{percent_rating_1}%;" title="Bad">
+ <t t-esc="percent_rating_1"/>%
+ </div>
+ </div>
+ </template>
+
+ <template id="portal_project_rating_partner_stat">
+ <t t-foreach="partner_stats.values()" t-as="partner_stat">
+ <div class="row mt8" t-if="partner_stat['total']" title="Top 5 partner ratings of last 15 days.">
+ <div class="col-md-1">
+ <img t-if="partner_stat['rated_partner'].image_1024" class="o_top_partner_image" t-att-src="image_data_uri(partner_stat['rated_partner'].image_1024)" alt="Rated partner"/>
+ <img t-if="not partner_stat['rated_partner'].image_1024" class="o_top_partner_image" src='/web/static/src/img/placeholder.png' alt="Placeholder"/>
+ </div>
+ <div class="col-md-4 o_smiley_no_padding_right">
+ <span class="ml8" t-esc="partner_stat['rated_partner'].name"/>
+ </div>
+ <div class="col-md-4 o_smiley_no_padding_right">
+ <div class="col-md-4 o_smiley_no_padding_right o_smiley_no_padding_left">
+ <span t-if="partner_stat['rating_5']">
+ <img title="happy" alt="Happy face" class="o_top_partner_rating_image" src='/rating/static/src/img/rating_5.png'/>
+ <span class="o_rating_count" t-esc="partner_stat['rating_5']"/>
+ </span>
+ <span t-if="not partner_stat['rating_5']">
+ <img title="happy" alt="Happy face" class="o_top_partner_rating_image o_lighter_smileys" src='/rating/static/src/img/rating_5.png'/>
+ <span class="o_rating_count o_lighter_smileys" t-esc="partner_stat['rating_5']"/>
+ </span>
+ </div>
+ <div class="col-md-4 o_smiley_no_padding_right o_smiley_no_padding_left">
+ <span t-if="partner_stat['rating_3']">
+ <img title="average" alt="Neutral face" class="o_top_partner_rating_image" src='/rating/static/src/img/rating_3.png'/>
+ <span class="o_rating_count" t-esc="partner_stat['rating_3']"/>
+ </span>
+ <span t-if="not partner_stat['rating_3']">
+ <img title="average" alt="Neutral face" class="o_top_partner_rating_image o_lighter_smileys" src='/rating/static/src/img/rating_3.png'/>
+ <span class="o_rating_count o_lighter_smileys" t-esc="partner_stat['rating_3']"/>
+ </span>
+ </div>
+ <div class="col-md-4 o_smiley_no_padding_right o_smiley_no_padding_left">
+ <span t-if="partner_stat['rating_1']">
+ <img title="bad" alt="Sad face" class="o_top_partner_rating_image" src='/rating/static/src/img/rating_1.png'/>
+ <span class="o_rating_count" t-esc="partner_stat['rating_1']"/>
+ </span>
+ <span t-if="not partner_stat['rating_1']">
+ <img title="bad" alt="Sad face" class="o_top_partner_rating_image o_lighter_smileys" src='/rating/static/src/img/rating_1.png'/>
+ <span class="o_rating_count o_lighter_smileys" t-esc="partner_stat['rating_1']"/>
+ </span>
+ </div>
+ </div>
+ <div class="col-md-3 o_smiley_no_padding_left">
+ <strong class="float-right"><t t-esc="int(round(partner_stat['percentage_happy']))"/>%</strong>
+ </div>
+ </div>
+ </t>
+ </template>
+
+ <template id="portal_project_rating_popover">
+ <div t-attf-id="rating_#{rating.id}" class="container d-none">
+ <div class="row">
+ <div class="col-lg-4">
+ <img t-if="rating.partner_id.image_1024" class="img-fluid" t-att-src="image_data_uri(rating.partner_id.image_1024)" alt="Rated partner"/>
+ <img t-if="not rating.partner_id.image_1024" class="img-fluid" src='/web/static/src/img/placeholder.png' alt="Placeholder"/>
+ </div>
+ <div class="col-lg-8">
+ <div class="mt4">
+ <strong><t t-esc="rating.rated_partner_id.name"/></strong> - <span class="rating_timeduration"><t t-esc="rating.create_date"/></span>
+ </div>
+ <strong>Rated by: </strong><t t-esc="rating.partner_id.name" />
+ </div>
+ </div>
+ <div class="row o_top_partner_feedback" t-if="rating.feedback">
+ <div class="col-lg-12">
+ <t t-esc="rating.feedback" />
+ </div>
+ </div>
+ </div>
+ </template>
+
+ <template id="rating_project_rating_page" name="Project Rating Page">
+ <t t-call="portal.frontend_layout">
+ <div id="wrap" class="row mt4">
+ <div class="oe_structure" id="oe_structure_project_rating_page_1"/>
+ <div class="container o_project o_portal_project_rating">
+ <br/>
+ <h2 t-esc="project.name"/>
+ <br/>
+ <div class="row mb32">
+ <div class="col-lg-12">
+ <div class="o_page_header">
+ <div class="justify-content-end" groups="project.group_project_manager">
+ <t t-call="website.publish_management">
+ <t t-set="object" t-value="project"/>
+ <t t-set="publish_edit" t-value="True"/>
+ </t>
+ </div>
+ <h3>Customer Ratings</h3>
+ </div>
+ <div class="row">
+ <div class="col-lg-7">
+ <t t-foreach="ratings" t-as="rating">
+ <img t-attf-src='/rating/static/src/img/#{rating._get_rating_image_filename()}'
+ class="mt4 o_rating_image"
+ t-att-alt="rating.res_name"
+ t-att-data-id="rating.id"
+ t-att-data-rating-date="rating.write_date"/>
+ <t t-call="website_project.portal_project_rating_popover"/>
+ </t>
+ </div>
+ <div class="col-lg-5 o_vertical_separator">
+ <div t-if="statistics['period_statistics']['days_06']">
+ <h5>Last 7 days</h5>
+ <t t-call="website_project.portal_project_rating_progressbar">
+ <t t-set="stats" t-value="statistics['period_statistics']['days_06']"/>
+ </t>
+ </div>
+ <div t-if="statistics['period_statistics']['days_30']">
+ <h5>Last 30 days</h5>
+ <t t-call="website_project.portal_project_rating_progressbar">
+ <t t-set="stats" t-value="statistics['period_statistics']['days_30']"/>
+ </t>
+ </div>
+ <div t-if="statistics['period_statistics']['days_90']">
+ <h5>Last 3 months</h5>
+ <t t-call="website_project.portal_project_rating_progressbar">
+ <t t-set="stats" t-value="statistics['period_statistics']['days_90']"/>
+ </t>
+ </div>
+ <div>
+ <t t-call="website_project.portal_project_rating_partner_stat">
+ <t t-set="partner_stats" t-value="statistics['partner_statistics']"/>
+ </t>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="oe_structure" id="oe_structure_project_rating_page_2"/>
+ </div>
+ </t>
+ </template>
+
+ </data>
+</odoo>