diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index aaba5cea675..5e62bf95162 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -17,18 +17,16 @@ Odoo Website CMS 'installable': True, 'data': [ 'data/website_data.xml', - 'data/web_planner_data.xml', 'security/website_security.xml', 'security/ir.model.access.csv', - 'views/website_templates.xml', 'views/website_navbar_templates.xml', 'views/snippets.xml', 'views/website_views.xml', 'views/res_config_views.xml', 'views/ir_actions_views.xml', - 'wizard/base_language_install_views.xml', + 'data/web_planner_data.xml', ], 'demo': [ 'data/website_demo.xml', diff --git a/addons/website/controllers/backend.py b/addons/website/controllers/backend.py index 6897da70fc3..3c9323c6823 100644 --- a/addons/website/controllers/backend.py +++ b/addons/website/controllers/backend.py @@ -9,16 +9,29 @@ class WebsiteBackend(http.Controller): @http.route('/website/fetch_dashboard_data', type="json", auth='user') def fetch_dashboard_data(self, date_from, date_to): - - params = request.env['ir.config_parameter'] - ga_client_id = params.sudo().get_param('google_management_client_id', default='') - - return { - 'groups': {'system': request.env['res.users'].has_group('base.group_system')}, + has_group_system = request.env['res.users'].has_group('base.group_system') + has_group_designer = request.env['res.users'].has_group('website.group_website_designer') + if has_group_system: + apps_data = dict((app['name'], app) for app in request.env['ir.module.module'].sudo().search_read( + ['|', ('name', 'ilike', 'website'), ('application', '=', True)], + ['id', 'sequence', 'name', 'shortdesc', 'state'], + order='sequence ASC')) + else: + apps_data = {} + dashboard_data = { + 'groups': { + 'system': has_group_system, + 'website_designer': has_group_designer + }, 'currency': request.env.user.company_id.currency_id.id, 'dashboards': { - 'visits': { - 'ga_client_id': ga_client_id, - } + 'apps_data': apps_data, + 'visits': {}, } } + if has_group_designer: + ga_dashboard = request.env['ir.values'].sudo().get_default('website.config.settings', 'has_google_analytics_dashboard') + if ga_dashboard: + ga_client_id = request.env['ir.config_parameter'].sudo().get_param('google_management_client_id', default='') + dashboard_data['dashboards']['visits']['ga_client_id'] = ga_client_id + return dashboard_data diff --git a/addons/website/data/web_planner_data.xml b/addons/website/data/web_planner_data.xml index 0485cd3aac4..0cb1687dda8 100644 --- a/addons/website/data/web_planner_data.xml +++ b/addons/website/data/web_planner_data.xml @@ -709,7 +709,7 @@ Website Planner - + planner_website self - - Website - - website,static/description/icon.png - - - open diff --git a/addons/website/models/website.py b/addons/website/models/website.py index db918401eed..95a229cda71 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -601,6 +601,12 @@ class Website(models.Model): return urlparse.urljoin(cdn_url, uri) return uri + @api.model + def action_dashboard_redirect(self): + if self.env.user.has_group('base.group_system') or self.env.user.has_group('website.group_website_designer'): + return self.env.ref('website.backend_dashboard').read()[0] + return self.env.ref('website.action_website').read()[0] + class Menu(models.Model): diff --git a/addons/website/static/description/icon_admin.png b/addons/website/static/description/icon_admin.png deleted file mode 100644 index c3c37a0d645..00000000000 Binary files a/addons/website/static/description/icon_admin.png and /dev/null differ diff --git a/addons/website/static/src/js/website.backendDashboard.js b/addons/website/static/src/js/website.backendDashboard.js index 5caad853fcf..3b5ad204ad7 100644 --- a/addons/website/static/src/js/website.backendDashboard.js +++ b/addons/website/static/src/js/website.backendDashboard.js @@ -6,12 +6,14 @@ var ajax = require('web.ajax'); var ControlPanelMixin = require('web.ControlPanelMixin'); var core = require('web.core'); var Dialog = require('web.Dialog'); +var formats = require('web.formats'); var Model = require('web.Model'); var session = require('web.session'); -var utils = require('web.utils'); var web_client = require('web.web_client'); var Widget = require('web.Widget'); +var local_storage = require('web.local_storage'); + var _t = core._t; var QWeb = core.qweb; @@ -19,6 +21,9 @@ var Dashboard = Widget.extend(ControlPanelMixin, { template: "website.WebsiteDashboardMain", events: { 'click .js_link_analytics_settings': 'on_link_analytics_settings', + 'click .o_dashboard_action': 'on_dashboard_action', + 'click .o_dashboard_action_form': 'on_dashboard_action_form', + 'click .o_dashboard_hide_panel': 'on_dashboard_hide_panel', }, init: function(parent, context) { @@ -27,6 +32,7 @@ var Dashboard = Widget.extend(ControlPanelMixin, { this.date_range = 'week'; // possible values : 'week', 'month', year' this.date_from = moment().subtract(1, 'week'); this.date_to = moment(); + this.hidden_apps = JSON.parse(local_storage.getItem('website_dashboard_hidden_app_ids') || '[]'); this.dashboards_templates = ['website.dashboard_visits']; this.graphs = []; @@ -106,7 +112,7 @@ var Dashboard = Widget.extend(ControlPanelMixin, { render_dashboards: function() { var self = this; _.each(this.dashboards_templates, function(template) { - self.$('.o_website_dashboard').append(QWeb.render(template, {widget: self})); + self.$('.o_website_dashboard_content').append(QWeb.render(template, {widget: self})); }); }, @@ -139,7 +145,7 @@ var Dashboard = Widget.extend(ControlPanelMixin, { .append("svg"); svg - .attr("height", '20em') + .attr("height", '24em') .datum(chart_values) .call(chart); @@ -205,7 +211,7 @@ var Dashboard = Widget.extend(ControlPanelMixin, { var self = this; $.when(this.fetch_data()).then(function() { - self.$('.o_website_dashboard').empty(); + self.$('.o_website_dashboard_content').empty(); self.render_dashboards(); self.render_graphs(); }); @@ -217,6 +223,49 @@ var Dashboard = Widget.extend(ControlPanelMixin, { this.update_cp(); }, + on_dashboard_action: function (ev) { + ev.preventDefault(); + var $action = $(ev.currentTarget); + var additional_context = {}; + if (this.date_range === 'week') { + additional_context = {'search_default_week': true} + } else if (this.date_range === 'month') { + additional_context = {'search_default_month': true} + } else if (this.date_range === 'year') { + additional_context = {'search_default_year': true} + } + this.do_action($action.attr('name'), { + additional_context: additional_context, + on_reverse_breadcrumb: this.on_reverse_breadcrumb + }); + }, + + on_dashboard_action_form: function (ev) { + ev.preventDefault(); + var $action = $(ev.currentTarget); + this.do_action({ + name: $action.attr('name'), + res_model: $action.data('res_model'), + res_id: $action.data('res_id'), + views: [[false, 'form']], + type: 'ir.actions.act_window', + }, { + on_reverse_breadcrumb: this.on_reverse_breadcrumb + }); + }, + + on_dashboard_hide_panel: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + var $action = $(ev.currentTarget); + // update hidden module list + this.hidden_apps = JSON.parse(local_storage.getItem('website_dashboard_hidden_app_ids') || '[]'); + this.hidden_apps.push(JSON.parse($action.data('module_id'))); + local_storage.setItem('website_dashboard_hidden_app_ids', JSON.stringify(this.hidden_apps)); + // remove box + $action.closest(".o_box_item").remove(); + }, + update_cp: function() { var self = this; if (!this.$searchview) { @@ -517,24 +566,24 @@ var Dashboard = Widget.extend(ControlPanelMixin, { return i % keep_one_of === 0; }); }, - format_number: function(value, symbol) { - value = utils.human_number(value); - if (symbol === 'currency') { + format_number: function(value, type, digits, symbol) { + if (type === 'currency') { return this.render_monetary_field(value, this.currency_id); } else { - return value + (symbol || ''); + return formats.format_value(value || 0, {type: type, digits: digits}) + ' ' + symbol; } }, render_monetary_field: function(value, currency_id) { var currency = session.get_currency(currency_id); + var formatted_value = formats.format_value(value || 0, {type: "float", digits: currency && currency.digits}); if (currency) { if (currency.position === "after") { - value += currency.symbol; + formatted_value += currency.symbol; } else { - value = currency.symbol + value; + formatted_value = currency.symbol + formatted_value; } } - return value; + return formatted_value; }, }); diff --git a/addons/website/static/src/less/website.backendDashboard.less b/addons/website/static/src/less/website.backendDashboard.less index 8c2c4737a3a..83116d07a94 100644 --- a/addons/website/static/src/less/website.backendDashboard.less +++ b/addons/website/static/src/less/website.backendDashboard.less @@ -27,9 +27,9 @@ h4 { margin: 0 0 0 8px; } - h2 { color: @odoo-brand-primary; } + h2, h4 { color: @odoo-brand-primary; } - .col-md-6 { + .col-md-5, .col-md-7 { padding: 15px; .js_field_selection { @@ -80,12 +80,6 @@ .translate(45; 30) !important; } - g.nv-y.nv-axis, g.nv-x.nv-axis, g.nv-y.nv-axis .nv-axisMaxMin { - text { - opacity: .3 !important; - } - } - g.nv-x.nv-axis .tick > line { opacity: 0 !important; } @@ -122,14 +116,86 @@ background-color: @odoo-brand-optional !important; } tr:last-child .legend-color-guide > div { - background-color: darken(@odoo-brand-optional, 10%) !important; + background-color: darken(@odoo-brand-lightsecondary, 10%) !important; } } } } + .o_dashboard_common { + .o_box { + display: flex; + flex-flow: row wrap; + justify-content: flex-start; + > .o_box_item { + @media (max-width: @screen-sm-min) { + flex: 1 1 200px; + display: block !important; + } + @media (min-width: @screen-sm-min) { + flex: 0 0 16.6%; + } + } + } + .o_inner_box { + padding-top: 10px; + text-align: center; + border: 1px solid @odoo-view-background-color; + height: 80px; + overflow: hidden; + text-overflow: ellipsis; + &.o_main { + background-color: @odoo-brand-optional; + &:hover { + background-color: darken(@odoo-brand-optional, 10%); + } + a { + color: white; + } + } + &.o_primary { + background-color: @odoo-brand-primary; + color: white; + &:hover { + background-color: darken(@odoo-brand-primary, 10%); + } + } + &.o_apps { + position: relative; + background-color: @odoo-brand-optional; + color: white; + .o_apps_hidden { + display: none; + &.o_dashboard_hide_panel { + position: absolute; + top: 0px; + right: 0px; + background: @brand-danger; + padding: 6px; + } + } + &:hover { + background-color: #45818E; + .o_apps_show { + display: none; + } + .o_apps_hidden { + display: block; + } + } + } + .o_highlight { + font-size: 27px; + } + } + } + .o_dashboard_visits { + h2 { + padding: 15px; + } + .o_demo_background { margin-top: 16px; diff --git a/addons/website/static/src/xml/website.backend.xml b/addons/website/static/src/xml/website.backend.xml index ad7c4715378..a30267af9a8 100644 --- a/addons/website/static/src/xml/website.backend.xml +++ b/addons/website/static/src/xml/website.backend.xml @@ -8,17 +8,69 @@
+ +
+
+
+ + + + -
+

Visits

@@ -26,8 +78,7 @@

There is no data currently available.

- -
You do not have sufficient access rights to configure the Google Analytics account.
+
@@ -42,14 +93,14 @@
Your Google Analytic Client ID: -
Go to our User Documentation to get more information. +
Go to our User Documentation to get more information.
-
- - - +
+ + +
diff --git a/addons/website/views/res_config_views.xml b/addons/website/views/res_config_views.xml index 0683496ec02..f923fa167a5 100644 --- a/addons/website/views/res_config_views.xml +++ b/addons/website/views/res_config_views.xml @@ -98,7 +98,7 @@
- Tutorial @@ -371,18 +371,12 @@ You can also edit the fields of the Extra Step page in eCommerce checkout."> inline - - Website Dashboard - backend_dashboard - - - - - - - + diff --git a/addons/website/views/website_views.xml b/addons/website/views/website_views.xml index c52476cc4ef..1b9b4c87fa5 100644 --- a/addons/website/views/website_views.xml +++ b/addons/website/views/website_views.xml @@ -1,6 +1,12 @@ + + @@ -112,5 +118,24 @@ + + + Dashboard + backend_dashboard + + + + Website: Dashboard + + code + action = model.action_dashboard_redirect() + + + + diff --git a/addons/website_blog/views/website_blog_views.xml b/addons/website_blog/views/website_blog_views.xml index c8192c33d44..46e80f1d0e1 100644 --- a/addons/website_blog/views/website_blog_views.xml +++ b/addons/website_blog/views/website_blog_views.xml @@ -1,6 +1,10 @@ - + diff --git a/addons/website_crm/__init__.py b/addons/website_crm/__init__.py index ac0011716f6..81fc81d29c9 100644 --- a/addons/website_crm/__init__.py +++ b/addons/website_crm/__init__.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import controllers import models diff --git a/addons/website_crm/__manifest__.py b/addons/website_crm/__manifest__.py index 53d16e58db9..2e16675dc95 100644 --- a/addons/website_crm/__manifest__.py +++ b/addons/website_crm/__manifest__.py @@ -1,6 +1,7 @@ { 'name': 'Contact Form', 'category': 'Website', + 'sequence': 54, 'website': 'https://www.odoo.com/page/website-builder', 'summary': 'Create Leads From Contact Form', 'version': '2.0', diff --git a/addons/website_crm/controllers/__init__.py b/addons/website_crm/controllers/__init__.py deleted file mode 100644 index f48716d3a69..00000000000 --- a/addons/website_crm/controllers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import main diff --git a/addons/website_crm/controllers/main.py b/addons/website_crm/controllers/main.py deleted file mode 100644 index 0782c760a62..00000000000 --- a/addons/website_crm/controllers/main.py +++ /dev/null @@ -1,91 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import babel -from datetime import datetime, timedelta - -from odoo import http -from odoo.http import request - -from odoo.addons.website.controllers.backend import WebsiteBackend -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT - - -class WebsiteCrmBackend(WebsiteBackend): - - @http.route() - def fetch_dashboard_data(self, date_from, date_to): - - results = super(WebsiteCrmBackend, self).fetch_dashboard_data(date_from, date_to) - results['groups']['sale_salesman'] = request.env['res.users'].has_group('sales_team.group_sale_salesman') - if not results['groups']['sale_salesman']: - return results - - date_from = datetime.strptime(date_from, DEFAULT_SERVER_DATE_FORMAT) - date_to = datetime.strptime(date_to, DEFAULT_SERVER_DATE_FORMAT) - - lead_domain = [] - - website_utm = request.env['ir.model.data'].xmlid_to_res_id('utm.utm_medium_website') - if website_utm: - lead_domain += [('medium_id', '=', website_utm)] - - lead_ids = request.env['crm.lead'].search( - lead_domain + [ - ('create_date', '>=', date_from.strftime(DEFAULT_SERVER_DATE_FORMAT)), - ('create_date', '<=', date_to.strftime(DEFAULT_SERVER_DATE_FORMAT))], - ) - - leads = [{ - 'create_date': datetime.strptime(lead.create_date, DEFAULT_SERVER_DATETIME_FORMAT).strftime(DEFAULT_SERVER_DATE_FORMAT), - 'campaign_id': lead.campaign_id.name, - 'medium_id': lead.medium_id.name, - 'source_id': lead.source_id.name, - } for lead in lead_ids] - - leads_graph = self._compute_lead_graph(date_from, date_to, lead_domain) - previous_leads_graph = self._compute_lead_graph(date_from - timedelta(days=(date_to - date_from).days), date_from, lead_domain, previous=True) - - results['dashboards']['leads'] = { - 'graph': [ - { - 'values': leads_graph, - 'key': 'Leads', - }, - { - 'values': previous_leads_graph, - 'key': 'Previous Leads', - }, - ], - 'leads': leads, - 'lead_fields': { - 'create_date': 'Create Date', - 'campaign_id': 'Campaign', - 'medium_id': 'Medium', - 'source_id': 'Source', - }, - } - - return results - - def _compute_lead_graph(self, date_from, date_to, lead_domain, previous=False): - - days_between = (date_to - date_from).days - date_list = [(date_from + timedelta(days=x)) for x in range(0, days_between + 1)] - - daily_leads = request.env['crm.lead'].read_group( - domain=lead_domain + [ - ('create_date', '>=', date_from.strftime(DEFAULT_SERVER_DATE_FORMAT)), - ('create_date', '<=', date_to.strftime(DEFAULT_SERVER_DATE_FORMAT))], - fields=['create_date'], - groupby='create_date:day') - - daily_leads_dict = {l['create_date:day']: l['create_date_count'] for l in daily_leads} - - leads_graph = [{ - '0': d.strftime(DEFAULT_SERVER_DATE_FORMAT) if not previous else (d + timedelta(days=days_between)).strftime(DEFAULT_SERVER_DATE_FORMAT), - # Respect read_group format in models.py - '1': daily_leads_dict.get(babel.dates.format_date(d, format='dd MMM yyyy', locale=request.env.context.get('lang', 'en_US')), 0) - } for d in date_list] - - return leads_graph diff --git a/addons/website_crm/static/src/js/website_crm_backend.js b/addons/website_crm/static/src/js/website_crm_backend.js deleted file mode 100644 index 4a8bee4875c..00000000000 --- a/addons/website_crm/static/src/js/website_crm_backend.js +++ /dev/null @@ -1,64 +0,0 @@ -odoo.define('website_crm.backend', function (require) { -"use strict"; - -var core = require('web.core'); -var WebsiteBackend = require('website.backendDashboard'); - -var QWeb = core.qweb; - -WebsiteBackend.include({ - - events: _.defaults({ - 'change .js_field_selection': 'on_field_selection', - }, WebsiteBackend.prototype.events), - - init: function(parent, context) { - this._super(parent, context); - - this.dashboards_templates.push('website_crm.dashboard_leads'); - this.graphs.push({'name': 'leads', 'group': 'sale_salesman'}); - - this.leads_table = []; - this.lead_fields = []; - this.lead_field = ''; - }, - - compute_percentages: function() { - if (!this.dashboards_data.leads.leads || !this.dashboards_data.leads.leads.length) { - return; - } - var leads = this.dashboards_data.leads.leads; - var lead_field_keys = _.pluck(leads, this.lead_field); - var leads_counter = _.groupBy(lead_field_keys); - this.leads_table = $.map(leads_counter, function(e, key) { - return { - 'value': e.length, - 'key': key, - 'perc': Math.round(e.length*100/leads.length) - }; - }); - this.leads_table.sort(function(a, b) { return b.value - a.value; }); - }, - - fetch_data: function() { - var self = this; - return this._super().then(function() { - if (self.dashboards_data.leads) { - var leads = self.dashboards_data.leads.leads; - if (leads && leads.length) { - self.lead_fields = self.dashboards_data.leads.lead_fields; - self.lead_field = Object.keys(self.lead_fields)[0]; - self.compute_percentages(); - } - } - }); - }, - - on_field_selection: function(ev) { - this.lead_field = ev.currentTarget.value; - this.compute_percentages(); - this.$('.js_leads_table').replaceWith(QWeb.render("website_crm.LeadsTable", {'widget': this})); - }, -}); - -}); diff --git a/addons/website_crm/static/src/xml/website_crm_dashboard.xml b/addons/website_crm/static/src/xml/website_crm_dashboard.xml deleted file mode 100644 index 277ac0f4f17..00000000000 --- a/addons/website_crm/static/src/xml/website_crm_dashboard.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - -
-
-
-

Leads Acquisition /

-
-
- -
-

Leads Analysis

-
- There is no recent lead. -
- -
- - - -
-
-
-
- - - - - - - - - - - - - - -
Leads% Leads
%
-
- diff --git a/addons/website_crm/views/website_crm_templates.xml b/addons/website_crm/views/website_crm_templates.xml index 5c7c2807bab..f3c657ed3a3 100644 --- a/addons/website_crm/views/website_crm_templates.xml +++ b/addons/website_crm/views/website_crm_templates.xml @@ -89,10 +89,4 @@ - - diff --git a/addons/website_event/__manifest__.py b/addons/website_event/__manifest__.py index 6811d586e16..cd5d5ead131 100644 --- a/addons/website_event/__manifest__.py +++ b/addons/website_event/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Online Events', 'category': 'Marketing', - 'sequence': 135, + 'sequence': 166, 'summary': 'Publish Events and Manage Online Registrations on your Website', 'website': 'https://www.odoo.com/page/website-builder', 'description': """ diff --git a/addons/website_hr_recruitment/__manifest__.py b/addons/website_hr_recruitment/__manifest__.py index 0d3097d73b7..0e661b173e2 100644 --- a/addons/website_hr_recruitment/__manifest__.py +++ b/addons/website_hr_recruitment/__manifest__.py @@ -4,6 +4,7 @@ { 'name': 'Online Jobs', 'category': 'Website', + 'sequence': 142, 'version': '1.0', 'summary': 'Job Descriptions And Application Forms', 'description': """ diff --git a/addons/website_sale/__manifest__.py b/addons/website_sale/__manifest__.py index a02e4627ce1..b31db20df7a 100644 --- a/addons/website_sale/__manifest__.py +++ b/addons/website_sale/__manifest__.py @@ -16,13 +16,13 @@ Odoo E-Commerce 'security/website_sale.xml', 'data/data.xml', 'data/web_planner_data.xml', - 'views/views.xml', - 'views/backend.xml', + 'views/product_views.xml', + 'views/account_views.xml', + 'views/sale_report_views.xml', + 'views/sale_order_views.xml', 'views/crm_team_views.xml', 'views/templates.xml', - 'views/payment.xml', - 'views/product_attribute_views.xml', - 'views/sale_order.xml', + 'views/payment_views.xml', 'views/snippets.xml', 'views/report_shop_saleorder.xml', 'views/res_config_view.xml', diff --git a/addons/website_sale/controllers/backend.py b/addons/website_sale/controllers/backend.py index 53bd7054e97..80cfc690d0e 100644 --- a/addons/website_sale/controllers/backend.py +++ b/addons/website_sale/controllers/backend.py @@ -1,72 +1,121 @@ # -*- coding: utf-8 -*- -from odoo import http -from odoo.http import request -from dateutil.relativedelta import relativedelta -from datetime import datetime, date, timedelta -from math import floor -import time -import operator + import babel +from datetime import datetime, timedelta, time + +from odoo import fields, http, _ from odoo.addons.website.controllers.backend import WebsiteBackend -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT +from odoo.http import request class WebsiteSaleBackend(WebsiteBackend): @http.route() def fetch_dashboard_data(self, date_from, date_to): - results = super(WebsiteSaleBackend, self).fetch_dashboard_data(date_from, date_to) + + sales_values = dict( + graph=[], + best_sellers=[], + summary=dict( + order_count=0, order_carts_count=0, order_unpaid_count=0, + order_to_invoice_count=0, order_carts_abandonned_count=0, + payment_to_capture_count=0, total_sold=0, + order_per_day_ratio=0, order_sold_ratio=0, order_convertion_pctg=0, + ) + ) + results['dashboards']['sales'] = sales_values + results['groups']['sale_salesman'] = request.env['res.users'].has_group('sales_team.group_sale_salesman') if not results['groups']['sale_salesman']: return results - date_from = datetime.strptime(date_from, DEFAULT_SERVER_DATE_FORMAT) - date_to = datetime.strptime(date_to, DEFAULT_SERVER_DATE_FORMAT) + date_date_from = fields.Date.from_string(date_from) + date_date_to = fields.Date.from_string(date_to) + date_diff_days = (date_date_to - date_date_from).days + datetime_from = datetime.combine(date_date_from, time.min) + datetime_to = datetime.combine(date_date_to, time.max) - # Best seller products - product_lines = request.env['sale.order.line'].read_group( + # Product-based computation + report_product_lines = request.env['sale.report'].read_group( domain=[ ('product_id.website_published', '=', True), - ('order_id.state', 'in', ['sent', 'sale', 'done']), - ('order_id.team_id.website_ids', '!=', False), - ('order_id.date_order', '>=', date_from.strftime(DEFAULT_SERVER_DATE_FORMAT)), - ('order_id.date_order', '<=', date_to.strftime(DEFAULT_SERVER_DATE_FORMAT))], - fields=['product_id', 'product_uom_qty', 'price_total'], + ('team_id.team_type', '=', 'website'), + ('state', 'in', ['sale', 'done']), + ('date', '>=', date_from), + ('date', '<=', date_to)], + fields=['product_id', 'product_uom_qty', 'price_subtotal'], groupby='product_id', orderby='product_uom_qty desc', limit=5) - - best_sellers = [] - for product_line in product_lines: + for product_line in report_product_lines: product_id = request.env['product.product'].browse(product_line['product_id'][0]) - best_sellers.append({ + sales_values['best_sellers'].append({ 'id': product_id.id, 'name': product_id.name, 'qty': product_line['product_uom_qty'], - 'sales': product_line['price_total'], + 'sales': product_line['price_subtotal'], }) - # Graphes computation - sales_domain = [ - ('team_id.website_ids', '!=', False), - ('state', 'in', ['sent', 'sale', 'done']), - ] - sales_graph = self._compute_sale_graph(date_from, date_to, sales_domain) - previous_sales_graph = self._compute_sale_graph(date_from - timedelta(days=(date_to - date_from).days), date_from, sales_domain, previous=True) + # Sale-based results computation + sale_order_domain = [ + ('team_id', 'in', request.env['crm.team'].search([('team_type', '=', 'website')]).ids), + ('date_order', '>=', fields.Datetime.to_string(datetime_from)), + ('date_order', '<=', fields.Datetime.to_string(datetime_to))] + so_group_data = request.env['sale.order'].read_group(sale_order_domain, fields=['state'], groupby='state') + for res in so_group_data: + if res.get('state') == 'sent': + sales_values['summary']['order_unpaid_count'] += res['state_count'] + elif res.get('state') in ['sale', 'done']: + sales_values['summary']['order_count'] += res['state_count'] + sales_values['summary']['order_carts_count'] += res['state_count'] - results['dashboards']['sales'] = { - 'graph': [ - { - 'values': sales_graph, - 'key': 'Sales', - }, - { - 'values': previous_sales_graph, - 'key': 'Previous Sales', - }, - ], - 'best_sellers': best_sellers, - } + sales_values['summary'].update( + order_to_invoice_count=request.env['sale.order'].search_count(sale_order_domain + [ + ('state', 'in', ['sale', 'done']), + ('invoice_status', '=', 'to invoice'), + ]), + order_carts_abandonned_count=request.env['sale.order'].search_count(sale_order_domain + [ + ('state', '=', 'draft'), + ('date_order', '<=', fields.Datetime.to_string(datetime.now() - timedelta(hours=1))), + ]), + payment_to_capture_count=request.env['payment.transaction'].search_count([ + ('state', '=', 'authorized'), + ('sale_order_id.state', '!=', 'cancel') + ]), + total_sold=sum(request.env['sale.report'].search([ + ('state', 'in', ['sale', 'done']), + ('team_id.team_type', '=', 'website'), + ('date', '>=', date_from), + ('date', '<=', date_to)]).mapped('price_subtotal') + ) + ) + + # Ratio computation + sales_values['summary']['order_per_day_ratio'] = round(float(sales_values['summary']['order_count']) / date_diff_days, 2) + sales_values['summary']['order_sold_ratio'] = round(float(sales_values['summary']['total_sold']) / sales_values['summary']['order_count'], 2) if sales_values['summary']['order_count'] else 0 + sales_values['summary']['order_convertion_pctg'] = 100.0 * sales_values['summary']['order_count'] / sales_values['summary']['order_carts_count'] if sales_values['summary']['order_carts_count'] else 0 + + # Graphes computation + if date_diff_days == 7: + previous_sale_label = _('Previous Week') + elif date_diff_days > 7 and date_diff_days <= 31: + previous_sale_label = _('Previous Month') + else: + previous_sale_label = _('Previous Year') + + sales_domain = [ + ('team_id.team_type', '=', 'website'), + ('state', 'in', ['sale', 'done']), + ('date', '>=', date_from), + ('date', '<=', date_to) + ] + sales_values['graph'] += [{ + 'values': self._compute_sale_graph(date_date_from, date_date_to, sales_domain), + 'key': 'Untaxed Total', + }, { + 'values': self._compute_sale_graph(date_date_from - timedelta(days=date_diff_days), date_date_from, sales_domain, previous=True), + 'key': previous_sale_label, + }] return results @@ -75,17 +124,15 @@ class WebsiteSaleBackend(WebsiteBackend): days_between = (date_to - date_from).days date_list = [(date_from + timedelta(days=x)) for x in range(0, days_between + 1)] - daily_sales = request.env['sale.order'].read_group( - domain=sales_domain + [ - ('date_order', '>=', date_from.strftime(DEFAULT_SERVER_DATE_FORMAT)), - ('date_order', '<=', date_to.strftime(DEFAULT_SERVER_DATE_FORMAT))], - fields=['date_order', 'amount_total'], - groupby='date_order:day') + daily_sales = request.env['sale.report'].read_group( + domain=sales_domain, + fields=['date', 'price_subtotal'], + groupby='date:day') - daily_sales_dict = {p['date_order:day']: p['amount_total'] for p in daily_sales} + daily_sales_dict = {p['date:day']: p['price_subtotal'] for p in daily_sales} sales_graph = [{ - '0': d.strftime(DEFAULT_SERVER_DATE_FORMAT) if not previous else (d + timedelta(days=days_between)).strftime(DEFAULT_SERVER_DATE_FORMAT), + '0': fields.Date.to_string(d) if not previous else fields.Date.to_string(d + timedelta(days=days_between)), # Respect read_group format in models.py '1': daily_sales_dict.get(babel.dates.format_date(d, format='dd MMM yyyy', locale=request.env.context.get('lang', 'en_US')), 0) } for d in date_list] diff --git a/addons/website_sale/data/demo.xml b/addons/website_sale/data/demo.xml index c0ee27a2be9..d583adbd514 100644 --- a/addons/website_sale/data/demo.xml +++ b/addons/website_sale/data/demo.xml @@ -794,20 +794,11 @@ Weight: 31 grams - Laptop E5023 - + iPad Mini + 1 - 2950.00 - - - - - Mouse, Wireless - - 1 - - 12.50 + 599.0 @@ -820,22 +811,13 @@ Weight: 31 grams - + - Laptop E5023 - - 2 + iPad Retina Display + + 1 - 3150.00 - - - - - Mouse, Wireless - - 2 - - 12.50 + 900 @@ -848,22 +830,13 @@ Weight: 31 grams - + - Laptop E5023 - + iPad Retina Display + 1 - 3300.00 - - - - - Mouse, Wireless - - 2 - - 12.50 + 750 @@ -876,22 +849,13 @@ Weight: 31 grams - + - Laptop E5023 - - 3 + iMac + + 1 - 2750.00 - - - - - Mouse, Wireless - - 3 - - 12.50 + 1199.0 @@ -904,22 +868,13 @@ Weight: 31 grams - + - Laptop E5023 - - 2 + iPad Retina Display + + 3 - 2950.00 - - - - - Mouse, Wireless - - 2 - - 12.50 + 349.0 @@ -932,22 +887,13 @@ Weight: 31 grams - + - Laptop E5023 - - 3 + iMac + + 1 - 3250.00 - - - - - Mouse, Wireless - - 3 - - 12.50 + 1599.00 @@ -960,22 +906,13 @@ Weight: 31 grams - + - Laptop E5023 - - 2 + iMac + + 1 - 2850.00 - - - - - Mouse, Wireless - - 2 - - 12.50 + 1349.00 @@ -988,22 +925,13 @@ Weight: 31 grams - + - Laptop E5023 - - 4 + iMac + + 1 - 3050.00 - - - - - Mouse, Wireless - - 4 - - 12.50 + 1799.00 @@ -1016,7 +944,7 @@ Weight: 31 grams - + Laptop E5023 @@ -1025,7 +953,7 @@ Weight: 31 grams 2950.00 - + Mouse, Wireless @@ -1044,4 +972,83 @@ Weight: 31 grams + + + + + + + + + + + + + + + iPod + + 2 + + 16.50 + + + + + + + + + + + + + + + + Apple Wireless Keyboard + + 1 + + 47.0 + + + + + + + + + + + + + sent + + + + + iMac + + 1 + + 1799.0 + + + + + + + + form + authorized + 1799.0 + + + + + + + + diff --git a/addons/website_sale/models/__init__.py b/addons/website_sale/models/__init__.py index a40ad2379a6..2ca6f70be07 100644 --- a/addons/website_sale/models/__init__.py +++ b/addons/website_sale/models/__init__.py @@ -4,3 +4,4 @@ import product import sale_order import crm_team import res_config +import website diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py new file mode 100644 index 00000000000..8cf1e6b78ae --- /dev/null +++ b/addons/website_sale/models/website.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class Website(models.Model): + _inherit = 'website' + + @api.model + def action_dashboard_redirect(self): + if self.env.user.has_group('sales_team.group_sale_salesman'): + return self.env.ref('website.backend_dashboard').read()[0] + return super(Website, self).action_dashboard_redirect() diff --git a/addons/website_sale/static/src/img/website_sale_dashboard_sales_demo.png b/addons/website_sale/static/src/img/website_sale_dashboard_sales_demo.png new file mode 100644 index 00000000000..4338a7223e6 Binary files /dev/null and b/addons/website_sale/static/src/img/website_sale_dashboard_sales_demo.png differ diff --git a/addons/website_sale/static/src/js/website_sale_backend.js b/addons/website_sale/static/src/js/website_sale_backend.js index 45257e3c72f..18819d7835f 100644 --- a/addons/website_sale/static/src/js/website_sale_backend.js +++ b/addons/website_sale/static/src/js/website_sale_backend.js @@ -12,7 +12,7 @@ WebsiteBackend.include({ init: function(parent, context) { this._super(parent, context); - this.dashboards_templates.push('website_sale.dashboard_sales'); + this.dashboards_templates.unshift('website_sale.dashboard_sales'); this.graphs.push({'name': 'sales', 'group': 'sale_salesman'}); }, diff --git a/addons/website_sale/static/src/less/website_sale_dashboard.less b/addons/website_sale/static/src/less/website_sale_dashboard.less new file mode 100644 index 00000000000..5a698a492ec --- /dev/null +++ b/addons/website_sale/static/src/less/website_sale_dashboard.less @@ -0,0 +1,61 @@ +.o_dashboard_sales { + h4 { margin: 3px 0 4px 0 !important; } + .o_demo_background { + margin-top: 16px; + height: 350px; + background-size: 100% !important; + background: url("/website_sale/static/src/img/website_sale_dashboard_sales_demo.png"); + position: relative; + opacity: 0.2; + } + .o_demo_message { + color: @odoo-main-color-muted; + width: 100%; + .o-position-absolute(@left: 0, @top: 12%); + .o-flex-display(); + .o-justify-content(center); + } + .o_link_enable { + @media (min-width: @screen-md-min) { + border-right: 1px solid #ccc; + } + height: initial; + padding: 5px; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + a { + color: darken(@odoo-brand-lightsecondary, 40%); + } + &:hover { + background-color: @odoo-brand-lightsecondary; + } + .o_highlight { + color: @odoo-brand-optional; + font-size: 20px; + font-weight: bold; + } + } + .o_link_disable { + @media (min-width: @screen-md-min) { + border-right: 1px solid #ccc; + } + height: initial; + padding: 5px; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + margin: 10px 0px; + color: darken(@odoo-brand-lightsecondary, 40%); + .o_highlight { + font-size: 20px; + font-weight: bold; + } + } + .o_invisible_border { + border-right: 0px; + } + .o_top_margin { + margin-top: 20px; + } +} \ No newline at end of file diff --git a/addons/website_sale/static/src/xml/website_sale_dashboard.xml b/addons/website_sale/static/src/xml/website_sale_dashboard.xml index 5ee96f22c55..dec7c0087f3 100644 --- a/addons/website_sale/static/src/xml/website_sale_dashboard.xml +++ b/addons/website_sale/static/src/xml/website_sale_dashboard.xml @@ -1,41 +1,128 @@ - + + + + + + + + -
+
-
-

Sales /

-
+
+
+ +

Sales Since Last

+
+

Sales

+
+
+
+ +
- -
- +
+
+ +

Sales Since Last

+
+

Sales

+
+
+
+

There is no recent confirmed order.

+
+
-

Best Sellers

-
- There is no recent online sale. -
- -
- - - - - - - - - - - -
ProductQty SoldAmount
+
+
+
+

AT A GLANCE

+ + + + + + + + + + + + +
+
+
+
+

Best Sellers

+ + + + + + + + + + + +
ProductQuantitySold
+
+
diff --git a/addons/website_sale/views/account_views.xml b/addons/website_sale/views/account_views.xml new file mode 100644 index 00000000000..53954449ae7 --- /dev/null +++ b/addons/website_sale/views/account_views.xml @@ -0,0 +1,24 @@ + + + + Invoices + account.invoice + form + tree,form + [('team_id.website_ids', '!=', False)] + + {'type':'out_invoice'} + + + + + Pricelists + ir.actions.act_window + product.pricelist + form + tree,form + [('website_id', '!=', False)] + + {"default_base":'list_price'} + + \ No newline at end of file diff --git a/addons/website_sale/views/backend.xml b/addons/website_sale/views/backend.xml deleted file mode 100644 index d970213c2a7..00000000000 --- a/addons/website_sale/views/backend.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - product.template.search.published - product.template - - - - - - - - - - - Products - product.template - form - kanban,tree,form - {'search_default_published': 1, 'default_website_published': True} - - - - sale.order.ecommerce.search.view - sale.order - - - - - - - - - - eCommerce Orders - sale.order - tree,form - [('state', 'in', ('sale', 'done'))] - {'show_sale': True, 'search_default_from_website': 1} - - You don't have any confirmed order from the website. - - - - Unpaid eCommerce Orders - sale.order - tree,form - [('state', '=', 'sent')] - {'show_sale': True, 'search_default_from_website': 1} - - You don't have any unpaid order from the website. - - - - Abandoned eCommerce Orders - sale.order - tree,form - [('state', '=', 'cancel')] - {'show_sale': True, 'search_default_from_website': 1} - - You don't have any cancelled order from the website. - - - - Invoices - account.invoice - form - tree,form - [('team_id.website_ids', '!=', False)] - - {'type':'out_invoice'} - - - - - Pricelists - ir.actions.act_window - product.pricelist - form - tree,form - [('website_id', '!=', False)] - - {"default_base":'list_price'} - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/website_sale/views/payment.xml b/addons/website_sale/views/payment.xml deleted file mode 100644 index 994ed13adad..00000000000 --- a/addons/website_sale/views/payment.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - payment.transaction.form.website_sale - payment.transaction - - - - - - - - - diff --git a/addons/website_sale/views/payment_views.xml b/addons/website_sale/views/payment_views.xml new file mode 100644 index 00000000000..c0b56d12f43 --- /dev/null +++ b/addons/website_sale/views/payment_views.xml @@ -0,0 +1,24 @@ + + + + payment.transaction.view.form.inherit.website_sale + payment.transaction + + + + + + + + + + Payments To Capture + payment.transaction + form + tree,form + [('state', '=', 'authorized'), ('sale_order_id.state', '!=', 'cancel')] + {'create': False} + + You don't have any payments to capture from the website. + + diff --git a/addons/website_sale/views/product_attribute_views.xml b/addons/website_sale/views/product_attribute_views.xml deleted file mode 100644 index c76d94aaab9..00000000000 --- a/addons/website_sale/views/product_attribute_views.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - product.attribute.line.form.inherit - product.attribute.line - - - - - - - - - - - product.attribute.form.inherit - product.attribute - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/addons/website_sale/views/views.xml b/addons/website_sale/views/product_views.xml similarity index 79% rename from addons/website_sale/views/views.xml rename to addons/website_sale/views/product_views.xml index eab192f3ce6..ac6fe261357 100644 --- a/addons/website_sale/views/views.xml +++ b/addons/website_sale/views/product_views.xml @@ -1,5 +1,54 @@ + + product.template.search.published + product.template + + + + + + + + + + + Products + product.template + form + kanban,tree,form + {'search_default_published': 1, 'default_website_published': True} + + + + product.attribute.line.form.inherit + product.attribute.line + + + + + + + + + + + product.attribute.form.inherit + product.attribute + + + + + + + + + + + + + + product.pricelist.website.form product.pricelist diff --git a/addons/website_sale/views/sale_order.xml b/addons/website_sale/views/sale_order.xml deleted file mode 100644 index 0040ac33fe3..00000000000 --- a/addons/website_sale/views/sale_order.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - sale.order.form.acquirer - sale.order - - - - - - - - - diff --git a/addons/website_sale/views/sale_order_views.xml b/addons/website_sale/views/sale_order_views.xml new file mode 100644 index 00000000000..ac2aa118746 --- /dev/null +++ b/addons/website_sale/views/sale_order_views.xml @@ -0,0 +1,139 @@ + + + + sale.order.ecommerce.search.view + sale.order + + + + + + + + + + + + sale.order.form.acquirer + sale.order + + + + + + + + + + + Orders + sale.order + tree,form + [] + {'show_sale': True, 'search_default_confirm_order': 1, 'search_default_from_website': 1} + + You don't have any confirmed order from the website. + + + + Unpaid Orders + sale.order + tree,form + [('state', '=', 'sent')] + {'show_sale': True, 'search_default_from_website': 1, 'create': False} + + You don't have any unpaid order from the website. + + + + Abandoned Carts + sale.order + tree,form + [('state', '=', 'draft'), ('order_line', '!=', False), ('date_order', '<=', (context_today() + datetime.timedelta(hours=-1)).strftime('%Y-%m-%d'))] + {'show_sale': True, 'search_default_from_website': 1, 'create': False} + + +

Find here all the abandoned carts, i.e. the carts generated by your website's visitors more than one hour ago and not confirmed.

+

You should send an email to revive them!

+
+ + + Orders To Invoice + sale.order + tree,form + [('state', 'in', ('sale', 'done')), ('invoice_status', '=', 'to invoice')] + {'show_sale': True, 'search_default_from_website': 1} + + You don't have any order from the website. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/addons/website_sale/views/sale_report_views.xml b/addons/website_sale/views/sale_report_views.xml new file mode 100644 index 00000000000..b9c39c34926 --- /dev/null +++ b/addons/website_sale/views/sale_report_views.xml @@ -0,0 +1,98 @@ + + + + sale.report.search + sale.report + + + + + + + + + + + sale.report.view.pivot.website + sale.report + + + + + + + + + + + sale.report.view.graph.website + sale.report + + + + + + + + + + Sales + sale.report + pivot,graph + [('state','in',('sale', 'done')), ('team_id.team_type', '=', 'website')] + + You don't have any order from the website. + + + + + pivot + + + + + + + graph + + + + + + Sales + sale.report + pivot,graph + [('team_id.team_type', '=', 'website')] + + You don't have any order from the website. + + + + + pivot + + + + + + + graph + + + + \ No newline at end of file diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index f79950916e6..f2bb05cb49e 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -4,6 +4,7 @@ diff --git a/addons/website_slides/view/website_slides_backend.xml b/addons/website_slides/view/website_slides_backend.xml index 85fdab556b0..1c0e5f75f36 100644 --- a/addons/website_slides/view/website_slides_backend.xml +++ b/addons/website_slides/view/website_slides_backend.xml @@ -1,7 +1,11 @@ - +