[MERGE] Sync with trunk
[odoo/odoo.git] / addons / crm / crm.py
index 9549e75..d119450 100644 (file)
 #
 ##############################################################################
 
-import base64
-import time
-from lxml import etree
-from osv import fields
-from osv import osv
-import tools
-from tools.translate import _
+from datetime import date, datetime
+from dateutil import relativedelta
+
+from openerp import tools
+from openerp.osv import fields
+from openerp.osv import osv
 
 MAX_LEVEL = 15
 AVAILABLE_STATES = [
@@ -58,7 +57,7 @@ class crm_case_channel(osv.osv):
 
 class crm_case_stage(osv.osv):
     """ Model for case stages. This models the main stages of a document
-        management flow. Main CRM objects (leads, opportunities, project 
+        management flow. Main CRM objects (leads, opportunities, project
         issues, ...) will now use only stages, instead of state and stages.
         Stages are for example used to display the kanban view of records.
     """
@@ -75,10 +74,12 @@ class crm_case_stage(osv.osv):
         'requirements': fields.text('Requirements'),
         'section_ids':fields.many2many('crm.case.section', 'section_stage_rel', 'stage_id', 'section_id', string='Sections',
                         help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."),
-        'state': fields.selection(AVAILABLE_STATES, 'State', required=True, help="The related state for the stage. The state of your document will automatically change regarding the selected stage. For example, if a stage is related to the state 'Close', when your document reaches this stage, it will be automatically have the 'closed' state."),
-        'case_default': fields.boolean('Common to All Teams',
+        'state': fields.selection(AVAILABLE_STATES, 'Related Status', required=True,
+            help="The status of your document will automatically change regarding the selected stage. " \
+                "For example, if a stage is related to the status 'Close', when your document reaches this stage, it is automatically closed."),
+        'case_default': fields.boolean('Default to New Sales Team',
                         help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."),
-        'fold': fields.boolean('Hide in Views when Empty',
+        'fold': fields.boolean('Fold by Default',
                         help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."),
         'type': fields.selection([  ('lead','Lead'),
                                     ('opportunity', 'Opportunity'),
@@ -90,20 +91,70 @@ class crm_case_stage(osv.osv):
     _defaults = {
         'sequence': lambda *args: 1,
         'probability': lambda *args: 0.0,
-        'state': 'draft',
+        'state': 'open',
         'fold': False,
         'type': 'both',
+        'case_default': True,
     }
 
+
 class crm_case_section(osv.osv):
     """ Model for sales teams. """
     _name = "crm.case.section"
     _inherits = {'mail.alias': 'alias_id'}
+    _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))
+        return dict(self.name_get(cr, uid, ids, context=context))
+
+    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).
+
+            :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')
+        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 = {
         'name': fields.char('Sales Team', size=64, required=True, translate=True),
@@ -111,30 +162,39 @@ class crm_case_section(osv.osv):
         'code': fields.char('Code', size=8),
         'active': fields.boolean('Active', help="If the active field is set to "\
                         "true, it will allow you to hide the sales team without removing it."),
-        'allow_unlink': fields.boolean('Allow Delete', help="Allows to delete non draft cases"),
-        'change_responsible': fields.boolean('Reassign Escalated', help="When escalating to this team override the saleman with the team leader."),
+        'change_responsible': fields.boolean('Reassign Escalated', help="When escalating to this team override the salesman with the team leader."),
         'user_id': fields.many2one('res.users', 'Team Leader'),
-        'member_ids':fields.many2many('res.users', 'sale_member_rel', 'section_id', 'member_id', 'Team Members'),
+        'member_ids': fields.many2many('res.users', 'sale_member_rel', 'section_id', 'member_id', 'Team Members'),
         'reply_to': fields.char('Reply-To', size=64, help="The email address put in the 'Reply-To' of all emails sent by OpenERP about cases in this sales team"),
         'parent_id': fields.many2one('crm.case.section', 'Parent Team'),
         'child_ids': fields.one2many('crm.case.section', 'parent_id', 'Child Teams'),
         'resource_calendar_id': fields.many2one('resource.calendar', "Working Time", help="Used to compute open days"),
         'note': fields.text('Description'),
-        'working_hours': fields.float('Working Hours', digits=(16,2 )),
+        'working_hours': fields.float('Working Hours', digits=(16, 2)),
         'stage_ids': fields.many2many('crm.case.stage', 'section_stage_rel', 'section_id', 'stage_id', 'Stages'),
-        'alias_id': fields.many2one('mail.alias', 'Mail Alias', ondelete="cascade", required=True, 
-                                    help="This Unique Mail Box Alias of the Sales Team allows to manage the Seamless email communication between Mail Box and OpenERP,"
-                                         "This Alias MailBox also create and Manage the new Email Leads for this Sales Team and also manage the existing Lead email communication."),
+        'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="cascade", required=True,
+                                    help="The email address associated with this team. New emails received will automatically "
+                                         "create new leads assigned to the team."),
+        'color': fields.integer('Color Index'),
+        '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."),
+
+        '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):
-        ids = self.pool.get('crm.case.stage').search(cr, uid, [('case_default','=',1)], context=context)
+        ids = self.pool.get('crm.case.stage').search(cr, uid, [('case_default', '=', 1)], context=context)
         return ids
 
     _defaults = {
-        'active': lambda *a: 1,
-        'allow_unlink': lambda *a: 1,
-        'stage_ids': _get_stage_common
+        'active': 1,
+        'stage_ids': _get_stage_common,
+        'use_leads': True,
     }
 
     _sql_constraints = [
@@ -147,7 +207,7 @@ class crm_case_section(osv.osv):
 
     def name_get(self, cr, uid, ids, context=None):
         """Overrides orm name_get method"""
-        if not isinstance(ids, list) :
+        if not isinstance(ids, list):
             ids = [ids]
         res = []
         if not ids:
@@ -160,26 +220,22 @@ class crm_case_section(osv.osv):
                 name = record['parent_id'][1] + ' / ' + name
             res.append((record['id'], name))
         return res
-        
-    def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
-        res = super(crm_case_section,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
-        if view_type == 'form':
-            domain = self.pool.get("ir.config_parameter").get_param(cr, uid, "mail.catchall.domain", context=context)
-            if not domain:
-                doc = etree.XML(res['arch'])
-                alias_node = doc.xpath("//div[@name='alias_box']")[0]
-                parent = alias_node.getparent()
-                parent.remove(alias_node)
-                res['arch'] = etree.tostring(doc)
-        return res
-    
+
     def create(self, cr, uid, vals, context=None):
-        alias_pool = self.pool.get('mail.alias')
-        if not vals.get('alias_id'):
-            alias_id = alias_pool.create_unique_alias(cr, uid, {'alias_name': "sales_team."+vals['name'], 'alias_model_id': self._name}, context=context)
-            vals.update({'alias_id': alias_id})
-        res = super(crm_case_section, self).create(cr, uid, vals, context)
-        alias_pool.write(cr, uid, [vals['alias_id']],{'alias_defaults':{'section_id': res,'type':'lead'}},context)
+        if context is None:
+            context = {}
+        create_context = dict(context, alias_model_name='crm.lead', alias_parent_model_name=self._name)
+        section_id = super(crm_case_section, self).create(cr, uid, vals, context=create_context)
+        section = self.browse(cr, uid, section_id, context=context)
+        self.pool.get('mail.alias').write(cr, uid, [section.alias_id.id], {'alias_parent_thread_id': section_id, 'alias_defaults': {'section_id': section_id, 'type': 'lead'}}, context=context)
+        return section_id
+
+    def unlink(self, cr, uid, ids, context=None):
+        # Cascade-delete mail aliases as well, as they should not exist without the sales team.
+        mail_alias = self.pool.get('mail.alias')
+        alias_ids = [team.alias_id.id for team in self.browse(cr, uid, ids, context=context) if team.alias_id]
+        res = super(crm_case_section, self).unlink(cr, uid, ids, context=context)
+        mail_alias.unlink(cr, uid, alias_ids, context=context)
         return res
 
 class crm_case_categ(osv.osv):
@@ -191,13 +247,12 @@ class crm_case_categ(osv.osv):
         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
         'object_id': fields.many2one('ir.model', 'Object Name'),
     }
-
     def _find_object_id(self, cr, uid, context=None):
         """Finds id for case object"""
-        object_id = context and context.get('object_id', False) or False
-        ids = self.pool.get('ir.model').search(cr, uid, [('id', '=', object_id)])
+        context = context or {}
+        object_id = context.get('object_id', False)
+        ids = self.pool.get('ir.model').search(cr, uid, ['|',('id', '=', object_id),('model', '=', context.get('object_name', False))])
         return ids and ids[0] or False
-
     _defaults = {
         'object_id' : _find_object_id
     }
@@ -212,7 +267,6 @@ class crm_case_resource_type(osv.osv):
         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
     }
 
-
 def _links_get(self, cr, uid, context=None):
     """Gets links value for reference field"""
     obj = self.pool.get('res.request.link')
@@ -220,4 +274,14 @@ def _links_get(self, cr, uid, context=None):
     res = obj.read(cr, uid, ids, ['object', 'name'], context)
     return [(r['object'], r['name']) for r in res]
 
+class crm_payment_mode(osv.osv):
+    """ Payment Mode for Fund """
+    _name = "crm.payment.mode"
+    _description = "CRM Payment Mode"
+    _columns = {
+        'name': fields.char('Name', size=64, required=True),
+        'section_id': fields.many2one('crm.case.section', 'Sales Team'),
+    }
+
+
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: