[ADD] sales_team: new sales dashboard with figures

A new view type has been created : sales_dashboard.
It is used to display some figure in the sales_team_dashboard.

Also added in this commit : 3 fields on res.users to fix sales target
	- target of won opportunities
	- target of activities done
	- target of invoiced sale orders
This commit is contained in:
Martin Geubelle
2015-08-28 14:06:12 +02:00
parent 39bc7871e1
commit de5366ac33
15 changed files with 711 additions and 7 deletions
+1
View File
@@ -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
+136 -1
View File
@@ -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"
+13
View File
@@ -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'),
}
@@ -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', []);
}
});
});
+3
View File
@@ -6,6 +6,9 @@
<link rel="stylesheet" href="/crm/static/src/css/crm.css"/>
<!-- Planner assets -->
<script type="text/javascript" src="/crm/static/src/js/web_planner_crm.js"></script>
<!-- Salesteam dashboard asset -->
<script type="text/javascript" src="/crm/static/src/js/sales_team_dashboard.js"></script>
</xpath>
</template>
</data>
+1
View File
@@ -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
+29 -1
View File
@@ -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
+12
View File
@@ -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'),
}
+3
View File
@@ -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,
+3 -1
View File
@@ -30,7 +30,7 @@
<field name="name">Sales Teams</field>
<field name="res_model">crm.team</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="view_mode">sales_team_dashboard</field>
<field name="context">{}</field>
<field name="view_id" ref="crm_team_salesteams_search"/>
<field name="help" type="html">
@@ -144,7 +144,9 @@
<template id="assets_backend" name="sales_team assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/sales_team/static/src/css/sales_team.css"/>
<link rel="stylesheet" href="/sales_team/static/src/less/sales_team_dashboard.less"/>
<script type="text/javascript" src="/sales_team/static/src/js/sales_team.js"></script>
<script type="text/javascript" src="/sales_team/static/src/js/sales_team_dashboard.js"></script>
</xpath>
</template>
+12 -4
View File
@@ -1,10 +1,10 @@
<odoo>
<!-- Case Teams Salesteams dashboard view -->
<record id="crm_team_salesteams_view_kanban" model="ir.ui.view" >
<field name="name">crm.team.kanban</field>
<field name="name">crm.team.dashboard</field>
<field name="model">crm.team</field>
<field name="arch" type="xml">
<kanban class="oe_background_grey o_kanban_dashboard o_salesteam_kanban" create="0">
<sales_team_dashboard class="oe_background_grey o_kanban_dashboard o_salesteam_kanban" create="0">
<field name="name"/>
<field name="user_id"/>
<field name="member_ids"/>
@@ -58,7 +58,15 @@
</div>
</t>
</templates>
</kanban>
</sales_team_dashboard>
</field>
</record>
</odoo>
<record id="action_sales_team_dashboard" model="ir.actions.client">
<field name="name">Sales Team Dashboard</field>
<field name="res_model">crm.team</field>
<field name="tag">sales_team_dashboard</field>
</record>
<menuitem action="action_sales_team_dashboard" id="menu_sales_team_dashboard" parent="base.menu_sales" sequence="20" groups="base.group_sale_salesman,base.group_sale_manager"/>
</odoo>
@@ -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/>');
$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
});
@@ -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;
}
}
}
}
}
}
}
@@ -0,0 +1,242 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<!--
This template is the rubbon at the top of the salesteam dashboard that adds
some figures to it. We call this rubbon the "SalesDashboard".
-->
<t t-name="sales_team.SalesDashboard">
<div class="o_sales_dashboard">
<div t-attf-class="o_left_panel #{show_demo ? 'o_demo' : ''}">
<table border="0" class="table">
<tr>
<td class="o_highlight o_text">
Today
</td>
<td class="o_main" title="To Calendar">
<a href="#" class="o_dashboard_action" name="calendar.action_calendar_event">
<span class="o_highlight">
<t t-if="!show_demo">
<t t-esc="values['meeting']['today']"/>
</t>
<t t-if="show_demo">
0
</t>
</span><br/>
Meetings
</a>
</td>
<td class="o_main" title="To Activities">
<a href="#" class="o_dashboard_action" name="crm.crm_lead_action_activities">
<span class="o_highlight">
<t t-if="!show_demo">
<t t-esc="values['activity']['today']"/>
</t>
<t t-if="show_demo">
7
</t>
</span> <br/>
Next Actions
</a>
</td>
<td class="o_main" title="To Opportunities">
<a href="#" class="o_dashboard_action" name="crm.crm_lead_opportunities">
<span class="o_highlight">
<t t-if="!show_demo">
<t t-esc="values['closing']['today']"/>
</t>
<t t-if="show_demo">
4
</t>
</span> <br/>
Expected Closing
</a>
</td>
</tr>
<tr>
<td></td>
<td></td>
<!-- Hide overdue when there is none -->
<td t-if="show_demo or (values and values['activity']['overdue'] != 0)" class="o_warning" title="To Activities">
<a href="#" class="o_dashboard_action" name="crm.crm_lead_action_activities">
<t t-if="!show_demo">
<t t-esc="values['activity']['overdue']"/> overdue
</t>
<t t-if="show_demo">
2
</t>
</a>
</td>
<td t-if="show_demo or (values and values['closing']['overdue'] != 0)" class="o_warning" title="To Opportunities">
<a href="#" class="o_dashboard_action" name="crm.crm_lead_opportunities">
<t t-if="!show_demo">
<t t-esc="values['closing']['overdue']"/> overdue
</t>
<t t-if="show_demo">
3
</t>
</a>
</td>
</tr>
<tr>
<td class="o_text">Next 7 days</td>
<td class="o_main" title="To Calendar">
<a href="#" class="o_dashboard_action" name="calendar.action_calendar_event">
<t t-if="!show_demo">
<t t-esc="values['meeting']['next_7_days']"/>
</t>
<t t-if="show_demo">
9
</t>
</a>
</td>
<td class="o_main" title="To Activities">
<a href="#" class="o_dashboard_action" name="crm.crm_lead_action_activities">
<t t-if="!show_demo">
<t t-esc="values['activity']['next_7_days']"/>
</t>
<t t-if="show_demo">
28
</t>
</a>
</td>
<td class="o_main" title="To Opportunities">
<a href="#" class="o_dashboard_action" name="crm.crm_lead_opportunities">
<t t-if="!show_demo">
<t t-esc="values['closing']['next_7_days']"/>
</t>
<t t-if="show_demo">
17
</t>
</a>
</td>
</tr>
</table>
</div>
<div t-attf-class="o_right_panel #{show_demo ? 'o_demo' : ''}">
<table class="table">
<tr>
<td class="o_highlight o_text">
This Month
</td>
<td class="o_secondary" title="To Activity Report">
<a href="#" class="o_dashboard_action" name="crm.crm_activity_report_action">
<span t-attf-class="o_highlight #{values and values['done']['target'] and values['done']['this_month'] >= values['done']['target'] ? 'o_target_reached' : ''}">
<t t-if="!show_demo">
<t t-esc="values['done']['this_month']"/>
</t>
<t t-if="show_demo">
94
</t>
</span> <br/>
Activities Done
</a>
</td>
<td class="o_secondary" title="To Opportunity Report">
<a href="#" class="o_dashboard_action" name="crm.crm_opportunity_report_action">
<span t-attf-class="o_highlight #{values and values['won']['target'] and values['won']['this_month'] >= values['won']['target'] ? 'o_target_reached' : ''}">
<t t-if="!show_demo">
<t t-esc="values['won']['this_month']"/>
</t>
<t t-if="show_demo" class="o_target_reached">
78,140.03€
</t>
</span> <br/>
Won in Opportunities
</a>
</td>
<td t-if="values['invoiced']" class="o_secondary" title="To Invoice Report">
<a href="#" class="o_dashboard_action" name="account.action_account_invoice_report_all">
<span t-attf-class="o_highlight #{values and values['invoiced']['target'] and values['invoiced']['this_month'] >= values['invoiced']['target'] ? 'o_target_reached' : ''}">
<t t-if="!show_demo">
<t t-esc="values['invoiced']['this_month']"/>
</t>
<t t-if="show_demo">
35,029.39€
</t>
</span> <br/>
Invoiced
</a>
</td>
</tr>
<tr>
<td class="o_text">Target</td>
<td class="o_secondary">
<span t-if="!show_demo" class="o_target_to_set" name='done' t-att-value="values['done']['target'] ? values['done']['target'] : undefined" title="Click to set">
<t t-if="values['done']['target']">
<t t-esc="values['done']['target']"/>
</t>
<t t-if="!values['done']['target']">
Click to set
</t>
</span>
<span t-if="show_demo">
100
</span>
</td>
<td class="o_secondary">
<span t-if="!show_demo" class="o_target_to_set" name='won' t-att-value="values['won']['target'] ? values['won']['target'] : undefined" title="Click to set">
<t t-if="values['won']['target']">
<t t-esc="values['won']['target']"/>
</t>
<t t-if="!values['won']['target']">
Click to set
</t>
</span>
<span t-if="show_demo">
80.000€
</span>
</td>
<td t-if="show_demo or values['invoiced']" class="o_secondary">
<span t-if="!show_demo" class="o_target_to_set" name='invoiced' t-att-value="values['invoiced']['target'] ? values['invoiced']['target'] : undefined" title="Click to set">
<t t-if="values['invoiced']['target']">
<t t-esc="values['invoiced']['target']"/>
</t>
<t t-if="!values['invoiced']['target']">
Click to set
</t>
</span>
<span t-if="show_demo">
Click to set
</span>
</td>
</tr>
<tr>
<td class="o_text">Last Month</td>
<td class="o_secondary" title="To Activity Report">
<a href="#" class="o_dashboard_action" name="crm.crm_activity_report_action">
<t t-esc="values['done']['last_month']"/>
</a>
</td>
<td class="o_secondary" title="To Opportunity Report">
<a href="#" class="o_dashboard_action" name="crm.crm_opportunity_report_action">
<t t-esc="values['won']['last_month']"/>
</a>
</td>
<td t-if="values['invoiced']" class="o_secondary" title="To Invoice Report">
<a href="#" class="o_dashboard_action" name="account.action_account_invoice_report_all">
<t t-esc="values['invoiced']['last_month']"/>
</a>
</td>
</tr>
</table>
</div>
<div t-if="show_demo" class="o_welcome_message">
<div class="o_welcome_image">
<i class="fa fa-smile-o fa-5x"></i>
</div>
<div class="o_welcome_content">
<h2>Hi there!</h2>
<h4>Great sales journeys start with a clean pipeline.</h4>
<h4>Have fun playing with Odoo CRM.</h4>
<a class="btn btn-primary o_dashboard_action" name="crm.crm_lead_opportunities">Your Pipeline</a>
</div>
</div>
</div>
</t>
</templates>
+1
View File
@@ -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),