[REF] crm_phonecall: refactored case management (directly in write + clickable status...
[odoo/odoo.git] / addons / crm / crm_lead.py
index 7c7dcfe..3fe9e13 100644 (file)
@@ -2,7 +2,7 @@
 ##############################################################################
 #
 #    OpenERP, Open Source Management Solution
-#    Copyright (C) 2004-Today OpenERP SA (<http://www.openerp.com>)
+#    Copyright (C) 2004-today OpenERP SA (<http://www.openerp.com>)
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU Affero General Public License as
 #
 ##############################################################################
 
+import crm
 from datetime import datetime
 from operator import itemgetter
-import time
 
 from openerp import SUPERUSER_ID
 from openerp import tools
-from openerp.addons.crm import crm
-from openerp.addons.base_status.base_stage import base_stage
 from openerp.addons.base.res.res_partner import format_address
 from openerp.osv import fields, osv, orm
 from openerp.tools import html2plaintext
@@ -64,7 +62,7 @@ CRM_LEAD_FIELDS_TO_MERGE = ['name',
     'partner_name']
 
 
-class crm_lead(base_stage, format_address, osv.osv):
+class crm_lead(format_address, osv.osv):
     """ CRM Lead Case """
     _name = "crm.lead"
     _description = "Lead/Opportunity"
@@ -77,7 +75,7 @@ class crm_lead(base_stage, format_address, osv.osv):
             'crm.mt_lead_stage': lambda self, cr, uid, obj, ctx=None: (obj.stage_id and obj.stage_id.sequence != 1) and obj.probability < 100,
             'crm.mt_lead_won': lambda self, cr, uid, obj, ctx=None: obj.probability == 100 and obj.stage_id and obj.stage_id.on_change,
             'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.sequence != 1,
-        }
+        },
     }
 
     def get_empty_list_help(self, cr, uid, help, context=None):
@@ -87,24 +85,15 @@ class crm_lead(base_stage, format_address, osv.osv):
         context['empty_list_help_document_name'] = _("leads")
         return super(crm_lead, self).get_empty_list_help(cr, uid, help, context=context)
 
-    def create(self, cr, uid, vals, context=None):
-        if context is None:
-            context = {}
-        if not vals.get('stage_id'):
-            ctx = context.copy()
-            if vals.get('section_id'):
-                ctx['default_section_id'] = vals['section_id']
-            if vals.get('type'):
-                ctx['default_type'] = vals['type']
-            vals['stage_id'] = self._get_default_stage_id(cr, uid, context=ctx)
-        # context: no_log, because subtype already handle this
-        create_context = dict(context, mail_create_nolog=True)
-        return super(crm_lead, self).create(cr, uid, vals, context=create_context)
-
     def _get_default_section_id(self, cr, uid, context=None):
         """ Gives default section by checking if present in the context """
         return self._resolve_section_id_from_context(cr, uid, context=context) or False
 
