diff options
Diffstat (limited to 'addons/project')
| -rw-r--r-- | addons/project/__manifest__.py | 3 | ||||
| -rw-r--r-- | addons/project/controllers/__init__.py | 1 | ||||
| -rw-r--r-- | addons/project/controllers/rating.py | 92 | ||||
| -rw-r--r-- | addons/project/models/project.py | 15 | ||||
| -rw-r--r-- | addons/project/static/src/js/portal_rating.js | 27 | ||||
| -rw-r--r-- | addons/project/static/src/less/portal_rating.less | 34 | ||||
| -rw-r--r-- | addons/project/views/project_assets.xml (renamed from addons/project/views/project_templates.xml) | 8 | ||||
| -rw-r--r-- | addons/project/views/project_rating_templates.xml | 203 | ||||
| -rw-r--r-- | addons/project/views/project_views.xml | 1 |
9 files changed, 380 insertions, 4 deletions
diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index 43b36689d88..e80837b5631 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -29,8 +29,9 @@ 'views/project_views.xml', 'views/res_partner_views.xml', 'views/res_config_settings_views.xml', - 'views/project_templates.xml', + 'views/project_assets.xml', 'views/project_portal_templates.xml', + 'views/project_rating_templates.xml', 'data/project_mail_template_data.xml', 'wizard/project_task_merge_wizard_views.xml', ], diff --git a/addons/project/controllers/__init__.py b/addons/project/controllers/__init__.py index 903b755e71e..10825353c81 100644 --- a/addons/project/controllers/__init__.py +++ b/addons/project/controllers/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import portal +from . import rating diff --git a/addons/project/controllers/rating.py b/addons/project/controllers/rating.py new file mode 100644 index 00000000000..3343674c0e8 --- /dev/null +++ b/addons/project/controllers/rating.py @@ -0,0 +1,92 @@ +# -*- 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) + def index(self, **kw): + projects = request.env['project.project'].sudo().search([('rating_status', '!=', 'no'), ('portal_show_rating', '=', True)]) + values = {'projects': projects} + return request.render('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 - create_date::date BETWEEN 0 AND 6 Then 'days_06' + WHEN now()::date - create_date::date BETWEEN 0 AND 15 Then 'days_15' + WHEN now()::date - create_date::date BETWEEN 0 AND 30 Then 'days_30' + WHEN now()::date - create_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 create_date >= current_date - interval '90' day + AND rating IN (1,5,10) + GROUP BY + rated_partner_id, rating, period + """ % (project_id)) + + raw_data = request.env.cr.dictfetchall() + + # periodical statistics + default_period_dict = {'rating_10': 0, 'rating_5': 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_10': 0, 'rating_5': 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_10'] / 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_status != 'no') and project.portal_show_rating) and not user.sudo(user).has_group('project.group_project_manager'): + raise NotFound() + + return request.render('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/project/models/project.py b/addons/project/models/project.py index c1e802839da..1df6bbad540 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -235,9 +235,11 @@ class Project(models.Model): "- 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.", default="no", required=True) rating_status_period = fields.Selection([ - ('daily', 'Daily'), ('weekly', 'Weekly'), ('bimonthly', 'Twice a Month'), - ('monthly', 'Once a Month'), ('quarterly', 'Quarterly'), ('yearly', 'Yearly') - ], 'Rating Frequency') + ('daily', 'Daily'), ('weekly', 'Weekly'), ('bimonthly', 'Twice a Month'), + ('monthly', 'Once a Month'), ('quarterly', 'Quarterly'), ('yearly', 'Yearly') + ], 'Rating Frequency') + + portal_show_rating = fields.Boolean('Rating visible publicly', copy=False, oldname='website_published') _sql_constraints = [ ('project_date_greater', 'check(date >= date_start)', 'Error! project start-date must be lower than project end-date.') @@ -405,6 +407,13 @@ class Project(models.Model): @api.multi def action_view_all_rating(self): """ return the action to see all the rating of the project, and activate default filters """ + if self.portal_show_rating: + return { + 'type': 'ir.actions.act_url', + 'name': "Redirect to the Website Projcet Rating Page", + 'target': 'self', + 'url': "/project/rating/%s" % (self.id,) + } action = self.env['ir.actions.act_window'].for_xml_id('project', 'rating_rating_action_view_project_rating') action['name'] = _('Ratings of %s') % (self.name,) action_context = safe_eval(action['context']) if action['context'] else {} diff --git a/addons/project/static/src/js/portal_rating.js b/addons/project/static/src/js/portal_rating.js new file mode 100644 index 00000000000..12740b9bbba --- /dev/null +++ b/addons/project/static/src/js/portal_rating.js @@ -0,0 +1,27 @@ +odoo.define('website_rating_project.rating', function (require) { +'use strict'; + + var time = require('web.time'); + require('web.dom_ready'); + + if(!$('.o_portal_project_rating').length) { + return $.Deferred().reject("DOM doesn't contain '.o_portal_project_rating'"); + } + + /** + * Rating popover with some informations + */ + $('.o_portal_project_rating .o_rating_image').popover({ + placement: 'bottom', + trigger: 'hover', + html: 'true', + content: function () { + var id = $(this).data('id'); + var rating_date = $(this).data('rating-date'); + var base_date = time.auto_str_to_date(rating_date); + var duration = moment(base_date).fromNow(); + $("#rating_"+ id).find(".rating_timeduration").text(duration); + return $("#rating_"+ id).html(); + } + }); +}); diff --git a/addons/project/static/src/less/portal_rating.less b/addons/project/static/src/less/portal_rating.less new file mode 100644 index 00000000000..4ae3290a75b --- /dev/null +++ b/addons/project/static/src/less/portal_rating.less @@ -0,0 +1,34 @@ +.o_portal_project_rating { + .thumbnail{ + height: 240px; + } + .o_top_partner_rating_image { + height: 15px; + } + .o_top_partner_image { + height: 30px; + width: 30px; + } + .o_top_partner_feedback{ + word-wrap: break-word; + } + .o_vertical_separator { + border-left: 1px solid #eeeeee + } + .o_rating_progress { + margin-bottom: 10px; + } + .o_rating_count { + display: inline-block; + min-width: 22px + } + .o_smiley_no_padding_left { + padding-left: 0; + } + .o_smiley_no_padding_right { + padding-right: 0; + } + .o_lighter_smileys { + opacity: 0.4 + } +}
\ No newline at end of file diff --git a/addons/project/views/project_templates.xml b/addons/project/views/project_assets.xml index 9b6d13d2582..7a2c4647984 100644 --- a/addons/project/views/project_templates.xml +++ b/addons/project/views/project_assets.xml @@ -8,6 +8,14 @@ <link rel="stylesheet" href="/project/static/src/less/project_dashboard.less"/> </xpath> </template> + + <template id="assets_frontend" inherit_id="web.assets_frontend" name="Rating Project Assets"> + <xpath expr="." position="inside"> + <link rel="stylesheet" href="/project/static/src/less/portal_rating.less"/> + <script type="text/javascript" src="/project/static/src/js/portal_rating.js"/> + </xpath> + </template> + <template id="qunit_suite" name="project_test" inherit_id="web.qunit_suite"> <xpath expr="//t[@t-set='head']" position="inside"> <script type="text/javascript" src="/project/static/tests/project_tests.js"></script> diff --git a/addons/project/views/project_rating_templates.xml b/addons/project/views/project_rating_templates.xml new file mode 100644 index 00000000000..7ac2e9463ba --- /dev/null +++ b/addons/project/views/project_rating_templates.xml @@ -0,0 +1,203 @@ +<?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"> + <div class="oe_structure"/> + <div class="container o_portal_project_rating"> + <div class="row text-center"> + <h2>Projects</h2> + </div> + <div class="oe_structure"/> + <div class="row"> + <t t-if="not projects"> + <div class="mt64 text-center"> + No project rating puslished for now. + </div> + </t> + <t t-foreach="projects" t-as="project"> + <div t-if="project.percentage_satisfaction_project != -1" class="col-sm-6 col-xs-4 col-md-4 col-lg-4"> + <div class="thumbnail"> + <div> + <div class="caption"> + <span class="label label-default pull-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-md-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 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"/> + </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_10" t-value="(stats['rating_10'] / float(stats['total'])) * 100 if stats['total'] else 0.0"/> + <div class="progress-bar progress-bar-success" t-attf-style="width: #{percent_rating_10}%;" title="Happy"> + <t t-esc="percent_rating_10"/>% + </div> + <t t-set="percent_rating_5" t-value="(stats['rating_5'] / float(stats['total'])) * 100 if stats['total'] else 0.0"/> + <div class="progress-bar progress-bar-warning" t-attf-style="width: #{percent_rating_5}%;" title="Average"> + <t t-esc="percent_rating_5"/>% + </div> + <t t-set="percent_rating_1" t-value="(stats['rating_1'] / float(stats['total'])) * 100 if stats['total'] else 0.0"/> + <div class="progress-bar progress-bar-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-sm-1"> + <img t-if="partner_stat['rated_partner'].image" class="o_top_partner_image" t-attf-src="data:image/png;base64,#{partner_stat['rated_partner'].image}"/> + <img t-if="not partner_stat['rated_partner'].image" class="o_top_partner_image" src='/web/static/src/img/placeholder.png'/> + </div> + <div class="col-sm-4 o_smiley_no_padding_right"> + <span class="ml8" t-esc="partner_stat['rated_partner'].name"/> + </div> + <div class="col-sm-4 o_smiley_no_padding_right"> + <div class="col-sm-4 o_smiley_no_padding_right o_smiley_no_padding_left"> + <span t-if="partner_stat['rating_10']"> + <img title="happy" class="o_top_partner_rating_image" src='/rating/static/src/img/rating_10.png'/> + <span class="o_rating_count" t-esc="partner_stat['rating_10']"/> + </span> + <span t-if="not partner_stat['rating_10']"> + <img title="happy" class="o_top_partner_rating_image o_lighter_smileys" src='/rating/static/src/img/rating_10.png'/> + <span class="o_rating_count o_lighter_smileys" t-esc="partner_stat['rating_10']"/> + </span> + </div> + <div class="col-sm-4 o_smiley_no_padding_right o_smiley_no_padding_left"> + <span t-if="partner_stat['rating_5']"> + <img title="happy" 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" 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-sm-4 o_smiley_no_padding_right o_smiley_no_padding_left"> + <span t-if="partner_stat['rating_1']"> + <img title="happy" 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="happy" 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-sm-3 o_smiley_no_padding_left"> + <strong class="pull-right"><t t-esc="partner_stat['percentage_happy']"/>%</strong> + </div> + </div> + </t> + </template> + + <template id="portal_project_rating_popover"> + <div t-attf-id="rating_#{rating.id}" class="container hidden"> + <div class="row"> + <div class="col-md-2"> + <img t-if="rating.partner_id.image" class="o_top_partner_image" t-attf-src="data:image/png;base64,#{rating.partner_id.image}"/> + <img t-if="not rating.partner_id.image" class="o_top_partner_image" src='/web/static/src/img/placeholder.png'/> + </div> + <div class="col-md-10"> + <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-md-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"> + <div class="oe_structure"/> + <div class="container o_project o_portal_project_rating"> + <h1 t-esc="project.name" class="text-center"/> + <div class="row mb32"> + <div> + <h3 class="page-header">Customer Ratings</h3> + <div class="row"> + <div class="col-md-7"> + <t t-foreach="ratings" t-as="rating"> + <img t-attf-src='/rating/static/src/img/rating_#{int(rating.rating)}.png' + class="mt4 o_rating_image" + t-att-data-id="rating.id" + t-att-data-rating-date="rating.write_date"/> + <t t-call="project.portal_project_rating_popover"/> + </t> + </div> + <div class="col-md-5 o_vertical_separator"> + <div t-if="statistics['period_statistics']['days_06']"> + <h5>Last 7 days</h5> + <t t-call="project.portal_project_rating_progressbar"> + <t t-set="stats" t-value="statistics['period_statistics']['days_06']"/> + </t> + </div> + <div t-if="statistics['period_statistics']['days_30']"> + <h5>Last 30 days</h5> + <t t-call="project.portal_project_rating_progressbar"> + <t t-set="stats" t-value="statistics['period_statistics']['days_30']"/> + </t> + </div> + <div t-if="statistics['period_statistics']['days_90']"> + <h5>Last 3 months</h5> + <t t-call="project.portal_project_rating_progressbar"> + <t t-set="stats" t-value="statistics['period_statistics']['days_90']"/> + </t> + </div> + <div> + <t t-call="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"/> + </div> + </t> + </template> + + </data> +</odoo> diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 3fc53cf4892..cf6ef36411c 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -152,6 +152,7 @@ </p> </div> <field name="rating_status_period" class="oe_inline" attrs="{'required': [('rating_status','=','periodic')], 'invisible': [('rating_status','!=','periodic')]}" groups="project.group_project_rating"/> + <field name="portal_show_rating" attrs="{'invisible': [('rating_status','==','no')]}" groups="project.group_project_rating"/> </group> <group name="misc"> <group string="Configuration" groups="base.group_no_one"> |
