diff --git a/addons/website_sale/controllers/backend.py b/addons/website_sale/controllers/backend.py index a4427df6de7..80cfc690d0e 100644 --- a/addons/website_sale/controllers/backend.py +++ b/addons/website_sale/controllers/backend.py @@ -2,12 +2,11 @@ import babel -from datetime import datetime, timedelta +from datetime import datetime, timedelta, time -from odoo import http +from odoo import fields, http, _ from odoo.addons.website.controllers.backend import WebsiteBackend from odoo.http import request -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT class WebsiteSaleBackend(WebsiteBackend): @@ -15,55 +14,108 @@ 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 @@ -72,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/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/payment_views.xml b/addons/website_sale/views/payment_views.xml index 781c1634e49..c0b56d12f43 100644 --- a/addons/website_sale/views/payment_views.xml +++ b/addons/website_sale/views/payment_views.xml @@ -1,7 +1,7 @@ - - payment.transaction.form.website_sale + + payment.transaction.view.form.inherit.website_sale payment.transaction @@ -10,4 +10,15 @@ + + + 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/sale_order_views.xml b/addons/website_sale/views/sale_order_views.xml index 2412367a7e7..ac2aa118746 100644 --- a/addons/website_sale/views/sale_order_views.xml +++ b/addons/website_sale/views/sale_order_views.xml @@ -6,7 +6,9 @@ - + + + @@ -24,51 +26,114 @@ - eCommerce Orders + Orders sale.order tree,form - [('state', 'in', ('sale', 'done'))] - {'show_sale': True, 'search_default_from_website': 1} + [] + {'show_sale': True, 'search_default_confirm_order': 1, 'search_default_from_website': 1} You don't have any confirmed order from the website. - Unpaid eCommerce Orders + Unpaid Orders sale.order tree,form [('state', '=', 'sent')] - {'show_sale': True, 'search_default_from_website': 1} + {'show_sale': True, 'search_default_from_website': 1, 'create': False} You don't have any unpaid order from the website. - Abandoned eCommerce Orders + Abandoned Carts sale.order tree,form - [('state', '=', 'cancel')] - {'show_sale': True, 'search_default_from_website': 1} + [('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} - You don't have any cancelled order from the website. + +

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/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 @@