[CLEAN] crm, sale_crm: cleaned code for new fields. Fixed some bugs in calculation and monthly repartition. Updated form views. Improved use of sparkline widget, now having tootips.

bzr revid: tde@openerp.com-20130531153100-q1cmpx0kjb02cbtu
This commit is contained in:
Thibault Delavallée 2013-05-31 17:31:00 +02:00
parent 71df2e6f7d
commit 09e0cd8dc6
6 changed files with 151 additions and 116 deletions

View File

@ -19,12 +19,12 @@
#
##############################################################################
from datetime import date, datetime
from dateutil import relativedelta
from openerp import tools
from openerp.osv import fields
from openerp.osv import osv
from openerp.tools.translate import _
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
MAX_LEVEL = 15
AVAILABLE_STATES = [
@ -105,34 +105,55 @@ class crm_case_section(osv.osv):
_inherit = "mail.thread"
_description = "Sales Teams"
_order = "complete_name"
# number of periods for lead/opportunities/... tracking in salesteam kanban dashboard/kanban view
_period_number = 5
def get_full_name(self, cr, uid, ids, field_name, arg, context=None):
return dict(self.name_get(cr, uid, ids, context=context))
def _get_open_lead_per_duration(self, cr, uid, ids, field_name, arg, context=None):
res = dict.fromkeys(ids, [])
obj = self.pool.get('crm.lead')
today = date.today().replace(day=1)
begin = (today + relativedelta(months=-5)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
for section in self.browse(cr, uid, ids, context=context):
domain = [("section_id", "=", section.id), '|', ('type', '=', 'lead'), ('date_open', '!=', None), ('create_date', '>=', begin)]
group_obj = obj.read_group(cr, uid, domain, ["create_date"], "create_date", context=context)
group_list = [group['create_date_count'] for group in group_obj]
nb_month = group_obj and relativedelta(today, datetime.strptime(group_obj[-1]['__domain'][0][2], '%Y-%m-%d')).months or 0
res[section.id] = [0]*(5 - len(group_list) - nb_month) + group_list + [0]*nb_month
return res
def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, context=None):
""" Generic method to generate data for bar chart values using SparklineBarWidget.
This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
def _get_won_opportunity_per_duration(self, cr, uid, ids, field_name, arg, context=None):
res = dict.fromkeys(ids, [])
:param obj: the target model (i.e. crm_lead)
:param domain: the domain applied to the read_group
:param list read_fields: the list of fields to read in the read_group
:param str value_field: the field used to compute the value of the bar slice
:param str groupby_field: the fields used to group
:return list section_result: a list of dicts: [
{ 'value': (int) bar_column_value,
'tootip': (str) bar_column_tooltip,
}
]
"""
month_begin = date.today().replace(day=1)
section_result = [{
'value': 0,
'tooltip': (month_begin + relativedelta.relativedelta(months=-i)).strftime('%B'),
} for i in range(self._period_number - 1, -1, -1)]
group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
for group in group_obj:
group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT)
month_delta = relativedelta.relativedelta(month_begin, group_begin_date)
section_result[self._period_number - (month_delta.months + 1)] = {'value': group.get(value_field, 0), 'tooltip': group_begin_date.strftime('%B')}
return section_result
def _get_opportunities_data(self, cr, uid, ids, field_name, arg, context=None):
""" Get opportunities-related data for salesteam kanban view
monthly_open_leads: number of open lead during the last months
monthly_planned_revenue: planned revenu of opportunities during the last months
"""
obj = self.pool.get('crm.lead')
today = date.today().replace(day=1)
begin = (today + relativedelta(months=-5)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
for section in self.browse(cr, uid, ids, context=context):
domain = [("section_id", "=", section.id), '|', ('type', '=', 'opportunity'), ('date_open', '!=', None), ('create_date', '>=', begin)]
group_obj = obj.read_group(cr, uid, domain, ['planned_revenue', "create_date"], "create_date", context=context)
group_list = [group['planned_revenue'] for group in group_obj]
nb_month = group_obj and relativedelta(today, datetime.strptime(group_obj[-1]['__domain'][0][2], '%Y-%m-%d')).months or 0
res[section.id] = [0]*(5 - len(group_list) - nb_month) + group_list + [0]*nb_month
res = dict.fromkeys(ids, False)
month_begin = date.today().replace(day=1)
groupby_begin = (month_begin + relativedelta.relativedelta(months=-4)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
for id in ids:
res[id] = dict()
lead_domain = [('type', '=', 'lead'), ('section_id', '=', id), ('create_date', '>=', groupby_begin)]
res[id]['monthly_open_leads'] = self.__get_bar_values(cr, uid, obj, lead_domain, ['create_date'], 'create_date_count', 'create_date', context=context)
opp_domain = [('type', '=', 'opportunity'), ('section_id', '=', id), ('create_date', '>=', groupby_begin)]
res[id]['monthly_planned_revenue'] = self.__get_bar_values(cr, uid, obj, opp_domain, ['planned_revenue', 'create_date'], 'planned_revenue', 'create_date', context=context)
return res
_columns = {
@ -158,8 +179,12 @@ class crm_case_section(osv.osv):
'use_leads': fields.boolean('Leads',
help="The first contact you get with a potential customer is a lead you qualify before converting it into a real business opportunity. Check this box to manage leads in this sales team."),
'open_lead_per_duration': fields.function(_get_open_lead_per_duration, string='Open Leads per duration', type="string", readonly=True),
'won_opportunity_per_duration': fields.function(_get_won_opportunity_per_duration, string='Revenue of opporunities whon per duration', type="string", readonly=True)
'monthly_open_leads': fields.function(_get_opportunities_data,
type="string", readonly=True, multi='_get_opportunities_data',
string='Open Leads per Month'),
'monthly_planned_revenue': fields.function(_get_opportunities_data,
type="string", readonly=True, multi='_get_opportunities_data',
string='Planned Revenue per Month')
}
def _get_stage_common(self, cr, uid, context):

View File

@ -78,8 +78,8 @@
<field name="note"/>
<field name="alias_id"/>
<field name="color"/>
<field name="open_lead_per_duration"/>
<field name="won_opportunity_per_duration"/>
<field name="monthly_open_leads"/>
<field name="monthly_planned_revenue"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_crm_salesteams">
@ -99,10 +99,11 @@
<div class="oe_items_list">
<div class="oe_salesteams_leads" t-if="record.use_leads.raw_value">
<a name="%(crm_case_form_view_salesteams_lead)d" type="action">Leads</a>
<a name="%(action_report_crm_lead)d" type="action" class="oe_sparkline_bar_link"><field name="open_lead_per_duration" widget="sparkline_bar">Number of opening leads per month.<br/>Click to see a detailed analysis of leads.</field></a>
</div><div class="oe_salesteams_opportunities">
<a name="%(action_report_crm_lead)d" type="action" class="oe_sparkline_bar_link"><field name="monthly_open_leads" widget="sparkline_bar">Open Leads per Month<br/>Click to see a detailed analysis of leads.</field></a>
</div>
<div class="oe_salesteams_opportunities">
<a name="%(crm_case_form_view_salesteams_opportunity)d" type="action">Opportunities</a>
<a name="%(action_report_crm_opportunity)d" type="action"><field name="won_opportunity_per_duration" widget="sparkline_bar">Revenue of won opportunities per month.<br/>Click to see a detailed analysis of opportunities.</field></a>
<a name="%(action_report_crm_opportunity)d" type="action"><field name="monthly_planned_revenue" widget="sparkline_bar">Planned Revenue per Month<br/>Click to see a detailed analysis of opportunities.</field></a>
</div>
</div>
</div>
@ -182,15 +183,13 @@
</div>
<group>
<group>
<field name="parent_id"/>
<field name="active"/>
<field name="user_id"/>
<field name="code"/>
</group>
<group>
<field name="user_id"/>
<field name="parent_id"/>
<field name="change_responsible"/>
</group>
<group colspan="4" attrs="{'invisible': [('use_leads', '=', False)]}">
<field name="active"/>
</group>
</group>
<notebook colspan="4">

View File

@ -15,7 +15,16 @@ openerp.crm = function(openerp) {
var self = this;
var title = this.$node.html();
setTimeout(function () {
self.$el.sparkline(self.field.value, {type: 'bar', barWidth: 5} );
var value = _.pluck(self.field.value, 'value');
var tooltips = _.pluck(self.field.value, 'tooltip');
self.$el.sparkline(value, {
type: 'bar',
barWidth: 5,
tooltipFormat: '{{offset:offset}} {{value}}',
tooltipValueLookups: {
'offset': tooltips
},
});
self.$el.tipsy({'delayIn': 0, 'html': true, 'title': function(){return title}, 'gravity': 'n'});
}, 0);
},

View File

@ -19,9 +19,10 @@
#
##############################################################################
from datetime import date, datetime
from datetime import date
from dateutil import relativedelta
from openerp import tools
from dateutil.relativedelta import relativedelta
from openerp.osv import osv, fields
@ -48,65 +49,51 @@ class sale_order(osv.osv):
class crm_case_section(osv.osv):
_inherit = 'crm.case.section'
def _get_created_quotation_per_duration(self, cr, uid, ids, field_name, arg, context=None):
res = dict.fromkeys(ids, [])
def _get_sale_orders_data(self, cr, uid, ids, field_name, arg, context=None):
obj = self.pool.get('sale.order')
today = date.today().replace(day=1)
begin = (today + relativedelta(months=-5)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
for section in self.browse(cr, uid, ids, context=context):
domain = [("section_id", "=", section.id), ('state', 'in', ['draft', 'sent']), ('date_order', '>=', begin)]
group_obj = obj.read_group(cr, uid, domain, ['amount_total', "date_order"], "date_order", context=context)
group_list = [group['amount_total'] for group in group_obj]
nb_month = group_obj and relativedelta(today, datetime.strptime(group_obj[-1]['__domain'][0][2], '%Y-%m-%d')).months or 0
res[section.id] = [0]*(5 - len(group_list) - nb_month) + group_list + [0]*nb_month
res = dict.fromkeys(ids, False)
month_begin = date.today().replace(day=1)
groupby_begin = (month_begin + relativedelta.relativedelta(months=-4)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
for id in ids:
res[id] = dict()
created_domain = [('section_id', '=', id), ('state', 'in', ['draft', 'sent']), ('date_order', '>=', groupby_begin)]
res[id]['monthly_quoted'] = self.__get_bar_values(cr, uid, obj, created_domain, ['amount_total', 'date_order'], 'amount_total', 'date_order', context=context)
validated_domain = [('section_id', '=', id), ('state', 'not in', ['draft', 'sent']), ('date_confirm', '>=', groupby_begin)]
res[id]['monthly_confirmed'] = self.__get_bar_values(cr, uid, obj, validated_domain, ['amount_total', 'date_confirm'], 'amount_total', 'date_confirm', context=context)
return res
def _get_validate_saleorder_per_duration(self, cr, uid, ids, field_name, arg, context=None):
res = dict.fromkeys(ids, [])
obj = self.pool.get('sale.order')
today = date.today().replace(day=1)
begin = (today + relativedelta(months=-5)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
for section in self.browse(cr, uid, ids, context=context):
domain = [("section_id", "=", section.id), ('state', 'not in', ['draft', 'sent']), ('date_confirm', '>=', begin)]
group_obj = obj.read_group(cr, uid, domain, ['amount_total', "date_confirm"], "date_confirm", context=context)
group_list = [group['amount_total'] for group in group_obj]
nb_month = group_obj and relativedelta(today, datetime.strptime(group_obj[-1]['__domain'][0][2], '%Y-%m-%d')).months or 0
res[section.id] = [0]*(5 - len(group_list) - nb_month) + group_list + [0]*nb_month
return res
def _get_sent_invoice_per_duration(self, cr, uid, ids, field_name, arg, context=None):
res = dict.fromkeys(ids, [])
def _get_invoices_data(self, cr, uid, ids, field_name, arg, context=None):
obj = self.pool.get('account.invoice.report')
today = date.today().replace(day=1)
begin = (today + relativedelta(months=-5)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
for section in self.browse(cr, uid, ids, context=context):
domain = [("section_id", "=", section.id), ('state', 'not in', ['draft', 'cancel']), ('date', '>=', begin)]
group_obj = obj.read_group(cr, uid, domain, ['price_total', "date"], "date", context=context)
group_list = [group['price_total'] for group in group_obj]
nb_month = group_obj and relativedelta(today, datetime.strptime(group_obj[-1]['__domain'][0][2], '%Y-%m-%d')).months or 0
res[section.id] = [0]*(5 - len(group_list) - nb_month) + group_list + [0]*nb_month
res = dict.fromkeys(ids, False)
month_begin = date.today().replace(day=1)
groupby_begin = (month_begin + relativedelta.relativedelta(months=-4)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
for id in ids:
created_domain = [('section_id', '=', id), ('state', 'not in', ['draft', 'cancel']), ('date', '>=', groupby_begin)]
res[id] = self.__get_bar_values(cr, uid, obj, created_domain, ['price_total', 'date'], 'price_total', 'date', context=context)
return res
_columns = {
'quotation_ids': fields.one2many('sale.order', 'section_id',
string='Quotations', readonly=True,
domain=[('state', 'in', ['draft', 'sent', 'cancel'])]),
'sale_order_ids': fields.one2many('sale.order', 'section_id',
string='Sale Orders', readonly=True,
domain=[('state', 'not in', ['draft', 'sent', 'cancel'])]),
'invoice_ids': fields.one2many('account.invoice', 'section_id',
string='Invoices', readonly=True,
domain=[('state', 'not in', ['draft', 'cancel'])]),
'forecast': fields.integer(string='Total forecast'),
'target_invoice': fields.integer(string='Invoicing Target'),
'created_quotation_per_duration': fields.function(_get_created_quotation_per_duration, string='Rate of created quotation per duration', type="string", readonly=True),
'validate_saleorder_per_duration': fields.function(_get_validate_saleorder_per_duration, string='Rate of validate sales orders per duration', type="string", readonly=True),
'sent_invoice_per_duration': fields.function(_get_sent_invoice_per_duration, string='Rate of sent invoices per duration', type="string", readonly=True),
'invoiced_forecast': fields.integer(string='Invoice Forecast',
help="Forecast of the invoice revenue for the current month. This is the amount the sales \n"
"team should invoice this month. It is used to compute the progression ratio \n"
" of the current and forecast revenue on the kanban view."),
'invoiced_target': fields.integer(string='Invoice Target',
help="Target of invoice revenue for the current month. This is the amount the sales \n"
"team estimates to be able to invoice this month."),
'monthly_quoted': fields.function(_get_sale_orders_data,
type='string', readonly=True, multi='_get_sale_orders_data',
string='Rate of created quotation per duration'),
'monthly_confirmed': fields.function(_get_sale_orders_data,
type='string', readonly=True, multi='_get_sale_orders_data',
string='Rate of validate sales orders per duration'),
'monthly_invoiced': fields.function(_get_invoices_data,
type='string', readonly=True,
string='Rate of sent invoices per duration'),
}
def action_forecast(self, cr, uid, id, value, context=None):
return self.write(cr, uid, [id], {'forecast': value}, context=context)
return self.write(cr, uid, [id], {'invoiced_forecast': value}, context=context)
class res_users(osv.Model):
_inherit = 'res.users'

View File

@ -217,10 +217,9 @@
<field name="inherit_id" ref="crm.crm_case_section_view_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//group" position="inside">
<group>
<field name="target_invoice"/>
</group>
<xpath expr="//field[@name='code']" position="after">
<field name="invoiced_target"/>
<field name="invoiced_forecast"/>
</xpath>
</data>
</field>
@ -233,30 +232,46 @@
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='name']" position="after">
<field name="created_quotation_per_duration"/>
<field name="validate_saleorder_per_duration"/>
<field name="sent_invoice_per_duration"/>
<field name="forecast"/>
<field name="target_invoice"/>
<field name="monthly_quoted"/>
<field name="monthly_confirmed"/>
<field name="monthly_invoiced"/>
<field name="invoiced_forecast"/>
<field name="invoiced_target"/>
</xpath>
<xpath expr="//div[@class='oe_salesteams_leads']" position="after"><div class="oe_salesteams_orders">
<xpath expr="//div[@class='oe_salesteams_leads']" position="after">
<div class="oe_salesteams_orders">
<a name="%(action_orders_salesteams)d" type="action">Sales Orders</a>
<a name="%(sale.action_order_report_all)d" type="action" class="oe_sparkline_bar_link"><field name="validate_saleorder_per_duration" widget="sparkline_bar">Revenue of confirmed sales orders per month.<br/>Click the acces to Sales Analysis</field></a>
</div></xpath>
<xpath expr="//div[@class='oe_salesteams_opportunities']" position="after"><div class="oe_salesteams_invoices">
<a name="%(action_invoice_salesteams)d" type="action" groups="account.group_account_invoice">Invoices</a>
<a name="%(account.action_account_invoice_report_all)d" type="action" class="oe_sparkline_bar_link"><field name="sent_invoice_per_duration" widget="sparkline_bar">Revenue of sent invoices per month.<br/>Click to see a detailed analysis of invoices.</field></a>
</div><div class="oe_salesteams_quotations">
<a name="%(action_quotations_salesteams)d" type="action" class="oe_sparkline_bar_link">Quotations</a>
<a name="%(sale.action_order_report_all)d" type="action" class="oe_sparkline_bar_link"><field name="created_quotation_per_duration" widget="sparkline_bar">Revenue of created quotation per month.<br/>Click to see a detailed analysis of sales.</field></a>
</div></xpath>
<xpath expr="//div[@class='oe_items_list']" position="after">
<div class="oe_center" t-if="record.target_invoice.raw_value">
<field name="sent_invoice_per_duration" widget="gage" style="width:160px; height: 120px;" options="{'max_field': 'target_invoice'}">Invoiced</field>
<field name="forecast" widget="gage" style="width:160px; height: 120px;" options="{'max_field': 'target_invoice', 'action_change': 'action_forecast'}">Forecast</field>
<a name="%(sale.action_order_report_all)d" type="action" class="oe_sparkline_bar_link">
<field name="monthly_confirmed" widget="sparkline_bar">
Revenue of confirmed sales orders per month.<br/>Click the acces to Sales Analysis
</field>
</a>
</div>
<div class="oe_center" style="color:#bbbbbb;" t-if="!record.target_invoice.raw_value">
</xpath>
<xpath expr="//div[@class='oe_salesteams_opportunities']" position="after">
<div class="oe_salesteams_invoices">
<a name="%(action_invoice_salesteams)d" type="action" groups="account.group_account_invoice">Invoices</a>
<a name="%(account.action_account_invoice_report_all)d" type="action" class="oe_sparkline_bar_link">
<field name="monthly_invoiced" widget="sparkline_bar">
Revenue of sent invoices per month.<br/>Click to see a detailed analysis of invoices.
</field>
</a>
</div>
<div class="oe_salesteams_quotations">
<a name="%(action_quotations_salesteams)d" type="action" class="oe_sparkline_bar_link">Quotations</a>
<a name="%(sale.action_order_report_all)d" type="action" class="oe_sparkline_bar_link">
<field name="monthly_quoted" widget="sparkline_bar">
Revenue of created quotation per month.<br/>Click to see a detailed analysis of sales.
</field>
</a>
</div>
</xpath>
<xpath expr="//div[@class='oe_items_list']" position="after">
<div class="oe_center" t-if="record.invoiced_target.raw_value">
<field name="monthly_invoiced" widget="gage" style="width:160px; height: 120px;" options="{'max_field': 'invoiced_target'}">Invoiced</field>
<field name="invoiced_forecast" widget="gage" style="width:160px; height: 120px;" options="{'max_field': 'invoiced_target', 'action_change': 'action_forecast'}">Forecast</field>
</div>
<div class="oe_center" style="color:#bbbbbb;" t-if="!record.invoiced_target.raw_value">
<br/>Define an invoicing target in the sales team settings to see the period's achievement and forecast at a glance.
</div>
</xpath>

View File

@ -11,7 +11,7 @@ openerp.sale_crm.GaugeWidget = openerp.web_kanban.AbstractField.extend({
var label = this.options.label_field ? parent.record[this.options.label_field].raw_value : "";
var title = this.$node.html();
var val = this.field.value;
var value = _.isArray(val) && val.length ? val[val.length-1] : val;
var value = _.isArray(val) && val.length ? val[val.length-1]['value'] : val;
var unique_id = _.uniqueId("JustGage");
this.$el.empty()