diff --git a/addons/crm/crm.py b/addons/crm/crm.py index 5866167ee8b..4c9a60634a5 100644 --- a/addons/crm/crm.py +++ b/addons/crm/crm.py @@ -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): diff --git a/addons/crm/crm_case_section_view.xml b/addons/crm/crm_case_section_view.xml index 7f33d7c2b5c..c73390cdb71 100644 --- a/addons/crm/crm_case_section_view.xml +++ b/addons/crm/crm_case_section_view.xml @@ -78,8 +78,8 @@ - - + + @@ -182,15 +183,13 @@ - - + + - + - - - + diff --git a/addons/crm/static/src/js/crm_case_section.js b/addons/crm/static/src/js/crm_case_section.js index 656e3680dec..2b4ca741d75 100644 --- a/addons/crm/static/src/js/crm_case_section.js +++ b/addons/crm/static/src/js/crm_case_section.js @@ -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); }, diff --git a/addons/sale_crm/sale_crm.py b/addons/sale_crm/sale_crm.py index b753438ee60..6a3e13f7016 100644 --- a/addons/sale_crm/sale_crm.py +++ b/addons/sale_crm/sale_crm.py @@ -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' diff --git a/addons/sale_crm/sale_crm_view.xml b/addons/sale_crm/sale_crm_view.xml index 32ddad3bd8f..2ae8bb396a7 100644 --- a/addons/sale_crm/sale_crm_view.xml +++ b/addons/sale_crm/sale_crm_view.xml @@ -217,10 +217,9 @@ - - - - + + + @@ -233,30 +232,46 @@ - - - - - - + + + + + -
+ + - - - -
+ + + + + + +
+ Invoiced + Forecast +
+

Define an invoicing target in the sales team settings to see the period's achievement and forecast at a glance.
diff --git a/addons/sale_crm/static/src/js/sale_crm.js b/addons/sale_crm/static/src/js/sale_crm.js index c0dc4dc56d3..207ac959af3 100644 --- a/addons/sale_crm/static/src/js/sale_crm.js +++ b/addons/sale_crm/static/src/js/sale_crm.js @@ -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()