+    def _get_default_stage_id(self, cr, uid, context=None):
+        """ Gives default stage_id """
+        section_id = self._get_default_section_id(cr, uid, context=context)
+        return self.stage_find(cr, uid, [], section_id, [('sequence', '=', '1')], context=context)
+
     def _resolve_section_id_from_context(self, cr, uid, context=None):
         """ Returns ID of section based on the value of 'section_id'
             context key, or None if it cannot be resolved to a single
@@ -115,8 +104,7 @@ class crm_lead(base_stage, format_address, osv.osv):
         if type(context.get('default_section_id')) in (int, long):
             return context.get('default_section_id')
         if isinstance(context.get('default_section_id'), basestring):
-            section_name = context['default_section_id']
-            section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=section_name, context=context)
+            section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
             if len(section_ids) == 1:
                 return int(section_ids[0][0])
         return None
@@ -238,7 +226,7 @@ class crm_lead(base_stage, format_address, osv.osv):
         'section_id': fields.many2one('crm.case.section', 'Sales Team',
                         select=True, track_visibility='onchange', help='When sending mails, the default email address is taken from the sales team.'),
         'create_date': fields.datetime('Creation Date', readonly=True),
-        'email_cc': fields.text('Global CC', size=252, help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
+        'email_cc': fields.text('Global CC', help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
         'description': fields.text('Notes'),
         'write_date': fields.datetime('Update Date', readonly=True),
         'categ_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Categories', \
@@ -247,21 +235,23 @@ class crm_lead(base_stage, format_address, osv.osv):
             domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
         'contact_name': fields.char('Contact Name', size=64),
-        'partner_name': fields.char("Customer Name", size=64, help='The name of the future partner company that will be created while converting the lead into opportunity', select=1),
+        'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner company that will be created while converting the lead into opportunity', select=1),
         'opt_out': fields.boolean('Opt-Out', oldname='optout',
             help="If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. "
                     "Filter 'Available for Mass Mailing' allows users to filter the leads when performing mass mailing."),
-        'type': fields.selection([('lead', 'Lead'), ('opportunity', 'Opportunity'), ], 'Type', help="Type is used to separate Leads and Opportunities"),
+        'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
         'date_closed': fields.datetime('Closed', readonly=True),
-        'stage_id': fields.many2one('crm.case.stage', 'Stage', track_visibility='onchange',),
+        'stage_id': fields.many2one('crm.case.stage', 'Stage', track_visibility='onchange',
+                        domain="['&', ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"),
         'user_id': fields.many2one('res.users', 'Salesperson', select=True, track_visibility='onchange'),
         'referred': fields.char('Referred By', size=64),
-        'date_open': fields.datetime('Opened', readonly=True),
+        'date_open': fields.datetime('Assigned', readonly=True),
         'day_open': fields.function(_compute_day, string='Days to Open', \
                                 multi='day_open', type="float", store=True),
         'day_close': fields.function(_compute_day, string='Days to Close', \
                                 multi='day_close', type="float", store=True),
+        'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
 
         # Only used for type opportunity
         'probability': fields.float('Success Rate (%)', group_operator="avg"),
@@ -300,13 +290,13 @@ class crm_lead(base_stage, format_address, osv.osv):
     _defaults = {
         'active': 1,
         'type': 'lead',
-        'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
-        'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
+        'user_id': lambda s, cr, uid, c: uid,
         'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
         'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
         'color': 0,
+        'date_last_stage_update': fields.datetime.now(),
     }
 
     _sql_constraints = [
@@ -321,6 +311,24 @@ class crm_lead(base_stage, format_address, osv.osv):
             return {'value': {}}
         return {'value': {'probability': stage.probability}}
 
+    def on_change_partner_id(self, cr, uid, ids, partner_id, context=None):
+        values = {}
+        if partner_id:
+            partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
+            values = {
+                'partner_name': partner.name,
+                'street': partner.street,
+                'street2': partner.street2,
+                'city': partner.city,
+                'state_id': partner.state_id and partner.state_id.id or False,
+                'country_id': partner.country_id and partner.country_id.id or False,
+                'email_from': partner.email,
+                'phone': partner.phone,
+                'mobile': partner.mobile,
+                'fax': partner.fax,
+            }
+        return {'value': values}
+
     def on_change_user(self, cr, uid, ids, user_id, context=None):
         """ When changing the user, also set a section_id or restrict section id
             to the ones user_id is member of. """
@@ -331,22 +339,6 @@ class crm_lead(base_stage, format_address, osv.osv):
                 section_id = section_ids[0]
         return {'value': {'section_id': section_id}}
 
-    def _check(self, cr, uid, ids=False, context=None):
-        """ Override of the base.stage method.
-            Function called by the scheduler to process cases for date actions
-            Avoid won cases
-        """
-        cr.execute('select * from crm_case \
-                where (date_action_last<%s or date_action_last is null) \
-                and (date_action_next<=%s or date_action_next is null) \
-                and probability not in (100)',
-                (time.strftime("%Y-%m-%d %H:%M:%S"),
-                    time.strftime('%Y-%m-%d %H:%M:%S')))
-
-        ids2 = map(lambda x: x[0], cr.fetchall() or [])
-        cases = self.browse(cr, uid, ids2, context=context)
-        return self._action(cr, uid, cases, False, context=context)
-
     def stage_find(self, cr, uid, cases, section_id, domain=None, order='sequence', context=None):
         """ Override of the base.stage method
             Parameter of the stage search taken from the lead:
