diff --git a/addons/crm/__init__.py b/addons/crm/__init__.py index e9a8b62b2da..927f765e815 100644 --- a/addons/crm/__init__.py +++ b/addons/crm/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import res_users import crm_stage import crm_lead import sales_team diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index c2361939365..4f38d41e933 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date +from dateutil.relativedelta import relativedelta import logging from operator import itemgetter @@ -1036,6 +1037,140 @@ Update your business card, phone book, social media,... Send an email right now break return res + def retrieve_sales_dashboard(self, cr, uid, context=None): + + res = { + 'meeting': { + 'today': 0, + 'next_7_days': 0, + }, + 'activity': { + 'today': 0, + 'overdue': 0, + 'next_7_days': 0, + }, + 'closing': { + 'today': 0, + 'overdue': 0, + 'next_7_days': 0, + }, + 'done': { + 'this_month': 0, + 'last_month': 0, + }, + 'won': { + 'this_month': 0, + 'last_month': 0, + }, + 'nb_opportunities': 0, + } + + opportunities = self.search_read( + cr, uid, + [('type', '=', 'opportunity'), ('user_id', '=', uid)], + ['date_deadline', 'next_activity_id', 'date_action', 'date_closed', 'planned_revenue'], context=context) + + for opp in opportunities: + + # Expected closing + if opp['date_deadline']: + date_deadline = datetime.strptime(opp['date_deadline'], tools.DEFAULT_SERVER_DATE_FORMAT).date() + + if date_deadline == date.today(): + res['closing']['today'] += 1 + if date_deadline >= date.today() and date_deadline <= date.today() + timedelta(days=7): + res['closing']['next_7_days'] += 1 + if date_deadline < date.today(): + res['closing']['overdue'] += 1 + + # Next activities + if opp['next_activity_id'] and opp['date_action']: + date_action = datetime.strptime(opp['date_action'], tools.DEFAULT_SERVER_DATE_FORMAT).date() + + if date_action == date.today(): + res['activity']['today'] += 1 + if date_action >= date.today() and date_action <= date.today() + timedelta(days=7): + res['activity']['next_7_days'] += 1 + if date_action < date.today(): + res['activity']['overdue'] += 1 + + # Won in Opportunities + if opp['date_closed']: + date_closed = datetime.strptime(opp['date_closed'], tools.DEFAULT_SERVER_DATETIME_FORMAT).date() + + if date_closed <= date.today() and date_closed >= date.today().replace(day=1): + if opp['planned_revenue']: + res['won']['this_month'] += opp['planned_revenue'] + elif date_closed < date.today().replace(day=1) and date_closed >= date.today().replace(day=1) - relativedelta(months=+1): + if opp['planned_revenue']: + res['won']['last_month'] += opp['planned_revenue'] + + # crm.activity is a very messy model so we need to do that in order to retrieve the actions done. + cr.execute(""" + SELECT + m.id, + m.subtype_id, + m.date, + l.user_id, + l.type + FROM + "mail_message" m + LEFT JOIN + "crm_lead" l + ON + (m.res_id = l.id) + INNER JOIN + "crm_activity" a + ON + (m.subtype_id = a.subtype_id) + WHERE + (m.model = 'crm.lead') AND (l.user_id = %s) AND (l.type = 'opportunity') + """, (uid,)) + activites_done = cr.dictfetchall() + + for act in activites_done: + if act['date']: + date_act = datetime.strptime(act['date'], tools.DEFAULT_SERVER_DATETIME_FORMAT).date() + if date_act <= date.today() and date_act >= date.today().replace(day=1): + res['done']['this_month'] += 1 + elif date_act < date.today().replace(day=1) and date_act >= date.today().replace(day=1) - relativedelta(months=+1): + res['done']['last_month'] += 1 + + # Meetings + min_date = datetime.now().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) + max_date = (datetime.now() + timedelta(days=8)).strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) + meetings_domain = [ + ('start', '>=', min_date), + ('start', '<=', max_date) + ] + # We need to add 'mymeetings' in the context for the search to be correct. + meetings = self.pool.get('calendar.event').search_read(cr, uid, meetings_domain, ['start'], context=context.update({'mymeetings': 1}) if context else {'mymeetings': 1}) + for meeting in meetings: + if meeting['start']: + start = datetime.strptime(meeting['start'], tools.DEFAULT_SERVER_DATETIME_FORMAT).date() + + if start == date.today(): + res['meeting']['today'] += 1 + if start >= date.today() and start <= date.today() + timedelta(days=7): + res['meeting']['next_7_days'] += 1 + + res['nb_opportunities'] = len(opportunities) + + user = self.pool('res.users').browse(cr, uid, uid, context=context) + res['done']['target'] = user.target_sales_done + res['won']['target'] = user.target_sales_won + + return res + + def modify_target_sales_dashboard(self, cr, uid, target_name, target_value, context=None): + + if target_name in ['won', 'done', 'invoiced']: + # bypass rights (with superuser_id) + self.pool('res.users').write(cr, SUPERUSER_ID, [uid], {'target_sales_' + target_name: target_value}, context=context) + else: + raise UserError(_('This target does not exist.')) + + class crm_lead_tag(osv.Model): _name = "crm.lead.tag" _description = "Category of lead" diff --git a/addons/crm/res_users.py b/addons/crm/res_users.py new file mode 100644 index 00000000000..b6c11b173a1 --- /dev/null +++ b/addons/crm/res_users.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from openerp.osv import osv, fields + +import openerp.addons.product.product + + +class res_users(osv.osv): + _inherit = 'res.users' + _columns = { + 'target_sales_won': fields.integer('Won in Opportunities Target'), + 'target_sales_done': fields.integer('Activities Done Target'), + } diff --git a/addons/crm/static/src/js/sales_team_dashboard.js b/addons/crm/static/src/js/sales_team_dashboard.js new file mode 100644 index 00000000000..b2bec86a753 --- /dev/null +++ b/addons/crm/static/src/js/sales_team_dashboard.js @@ -0,0 +1,15 @@ +odoo.define('crm.sales_team_dashboard', function (require) { +"use strict"; + +var SalesTeamDashboardView = require('sales_team.dashboard'); +var Model = require('web.Model'); + +SalesTeamDashboardView.include({ + + fetch_data: function() { + return new Model('crm.lead') + .call('retrieve_sales_dashboard', []); + } +}); + +}); diff --git a/addons/crm/views/crm.xml b/addons/crm/views/crm.xml index d29f5f7b3e9..4401c82d153 100644 --- a/addons/crm/views/crm.xml +++ b/addons/crm/views/crm.xml @@ -6,6 +6,9 @@ + + + diff --git a/addons/sale_crm/__init__.py b/addons/sale_crm/__init__.py index 0223759a9b4..0f8f154c60f 100644 --- a/addons/sale_crm/__init__.py +++ b/addons/sale_crm/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import res_users import sale_crm import crm_lead diff --git a/addons/sale_crm/crm_lead.py b/addons/sale_crm/crm_lead.py index 0c64496ba07..0980eff5f78 100644 --- a/addons/sale_crm/crm_lead.py +++ b/addons/sale_crm/crm_lead.py @@ -1,5 +1,7 @@ -from openerp import models, fields, api, _ +from openerp import models, fields, api, _, tools +from datetime import datetime, date +from dateutil.relativedelta import relativedelta import openerp.addons.decimal_precision as dp class crm_lead(models.Model): @@ -21,3 +23,29 @@ class crm_lead(models.Model): sale_amount_total= fields.Float(compute='_get_sale_amount_total', string="Sum of Orders", readonly=True, digits=0) sale_number = fields.Integer(compute='_get_sale_amount_total', string="Number of Quotations", readonly=True) order_ids = fields.One2many('sale.order', 'opportunity_id', string='Orders') + + def retrieve_sales_dashboard(self, cr, uid, context=None): + res = super(crm_lead, self).retrieve_sales_dashboard(cr, uid, context=None) + + res['invoiced'] = { + 'this_month': 0, + 'last_month': 0, + } + account_invoice_domain = [ + ('state', 'in', ['open', 'paid']), + ('user_id', '=', uid), + ('date', '>=', date.today().replace(day=1) - relativedelta(months=+1)) + ] + + invoice_ids = self.pool.get('account.invoice').search_read(cr, uid, account_invoice_domain, ['date', 'amount_untaxed_signed'], context=context) + for inv in invoice_ids: + if inv['date']: + inv_date = datetime.strptime(inv['date'], tools.DEFAULT_SERVER_DATE_FORMAT).date() + if inv_date <= date.today() and inv_date >= date.today().replace(day=1): + res['invoiced']['this_month'] += inv['amount_untaxed_signed'] + elif inv_date < date.today().replace(day=1) and inv_date >= date.today().replace(day=1) - relativedelta(months=+1): + res['invoiced']['last_month'] += inv['amount_untaxed_signed'] + + res['invoiced']['target'] = self.pool('res.users').browse(cr, uid, uid, context=context).target_sales_invoiced + + return res diff --git a/addons/sale_crm/res_users.py b/addons/sale_crm/res_users.py new file mode 100644 index 00000000000..9c393085186 --- /dev/null +++ b/addons/sale_crm/res_users.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +from openerp.osv import osv, fields + +import openerp.addons.product.product + + +class res_users(osv.osv): + _inherit = 'res.users' + _columns = { + 'target_sales_invoiced': fields.integer('Invoiced in Sale Orders Target'), + } diff --git a/addons/sales_team/__openerp__.py b/addons/sales_team/__openerp__.py index f9da1c53e00..c9044c90411 100644 --- a/addons/sales_team/__openerp__.py +++ b/addons/sales_team/__openerp__.py @@ -17,6 +17,9 @@ Using this application you can manage Sales Team with CRM and/or Sales 'sales_team.xml', 'sales_team_dashboard.xml', ], + 'qweb': [ + "static/src/xml/sales_team_dashboard.xml", + ], 'demo': ['sales_team_demo.xml'], 'css': ['static/src/css/sales_team.css'], 'installable': True, diff --git a/addons/sales_team/sales_team.xml b/addons/sales_team/sales_team.xml index 1b3ede5d430..81e27b85597 100644 --- a/addons/sales_team/sales_team.xml +++ b/addons/sales_team/sales_team.xml @@ -30,7 +30,7 @@ Sales Teams crm.team form - kanban,tree,form + sales_team_dashboard {} @@ -144,7 +144,9 @@ diff --git a/addons/sales_team/sales_team_dashboard.xml b/addons/sales_team/sales_team_dashboard.xml index 36e44c8e535..79f2bfba168 100644 --- a/addons/sales_team/sales_team_dashboard.xml +++ b/addons/sales_team/sales_team_dashboard.xml @@ -1,10 +1,10 @@ - crm.team.kanban + crm.team.dashboard crm.team - + @@ -58,7 +58,15 @@ - + - \ No newline at end of file + + + Sales Team Dashboard + crm.team + sales_team_dashboard + + + + diff --git a/addons/sales_team/static/src/js/sales_team_dashboard.js b/addons/sales_team/static/src/js/sales_team_dashboard.js new file mode 100644 index 00000000000..0f6fe34ad9e --- /dev/null +++ b/addons/sales_team/static/src/js/sales_team_dashboard.js @@ -0,0 +1,126 @@ +odoo.define('sales_team.dashboard', function (require) { +"use strict"; + +var core = require('web.core'); +var KanbanView = require('web_kanban.KanbanView'); +var Model = require('web.Model'); + +var QWeb = core.qweb; + +var _t = core._t; +var _lt = core._lt; + +var SalesTeamDashboardView = KanbanView.extend({ + display_name: _lt('Dashboard'), + icon: 'fa-dashboard', + view_type: "sales_team_dashboard", + searchview_hidden: true, + events: { + 'click .o_dashboard_action': 'on_dashboard_action_clicked', + 'click .o_target_to_set': 'on_dashboard_target_clicked', + }, + + fetch_data: function() { + // Overwrite this function with useful data + return $.Deferred().resolve(); + }, + + render: function() { + var super_render = this._super; + var self = this; + + this.fetch_data().then(function(result){ + + self.show_demo = !(result && result['nb_opportunities'] > 0); + + var sales_dashboard = QWeb.render('sales_team.SalesDashboard', { + show_demo: self.show_demo, + values: result, + }); + super_render.call(self); + $(sales_dashboard).prependTo(self.$el); + }); + }, + + on_dashboard_action_clicked: function(ev){ + ev.preventDefault(); + + var self = this; + var $action = $(ev.currentTarget); + var action_name = $action.attr('name'); + var additional_context = {} + + // TODO: find a better way to add defaults to search view + if (action_name === 'calendar.action_calendar_event') { + additional_context['search_default_mymeetings'] = 1; + } + if (action_name === 'crm.crm_lead_opportunities') { + additional_context['search_default_assigned_to_me'] = 1; + } + + new Model("ir.model.data") + .call("xmlid_to_res_id", [action_name]) + .then(function(data) { + if (data){ + self.do_action(data, {additional_context: additional_context}); + } + }); + }, + + on_change_input_target: function(e) { + + var self = this; + var $input = $(e.target); + var target_name = $input.attr('name'); + var target_value = $input.val(); + + if(isNaN($input.val())) { + this.do_warn(_t("Wrong value entered!"), _t("Only Integer Value should be valid.")); + } else { + this.modify_target(target_name, target_value).then(function() { + self.render(); + }); + } + }, + + modify_target: function(target_name, value){ + return new Model('crm.lead') + .call('modify_target_sales_dashboard', [target_name, value]) + + }, + + on_dashboard_target_clicked: function(ev){ + + if (this.show_demo) { + // The user is not allowed to modify the targets in demo mode + return; + } + + var self = this; + var $target = $(ev.currentTarget); + var target_name = $target.attr('name'); + var target_value = $target.attr('value'); + + var $input = $(''); + $input.attr('name', target_name); + if (target_value) { + $input.attr('value', target_value); + } + $input.on('keyup input', function(e) { + if(e.which === $.ui.keyCode.ENTER) { + self.on_change_input_target(e); + } + }); + $input.on('blur', function(e) { + self.on_change_input_target(e); + }); + $target.replaceWith($input); + $input.focus().select(); + }, +}); + +core.view_registry.add('sales_team_dashboard', SalesTeamDashboardView); + +return SalesTeamDashboardView + +}); diff --git a/addons/sales_team/static/src/less/sales_team_dashboard.less b/addons/sales_team/static/src/less/sales_team_dashboard.less new file mode 100644 index 00000000000..9bd4f900baf --- /dev/null +++ b/addons/sales_team/static/src/less/sales_team_dashboard.less @@ -0,0 +1,114 @@ +.o_kanban_view.o_kanban_dashboard.o_salesteam_kanban { + + padding: 0; // remove the padding of the kanban view + + .o_kanban_record { + min-width: 450px; + + @media (max-width: @grid-float-breakpoint) { + min-width: inherit; + } + } + + .o_sales_dashboard { + padding-top: 20px; + background-color: @odoo-view-background-color; + position: relative; + .o-flex(0, 0,100%); + .o-flex-display(); + + .o_welcome_message { + position: absolute; + left: 0; + right: 0; + .o-flex-display(); + justify-content: center; + + .o_welcome_image { + padding: 20px; + } + .o_welcome_content { + > a { + color: white; + display: inline-block; + } + } + } + + .o_demo { + opacity: 0.07; + } + + .o_left_panel { + .o-flex(1, 0, 0); + } + .o_right_panel { + .o-flex(1, 0, 0); + + @media (max-width: @grid-float-breakpoint) { + display: none; + } + } + + table { + -webkit-border-horizontal-spacing: 10px; + border-collapse: separate; + + > tbody { + > tr { + > td { + vertical-align: middle; + text-align: center; + border-top: 1px solid @odoo-view-background-color; + + span { + display: inline; + } + + .o_target_reached { + color: green; + } + + a:hover { + text-decoration: none; + } + + &.o_main { + background-color: @odoo-brand-optional; + &:hover { + background-color: darken(@odoo-brand-optional, 10%); + } + a { + color: white; + } + } + &.o_warning { + background-color: orange; + &:hover { + background-color: darken(orange, 10%); + } + a { + color: white; + } + } + &.o_secondary { + background-color: lightgrey; + &:hover { + background-color: darken(lightgrey, 10%); + } + a { + color: black; + } + } + &.o_highlight, .o_highlight { + font-size: 20px; + } + &.o_text { + text-align: left; + } + } + } + } + } + } +} diff --git a/addons/sales_team/static/src/xml/sales_team_dashboard.xml b/addons/sales_team/static/src/xml/sales_team_dashboard.xml new file mode 100644 index 00000000000..985f7ca0431 --- /dev/null +++ b/addons/sales_team/static/src/xml/sales_team_dashboard.xml @@ -0,0 +1,242 @@ + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + +
+ This Month + + + + + + + + 94 + +
+ Activities Done +
+
+ + + + + + + 78,140.03€ + +
+ Won in Opportunities +
+
+ + + + + + + 35,029.39€ + +
+ Invoiced +
+
Target + + + + + + Click to set + + + + 100 + + + + + + + + Click to set + + + + 80.000€ + + + + + + + + Click to set + + + + Click to set + +
Last Month + + + + + + + + + + + +
+
+ +
+
+ +
+
+

Hi there!

+

Great sales journeys start with a clean pipeline.

+

Have fun playing with Odoo CRM.

+ Your Pipeline +
+
+
+
+ +
diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 97b5fbe17fe..c780e70a088 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -205,6 +205,7 @@ class view(osv.osv): ('diagram','Diagram'), ('gantt', 'Gantt'), ('kanban', 'Kanban'), + ('sales_team_dashboard', 'Sales Team Dashboard'), ('search','Search'), ('qweb', 'QWeb')], string='View Type'), 'arch': fields.function(_arch_get, fnct_inv=_arch_set, string='View Architecture', type="text", nodrop=True),