[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 import tools
from openerp.osv import fields from openerp.osv import fields
from openerp.osv import osv from openerp.osv import osv
from openerp.tools.translate import _
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
MAX_LEVEL = 15 MAX_LEVEL = 15
AVAILABLE_STATES = [ AVAILABLE_STATES = [
@ -105,34 +105,55 @@ class crm_case_section(osv.osv):
_inherit = "mail.thread" _inherit = "mail.thread"
_description = "Sales Teams" _description = "Sales Teams"
_order = "complete_name" _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): def get_full_name(self, cr, uid, ids, field_name, arg, context=None):
return dict(self.name_get(cr, uid, ids, context=context)) 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): def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, context=None):
res = dict.fromkeys(ids, []) """ Generic method to generate data for bar chart values using SparklineBarWidget.
obj = self.pool.get('crm.lead') This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
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_won_opportunity_per_duration(self, cr, uid, ids, field_name, arg, context=None): :param obj: the target model (i.e. crm_lead)
res = dict.fromkeys(ids, []) :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') obj = self.pool.get('crm.lead')
today = date.today().replace(day=1) res = dict.fromkeys(ids, False)
begin = (today + relativedelta(months=-5)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT) month_begin = date.today().replace(day=1)
for section in self.browse(cr, uid, ids, context=context): groupby_begin = (month_begin + relativedelta.relativedelta(months=-4)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
domain = [("section_id", "=", section.id), '|', ('type', '=', 'opportunity'), ('date_open', '!=', None), ('create_date', '>=', begin)] for id in ids:
group_obj = obj.read_group(cr, uid, domain, ['planned_revenue', "create_date"], "create_date", context=context) res[id] = dict()
group_list = [group['planned_revenue'] for group in group_obj] lead_domain = [('type', '=', 'lead'), ('section_id', '=', id), ('create_date', '>=', groupby_begin)]
nb_month = group_obj and relativedelta(today, datetime.strptime(group_obj[-1]['__domain'][0][2], '%Y-%m-%d')).months or 0 res[id]['monthly_open_leads'] = self.__get_bar_values(cr, uid, obj, lead_domain, ['create_date'], 'create_date_count', 'create_date', context=context)
res[section.id] = [0]*(5 - len(group_list) - nb_month) + group_list + [0]*nb_month 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 return res
_columns = { _columns = {
@ -158,8 +179,12 @@ class crm_case_section(osv.osv):
'use_leads': fields.boolean('Leads', '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."), 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), 'monthly_open_leads': fields.function(_get_opportunities_data,
'won_opportunity_per_duration': fields.function(_get_won_opportunity_per_duration, string='Revenue of opporunities whon per duration', type="string", readonly=True) 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): def _get_stage_common(self, cr, uid, context):

View File

@ -78,8 +78,8 @@
<field name="note"/> <field name="note"/>
<field name="alias_id"/> <field name="alias_id"/>
<field name="color"/> <field name="color"/>
<field name="open_lead_per_duration"/> <field name="monthly_open_leads"/>
<field name="won_opportunity_per_duration"/> <field name="monthly_planned_revenue"/>
<templates> <templates>
<t t-name="kanban-box"> <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"> <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_items_list">
<div class="oe_salesteams_leads" t-if="record.use_leads.raw_value"> <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="%(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> <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"> </div>
<div class="oe_salesteams_opportunities">
<a name="%(crm_case_form_view_salesteams_opportunity)d" type="action">Opportunities</a> <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> </div>
</div> </div>
@ -182,15 +183,13 @@
</div> </div>
<group> <group>
<group> <group>
<field name="parent_id"/> <field name="user_id"/>
<field name="active"/> <field name="code"/>
</group> </group>
<group> <group>
<field name="user_id"/> <field name="parent_id"/>
<field name="change_responsible"/> <field name="change_responsible"/>
</group> <field name="active"/>
<group colspan="4" attrs="{'invisible': [('use_leads', '=', False)]}">
</group> </group>
</group> </group>
<notebook colspan="4"> <notebook colspan="4">

View File

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

View File

@ -217,10 +217,9 @@
<field name="inherit_id" ref="crm.crm_case_section_view_form"/> <field name="inherit_id" ref="crm.crm_case_section_view_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<data> <data>
<xpath expr="//group" position="inside"> <xpath expr="//field[@name='code']" position="after">
<group> <field name="invoiced_target"/>
<field name="target_invoice"/> <field name="invoiced_forecast"/>
</group>
</xpath> </xpath>
</data> </data>
</field> </field>
@ -233,30 +232,46 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<data> <data>
<xpath expr="//field[@name='name']" position="after"> <xpath expr="//field[@name='name']" position="after">
<field name="created_quotation_per_duration"/> <field name="monthly_quoted"/>
<field name="validate_saleorder_per_duration"/> <field name="monthly_confirmed"/>
<field name="sent_invoice_per_duration"/> <field name="monthly_invoiced"/>
<field name="invoiced_forecast"/>
<field name="forecast"/> <field name="invoiced_target"/>
<field name="target_invoice"/>
</xpath> </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="%(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> <a name="%(sale.action_order_report_all)d" type="action" class="oe_sparkline_bar_link">
</div></xpath> <field name="monthly_confirmed" widget="sparkline_bar">
<xpath expr="//div[@class='oe_salesteams_opportunities']" position="after"><div class="oe_salesteams_invoices"> Revenue of confirmed sales orders per month.<br/>Click the acces to Sales Analysis
<a name="%(action_invoice_salesteams)d" type="action" groups="account.group_account_invoice">Invoices</a> </field>
<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> </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>
</div> </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. <br/>Define an invoicing target in the sales team settings to see the period's achievement and forecast at a glance.
</div> </div>
</xpath> </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 label = this.options.label_field ? parent.record[this.options.label_field].raw_value : "";
var title = this.$node.html(); var title = this.$node.html();
var val = this.field.value; 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"); var unique_id = _.uniqueId("JustGage");
this.$el.empty() this.$el.empty()