@@ -388,29 +380,41 @@ class crm_lead(base_stage, format_address, osv.osv):
         return False
 
     def case_mark_lost(self, cr, uid, ids, context=None):
-        """ Mark the case as lost: stage with probability=0, on_change=True """
+        """ Mark the case as lost: state=cancel and probability=0 """
         for lead in self.browse(cr, uid, ids):
             stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0), ('on_change', '=', True), ('sequence', '>', 1)], context=context)
             if stage_id:
-                self.case_set(cr, uid, [lead.id], new_stage_id=stage_id, context=context)
+                return self.write(cr, uid, [lead.id], {'stage_id': stage_id}, context=context)
             else:
                 raise osv.except_osv(_('Warning!'),
                     _('To relieve your sales pipe and group all Lost opportunities, configure one of your sales stage as follow:\n'
                         'probability = 0 %, select "Change Probability Automatically".\n'
                         'Create a specific stage or edit an existing one by editing columns of your opportunity pipe.'))
-        return True
 
     def case_mark_won(self, cr, uid, ids, context=None):
-        """ Mark the case as won: stage with probability=100, , on_change=True """
+        """ Mark the case as won: state=done and probability=100 """
         for lead in self.browse(cr, uid, ids):
-            stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0), ('on_change', '=', True)], context=context)
+            stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0), ('on_change', '=', True), ('sequence', '>', 1)], context=context)
             if stage_id:
-                self.case_set(cr, uid, [lead.id], new_stage_id=stage_id, context=context)
+                return self.write(cr, uid, [lead.id], {'stage_id': stage_id}, context=context)
             else:
                 raise osv.except_osv(_('Warning!'),
                     _('To relieve your sales pipe and group all Won opportunities, configure one of your sales stage as follow:\n'
                         'probability = 100 % and select "Change Probability Automatically".\n'
                         'Create a specific stage or edit an existing one by editing columns of your opportunity pipe.'))
+
+    def case_escalate(self, cr, uid, ids, context=None):
+        """ Escalates case to parent level """
+        for case in self.browse(cr, uid, ids, context=context):
+            data = {'active': True}
+            if case.section_id.parent_id:
+                data['section_id'] = case.section_id.parent_id.id
+                if case.section_id.parent_id.change_responsible:
+                    if case.section_id.parent_id.user_id:
+                        data['user_id'] = case.section_id.parent_id.user_id.id
+            else:
+                raise osv.except_osv(_('Error!'), _("You are already at the top level of your sales-team category.\nTherefore you cannot escalate furthermore."))
+            self.write(cr, uid, [case.id], data, context=context)
         return True
 
     def set_priority(self, cr, uid, ids, priority, context=None):
@@ -583,7 +587,7 @@ class crm_lead(base_stage, format_address, osv.osv):
                 attachment.write(values)
         return True
 
-    def merge_opportunity(self, cr, uid, ids, context=None):
+    def merge_opportunity(self, cr, uid, ids, user_id=False, section_id=False, context=None):
         """
         Different cases of merge:
         - merge leads together = 1 new lead
@@ -602,7 +606,8 @@ class crm_lead(base_stage, format_address, osv.osv):
         sequenced_opps = []
         for opportunity in opportunities:
             sequence = -1
-            if opportunity.probability == 0 and opportunity.stage_id and opportunity.stage_id.sequence != 1:
+            # TDE: was "if opportunity.stage_id and opportunity.stage_id.state != 'cancel':"
+            if opportunity.probability == 0 and opportunity.stage_id and opportunity.stage_id.sequence != 1 and opportunity.stage_id.fold:
                 sequence = opportunity.stage_id.sequence
             sequenced_opps.append(((int(sequence != -1 and opportunity.type == 'opportunity'), sequence, -opportunity.id), opportunity))
 
@@ -617,6 +622,11 @@ class crm_lead(base_stage, format_address, osv.osv):
         fields = list(CRM_LEAD_FIELDS_TO_MERGE)
         merged_data = self._merge_data(cr, uid, ids, highest, fields, context=context)
 
+        if user_id:
+            merged_data['user_id'] = user_id
+        if section_id:
+            merged_data['section_id'] = section_id
+
         # Merge messages and attachements into the first opportunity
         self._merge_opportunity_history(cr, uid, highest.id, tail_opportunities, context=context)
         self._merge_opportunity_attachments(cr, uid, highest.id, tail_opportunities, context=context)
@@ -657,8 +667,8 @@ class crm_lead(base_stage, format_address, osv.osv):
             'email_from': customer and customer.email or lead.email_from,
             'phone': customer and customer.phone or lead.phone,
         }
-        if not lead.stage_id or lead.stage_id.type == 'lead':
-            val['stage_id'] = self.stage_find(cr, uid, [lead], section_id, ['&', ('sequence', '=', '1'), ('type', 'in', ('opportunity', 'both'))], context=context)
+        if not lead.stage_id or lead.stage_id.type=='lead':
+            val['stage_id'] = self.stage_find(cr, uid, [lead], section_id, [('sequence', '=', '1'), ('type', 'in', ('opportunity','both'))], context=context)
         return val
 
     def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
@@ -667,8 +677,8 @@ class crm_lead(base_stage, format_address, osv.osv):
             partner = self.pool.get('res.partner')
             customer = partner.browse(cr, uid, partner_id, context=context)
         for lead in self.browse(cr, uid, ids, context=context):
-            # avoid done / cancelled leads
-            if lead.probability == 100 or (lead.probability == 0 and lead.stage_id and lead.stage_id.sequence != 1):
+            # TDE: was if lead.state in ('done', 'cancel'):
+            if (lead.probability == '100') or (lead.probability == '0' and lead.stage_id.sequence != '1'):
                 continue
             vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
             self.write(cr, uid, [lead.id], vals, context=context)
@@ -734,7 +744,10 @@ class crm_lead(base_stage, format_address, osv.osv):
         res_partner = self.pool.get('res.partner')
         if partner_id:
             res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id and lead.section_id.id or False})
+            contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
             res = lead.write({'partner_id': partner_id}, context=context)
+            message = _("<b>Partner</b> set to <em>%s</em>." % (lead.partner_id.name))
+            self.message_post(cr, uid, [lead.id], body=message, context=context)
         return res
 
     def handle_partner_assignation(self, cr, uid, ids, action='create', partner_id=False, context=None):
@@ -817,9 +830,9 @@ class crm_lead(base_stage, format_address, osv.osv):
                 'priority': lead.priority,
             }
             new_id = phonecall.create(cr, uid, vals, context=context)
-            phonecall.case_open(cr, uid, [new_id], context=context)
+            phonecall.write(cr, uid, [new_id], {'state': 'open'}, context=context)
             if action == 'log':
-                phonecall.case_close(cr, uid, [new_id], context=context)
+                phonecall.write(cr, uid, [new_id], {'state': 'done'}, context=context)
             phonecall_dict[lead.id] = new_id
             self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
         return phonecall_dict
@@ -874,7 +887,7 @@ class crm_lead(base_stage, format_address, osv.osv):
         res['context'] = {
             'default_opportunity_id': opportunity.id,
             'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
-            'default_partner_ids': opportunity.partner_id and [opportunity.partner_id.id] or False,
+            'default_partner_ids' : opportunity.partner_id and [opportunity.partner_id.id] or False,
             'default_user_id': uid,
             'default_section_id': opportunity.section_id and opportunity.section_id.id or False,
             'default_email_from': opportunity.email_from,
@@ -882,50 +895,28 @@ class crm_lead(base_stage, format_address, osv.osv):
         }
         return res
 
+    def create(self, cr, uid, vals, context=None):
+        if context is None:
+            context = {}
+        if vals.get('type') and not context.get('default_type'):
+            context['default_type'] = vals.get('type')
+        if vals.get('section_id') and not context.get('default_section_id'):
+            context['default_section_id'] = vals.get('section_id')
+
+        # context: no_log, because subtype already handle this
+        create_context = dict(context, mail_create_nolog=True)
+        return super(crm_lead, self).create(cr, uid, vals, context=create_context)
+
     def write(self, cr, uid, ids, vals, context=None):
+        # stage change: update date_last_stage_update
+        if 'stage_id' in vals:
+            vals['date_last_stage_update'] = fields.datetime.now()
+        # stage change with new stage: update probability
         if vals.get('stage_id') and not vals.get('probability'):
-            # change probability of lead(s) if required by stage
-            stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
-            if stage.on_change:
-                vals['probability'] = stage.probability
+            onchange_stage_values = self.onchange_stage_id(cr, uid, ids, vals.get('stage_id'), context=context)['value']
+            vals.update(onchange_stage_values)
         return super(crm_lead, self).write(cr, uid, ids, vals, context=context)
 
-    def new_mail_send(self, cr, uid, ids, context=None):
-        '''
-        This function opens a window to compose an email, with the edi sale template message loaded by default
-        '''
-        assert len(ids) == 1, 'This option should only be used for a single id at a time.'
-        ir_model_data = self.pool.get('ir.model.data')
-        try:
-            template_id = ir_model_data.get_object_reference(cr, uid, 'crm', 'email_template_opportunity_mail')[1]
-        except ValueError:
-            template_id = False
-        try:
-            compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
-        except ValueError:
-            compose_form_id = False
-        if context is None:
-            context = {}
-        ctx = context.copy()
-        ctx.update({
-            'default_model': 'crm.lead',
-            'default_res_id': ids[0],
-            'default_use_template': bool(template_id),
-            'default_template_id': template_id,
-            'default_composition_mode': 'comment',
-        })
-        return {
-            'name': _('Compose Email'),
-            'type': 'ir.actions.act_window',
-            'view_type': 'form',
-            'view_mode': 'form',
-            'res_model': 'mail.compose.message',
-            'views': [(compose_form_id, 'form')],
-            'view_id': compose_form_id,
-            'target': 'new',
-            'context': ctx,
-        }
-
     # ----------------------------------------
     # Mail Gateway
     # ----------------------------------------
@@ -974,7 +965,7 @@ class crm_lead(base_stage, format_address, osv.osv):
             'user_id': False,
         }
         if msg.get('author_id'):
-            defaults.update(self.onchange_partner_id(cr, uid, None, msg.get('author_id'), context=context)['value'])
+            defaults.update(self.on_change_partner_id(cr, uid, None, msg.get('author_id'), context=context)['value'])
         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
             defaults['priority'] = msg.get('priority')
         defaults.update(custom_values)
@@ -987,14 +978,14 @@ class crm_lead(base_stage, format_address, osv.osv):
         """
         if isinstance(ids, (str, int, long)):
             ids = [ids]
-        if update_vals is None:
-            update_vals = {}
+        if update_vals is None: update_vals = {}
+
         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
             update_vals['priority'] = msg.get('priority')
         maps = {
-            'cost': 'planned_cost',
+            'cost':'planned_cost',
             'revenue': 'planned_revenue',
-            'probability': 'probability',
+            'probability':'probability',
         }
         for line in msg.get('body', '').split('\n'):
             line = line.strip()