[IMP] notification with need action in phonecall when schedule a call.
[odoo/odoo.git] / addons / crm / crm_lead.py
index 395d5dd..18f7468 100644 (file)
@@ -27,7 +27,7 @@ from tools.translate import _
 from crm import crm_case
 import binascii
 import tools
-
+from mail.mail_message import to_email
 
 CRM_LEAD_PENDING_STATES = (
     crm.AVAILABLE_STATES[2][0], # Cancelled
@@ -39,8 +39,35 @@ class crm_lead(crm_case, osv.osv):
     """ CRM Lead Case """
     _name = "crm.lead"
     _description = "Lead/Opportunity"
-    _order = "date_action, priority, id desc"
+    _order = "priority,date_action,id desc"
     _inherit = ['mail.thread','res.partner.address']
+
+    def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
+        access_rights_uid = access_rights_uid or uid
+        stage_obj = self.pool.get('crm.case.stage')
+        order = stage_obj._order
+        if read_group_order == 'stage_id desc':
+            # lame hack to allow reverting search, should just work in the trivial case
+            order = "%s desc" % order
+        stage_ids = stage_obj._search(cr, uid, ['|', ('id','in',ids),('case_default','=',1)], order=order,
+                                      access_rights_uid=access_rights_uid, context=context)
+        result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
+        # restore order of the search
+        result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
+        return result
+
+    _group_by_full = {
+        'stage_id': _read_group_stage_ids
+    }
+
+    # overridden because res.partner.address has an inconvenient name_get,
+    # especially if base_contact is installed.
+    def name_get(self, cr, user, ids, context=None):
+        if isinstance(ids, (int, long)):
+            ids = [ids]
+        return [(r['id'], tools.ustr(r[self._rec_name]))
+                    for r in self.read(cr, user, ids, [self._rec_name], context)]
+
     def _compute_day(self, cr, uid, ids, fields, args, context=None):
         """
         @param cr: the current row, from the database cursor,
@@ -124,39 +151,33 @@ class crm_lead(crm_case, osv.osv):
         'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
             select=True, help="Optional linked partner, usually after conversion of the lead"),
 
-        # From crm.case
-        'id': fields.integer('ID'),
+        'id': fields.integer('ID', readonly=True),
         'name': fields.char('Name', size=64, select=1),
         'active': fields.boolean('Active', required=False),
         'date_action_last': fields.datetime('Last Action', readonly=1),
         'date_action_next': fields.datetime('Next Action', readonly=1),
         'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
-                        select=True, help='Sales team to which this case belongs to. Defines responsible user and e-mail address for the mail gateway.'),
+                        select=True, 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"),
         'description': fields.text('Notes'),
         'write_date': fields.datetime('Update Date' , readonly=True),
 
-        # Lead fields
         'categ_id': fields.many2one('crm.case.categ', 'Category', \
             domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
         'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
             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('res.partner.canal', 'Channel', help="From which channel (mail, direct, phone, ...) did this contact reach you?"),
+        '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 that will be created while converting the into opportunity', select=1),
+        'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner that will be created while converting the lead into opportunity', select=1),
         'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
         'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
-        'type':fields.selection([
-            ('lead','Lead'),
-            ('opportunity','Opportunity'),
-
-        ],'Type', help="Type is used to separate Leads and Opportunities"),
-        'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
+        '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', domain="[('type','=','lead')]"),
-        'user_id': fields.many2one('res.users', 'Salesman', select=1),
+        'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
+        'user_id': fields.many2one('res.users', 'Salesman'),
         'referred': fields.char('Referred By', size=64),
         'date_open': fields.datetime('Opened', readonly=True),
         'day_open': fields.function(_compute_day, string='Days to Open', \
@@ -170,8 +191,26 @@ class crm_lead(crm_case, osv.osv):
                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
         'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
         'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
-    }
 
+        # Only used for type opportunity
+        'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"),
+        'probability': fields.float('Probability (%)',group_operator="avg"),
+        'planned_revenue': fields.float('Expected Revenue'),
+        'ref': fields.reference('Reference', selection=crm._links_get, size=128),
+        'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
+        'phone': fields.char("Phone", size=64),
+        'date_deadline': fields.date('Expected Closing'),
+        'date_action': fields.date('Next Action Date', select=True),
+        'title_action': fields.char('Next Action', size=64),
+        'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
+        'color': fields.integer('Color Index'),
+        'partner_address_name': fields.related('partner_address_id', 'name', type='char', string='Partner Contact Name', readonly=True),
+        'partner_address_email': fields.related('partner_address_id', 'email', type='char', string='Partner Contact Email', readonly=True),
+        'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
+        'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
+        'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
+
+    }
 
     _defaults = {
         'active': lambda *a: 1,
@@ -182,164 +221,579 @@ class crm_lead(crm_case, osv.osv):
         'section_id': crm_case._get_section,
         '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],
-        #'stage_id': _get_stage_id,
+        'color': 0,
     }
 
+    def create(self, cr, uid, vals, context=None):
+        obj_id = super(crm_lead, self).create(cr, uid, vals, context)
+        self._case_create_notification(cr, uid, [obj_id], context=context)
+        return obj_id
 
 
     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
         """This function returns value of partner email based on Partner Address
-        @param self: The object pointer
-        @param cr: the current row, from the database cursor,
-        @param uid: the current user’s ID for security checks,
-        @param ids: List of case IDs
-        @param add: Id of Partner's address
-        @email: Partner's email ID
         """
         if not add:
             return {'value': {'email_from': False, 'country_id': False}}
         address = self.pool.get('res.partner.address').browse(cr, uid, add)
         return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
 
-    def case_open(self, cr, uid, ids, *args):
-        """Overrides cancel for crm_case for setting Open Date
-        @param self: The object pointer
-        @param cr: the current row, from the database cursor,
-        @param uid: the current user’s ID for security checks,
-        @param ids: List of case's Ids
-        @param *args: Give Tuple Value
+    def on_change_optin(self, cr, uid, ids, optin):
+        return {'value':{'optin':optin,'optout':False}}
+
+    def on_change_optout(self, cr, uid, ids, optout):
+        return {'value':{'optout':optout,'optin':False}}
+
+    def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
+        if not stage_id:
+            return {'value':{}}
+        stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
+        if not stage.on_change:
+            return {'value':{}}
+        return {'value':{'probability': stage.probability}}
+
+    def stage_find_percent(self, cr, uid, percent, section_id):
+        """ Return the first stage with a probability == percent
         """
-        leads = self.browse(cr, uid, ids)
+        stage_pool = self.pool.get('crm.case.stage')
+        if section_id :
+            ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
+        else :
+            ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
 
+        if ids:
+            return ids[0]
+        return False
 
+    def stage_find_lost(self, cr, uid, section_id):
+        return self.stage_find_percent(cr, uid, 0.0, section_id)
 
-        for i in xrange(0, len(ids)):
-            if leads[i].state == 'draft':
-                value = {}
-                if not leads[i].stage_id :
-                    stage_id = self._find_first_stage(cr, uid, leads[i].type, leads[i].section_id.id or False)
-                    value.update({'stage_id' : stage_id})
-                value.update({'date_open': time.strftime('%Y-%m-%d %H:%M:%S')})
-                self.write(cr, uid, [ids[i]], value)
-            self.log_open( cr, uid, leads[i])
-        res = super(crm_lead, self).case_open(cr, uid, ids, *args)
-        return res
+    def stage_find_won(self, cr, uid, section_id):
+        return self.stage_find_percent(cr, uid, 100.0, section_id)
 
-    def log_open(self, cr, uid, case):
-        if case.type == 'lead':
-            message = _("The lead '%s' has been opened.") % case.name
-        elif case.type == 'opportunity':
-            message = _("The opportunity '%s' has been opened.") % case.name
+    def _case_create_notification(self, cr, uid, ids, context=None):
+        for obj in self.browse(cr, uid, ids, context=context):
+            self.message_subscribe(cr, uid, ids, [obj.user_id.id], context=context)
+            if obj.type=="opportunity" and obj.state=="draft":
+                message = _("Opportunity is <b>created</b>.")
+            elif obj.type=="lead" :
+                message = _("Lead is <b>created</b>.")
+            else:
+                message = _("The case has been <b>created</b>.")
+            self.message_append_note(cr, uid, ids, _('System notification'),
+                        message, type='notification', need_action_user_id=obj.user_id.id, context=context)
+        return True
+
+    def _case_open_notification(self, lead, context=None):
+        if lead.state != 'draft' and lead.state != 'pending':
+            return False
+        if lead.type == 'lead':
+            message = _("The lead has been <b>opened</b>.")
+        elif lead.type == 'opportunity':
+            message = _("The opportunity has been <b>opened</b>.")
         else:
-            message = _("The case '%s' has been opened.") % case.name
-        self.log(cr, uid, case.id, message)
+            message = _("The case has been <b>opened</b>.")
+        lead.message_append_note('' ,message, need_action_user_id=lead.user_id.id)
+
+    def _case_close_notification(self, lead, context=None):
+        lead[0].message_mark_done(context)
+        if lead[0].type == 'lead':
+            message = _("The lead has been <b>closed</b>.")
+        elif lead[0].type == 'opportunity':
+            message = _("The opportunity has been <b>closed</b>.")
+        else:
+            message = _("The case has been <b>closed</b>.")
+        lead[0].message_append_note('' ,message)
+
+    def _case_mark_lost_notification(self, lead, context=None):
+        lead.message_mark_done(context)
+        message = _("The opportunity has been <b>marked as lost</b>.")
+        lead.message_append_note('' ,message)
+
+    def _case_mark_won_notification(self, lead, context=None):
+        lead.message_mark_done(context)
+        message = _("The opportunity has been <b>won</b>.")
+        lead.message_append_note('' ,message)
+
+    def _case_cancel_notification(self, lead, context=None):
+        lead[0].message_mark_done(context)
+        if lead[0].type == 'lead':
+            message = _("The lead has been <b>cancelled</b>.")
+        elif lead[0].type == 'opportunity':
+            message = _("The opportunity has been <b>cancelled</b>.")
+        lead[0].message_append_note('' ,message)
+
+    def _case_pending_notification(self, case, context=None):
+        if case[0].type == 'lead':
+            message = _("The lead is <b>pending</b>.")
+        elif case[0].type == 'opportunity':
+            message = _("The opportunity is <b>pending</b>.")
+        case[0].message_append_note('' ,message)
+
+    def _case_escalate_notification(self, case, context=None):
+        message = _("The lead is <b>escalated</b>.")
+        case.message_append_note('' ,message)
+
+    def _case_phonecall_notification(self, cr, uid, ids, case, phonecall, action, context=None):
+        for obj in phonecall.browse(cr, uid, ids, context=context):
+            if action == "schedule" :
+                message = _("<b>%s a call</b> for the %s.") % (action, obj.date)
+            else :
+                message = _("<b>%s a call</b>.") % (action)
+            case.message_append_note('', message)
+            if action == "schedule" :
+                phonecall.message_append_note(cr, uid, ids, '', message, need_action_user_id=obj.user_id.id)
+
+    def case_open(self, cr, uid, ids, context=None):
+        res = super(crm_lead, self).case_open(cr, uid, ids, context)
+        for lead in self.browse(cr, uid, ids, context=context):
+            value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
+            self.write(cr, uid, [lead.id], value)
+            if lead.type == 'opportunity' and not lead.stage_id:
+                stage_id = self.stage_find(cr, uid, lead.section_id.id or False, [('sequence','>',0)])
+                if stage_id:
+                    self.stage_set(cr, uid, [lead.id], stage_id)
+        return res
 
-    def case_close(self, cr, uid, ids, *args):
-        """Overrides close for crm_case for setting close date
-        @param self: The object pointer
-        @param cr: the current row, from the database cursor,
-        @param uid: the current user’s ID for security checks,
-        @param ids: List of case Ids
-        @param *args: Tuple Value for additional Params
-        """
-        res = super(crm_lead, self).case_close(cr, uid, ids, *args)
+    def case_close(self, cr, uid, ids, context=None):
+        res = super(crm_lead, self).case_close(cr, uid, ids, context)
         self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
-        for case in self.browse(cr, uid, ids):
-            if case.type == 'lead':
-                message = _("The lead '%s' has been closed.") % case.name
-            elif case.type == 'opportunity':
-                message = _("The opportunity '%s' has been closed.") % case.name
-            else:
-                message = _("The case '%s' has been closed.") % case.name
-            self.log(cr, uid, case.id, message)
         return res
 
-    def convert_opportunity(self, cr, uid, ids, context=None):
-        """ Precomputation for converting lead to opportunity
-        @param cr: the current row, from the database cursor,
-        @param uid: the current user’s ID for security checks,
-        @param ids: List of closeday’s IDs
-        @param context: A standard dictionary for contextual values
-        @return: Value of action in dict
+    def case_cancel(self, cr, uid, ids, context=None):
+        """Overrides cancel for crm_case for setting probability
+        """
+        res = super(crm_lead, self).case_cancel(cr, uid, ids, context)
+        self.write(cr, uid, ids, {'probability' : 0.0})
+        return res
+
+    def case_reset(self, cr, uid, ids, context=None):
+        """Overrides reset as draft in order to set the stage field as empty
+        """
+        res = super(crm_lead, self).case_reset(cr, uid, ids, context)
+        self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
+        return res
+
+    def case_mark_lost(self, cr, uid, ids, context=None):
+        """Mark the case as lost: state = done and probability = 0%
+        """
+        res = super(crm_lead, self).case_close(cr, uid, ids, context)
+        self.write(cr, uid, ids, {'probability' : 0.0})
+        for lead in self.browse(cr, uid, ids):
+            stage_id = self.stage_find_lost(cr, uid, lead.section_id.id or False)
+            if stage_id:
+                self.stage_set(cr, uid, [lead.id], stage_id)
+        return res
+
+    def case_mark_won(self, cr, uid, ids, context=None):
+        """Mark the case as lost: state = done and probability = 0%
+        """
+        res = super(crm_lead, self).case_close(cr, uid, ids, context=None)
+        self.write(cr, uid, ids, {'probability' : 100.0})
+        for lead in self.browse(cr, uid, ids):
+            stage_id = self.stage_find_won(cr, uid, lead.section_id.id or False)
+            if stage_id:
+                self.stage_set(cr, uid, [lead.id], stage_id)
+            self._case_mark_won_notification(lead, context=context)
+        return res
+
+    def set_priority(self, cr, uid, ids, priority):
+        """Set lead priority
+        """
+        return self.write(cr, uid, ids, {'priority' : priority})
+
+    def set_high_priority(self, cr, uid, ids, context=None):
+        """Set lead priority to high
         """
+        return self.set_priority(cr, uid, ids, '1')
+
+    def set_normal_priority(self, cr, uid, ids, context=None):
+        """Set lead priority to normal
+        """
+        return self.set_priority(cr, uid, ids, '3')
+
+
+    def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
+        # prepare opportunity data into dictionary for merging
+        opportunities = self.browse(cr, uid, ids, context=context)
+        def _get_first_not_null(attr):
+            if hasattr(oldest, attr):
+                return getattr(oldest, attr)
+            for opportunity in opportunities:
+                if hasattr(opportunity, attr):
+                    return getattr(opportunity, attr)
+            return False
+
+        def _get_first_not_null_id(attr):
+            res = _get_first_not_null(attr)
+            return res and res.id or False
+
+        def _concat_all(attr):
+            return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
+
+        data = {}
+        for field_name in fields:
+            field_info = self._all_columns.get(field_name)
+            if field_info is None:
+                continue
+            field = field_info.column
+            if field._type in ('many2many', 'one2many'):
+                continue
+            elif field._type == 'many2one':
+                data[field_name] = _get_first_not_null_id(field_name)  # !!
+            elif field._type == 'text':
+                data[field_name] = _concat_all(field_name)  #not lost
+            else:
+                data[field_name] = _get_first_not_null(field_name)  #not lost
+        return data
+
+    def _merge_find_oldest(self, cr, uid, ids, context=None):
         if context is None:
             context = {}
-        context.update({'active_ids': ids})
+        #TOCHECK: where pass 'convert' in context ?
+        if context.get('convert'):
+            ids = list(set(ids) - set(context.get('lead_ids', False)) )
+
+        #search opportunities order by create date
+        opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
+        oldest_id = opportunity_ids[0]
+        return self.browse(cr, uid, oldest_id, context=context)
+
+    def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
+        body = []
+        if title:
+            body.append("%s\n" % (title))
+        for field_name in fields:
+            field_info = self._all_columns.get(field_name)
+            if field_info is None:
+                continue
+            field = field_info.column
+            value = None
+
+            if field._type == 'selection':
+                if hasattr(field.selection, '__call__'):
+                    key = field.selection(self, cr, uid, context=context)
+                else:
+                    key = field.selection
+                value = dict(key).get(lead[field_name], lead[field_name])
+            elif field._type == 'many2one':
+                if lead[field_name]:
+                    value = lead[field_name].name_get()[0][1]
+            else:
+                value = lead[field_name]
+
+            body.append("%s: %s" % (field.string, value or ''))
+        return "\n".join(body + ['---'])
+
+    def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
+        #TOFIX: mail template should be used instead of fix body, subject text
+        details = []
+        merge_message = _('Merged opportunities')
+        subject = [merge_message]
+        fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
+                  'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
+                  'country_id', 'city', 'street', 'street2', 'zip']
+        for opportunity in opportunities:
+            subject.append(opportunity.name)
+            title = "%s : %s" % (merge_message, opportunity.name)
+            details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
+
+        subject = subject[0] + ", ".join(subject[1:])
+        details = "\n\n".join(details)
+        return opportunity.message_append_note(subject, body=details, need_action_user_id=opportunity.user_id.id)
+
+    def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
+        message = self.pool.get('mail.message')
+        for opportunity in opportunities:
+            for history in opportunity.message_ids:
+                message.write(cr, uid, history.id, {
+                        'res_id': opportunity_id,
+                        'subject' : _("From %s : %s") % (opportunity.name, history.subject)
+                }, context=context)
+
+        return True
+
+    def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
+        attachment = self.pool.get('ir.attachment')
+
+        # return attachments of opportunity
+        def _get_attachments(opportunity_id):
+            attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
+            return attachment.browse(cr, uid, attachment_ids, context=context)
+
+        count = 1
+        first_attachments = _get_attachments(opportunity_id)
+        for opportunity in opportunities:
+            attachments = _get_attachments(opportunity.id)
+            for first in first_attachments:
+                for attachment in attachments:
+                    if attachment.name == first.name:
+                        values = dict(
+                            name = "%s (%s)" % (attachment.name, count,),
+                            res_id = opportunity_id,
+                        )
+                        attachment.write(values)
+                        count+=1
 
-        data_obj = self.pool.get('ir.model.data')
-        value = {}
+        return True
 
+    def merge_opportunity(self, cr, uid, ids, context=None):
+        """
+        To merge opportunities
+            :param ids: list of opportunities ids to merge
+        """
+        if context is None: context = {}
 
-        for case in self.browse(cr, uid, ids, context=context):
-            context.update({'active_id': case.id})
-            data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
-            view_id1 = False
-            if data_id:
-                view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
-            value = {
-                    'name': _('Create Partner'),
-                    'view_type': 'form',
-                    'view_mode': 'form,tree',
-                    'res_model': 'crm.lead2opportunity.partner',
-                    'view_id': False,
-                    'context': context,
-                    'views': [(view_id1, 'form')],
-                    'type': 'ir.actions.act_window',
-                    'target': 'new',
-                    'nodestroy': True
-            }
-        return value
+        #TOCHECK: where pass lead_ids in context?
+        lead_ids = context and context.get('lead_ids', []) or []
 
-    def write(self, cr, uid, ids, vals, context=None):
-        if not context:
-            context = {}
+        if len(ids) <= 1:
+            raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
 
-        if 'date_closed' in vals:
-            return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
+        ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
+        opportunities = self.browse(cr, uid, ids, context=context)
+        opportunities_list = list(set(opportunities) - set(ctx_opportunities))
+        oldest = self._merge_find_oldest(cr, uid, ids, context=context)
+        if ctx_opportunities :
+            first_opportunity = ctx_opportunities[0]
+            tail_opportunities = opportunities_list
+        else:
+            first_opportunity = opportunities_list[0]
+            tail_opportunities = opportunities_list[1:]
+
+        fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id',
+            'partner_address_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
+            'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
+            'date_action_next', 'email_from', 'email_cc', 'partner_name']
+
+        data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
+
+        # merge data into first opportunity
+        self.write(cr, uid, [first_opportunity.id], data, context=context)
+
+        #copy message and attachements into the first opportunity
+        self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
+        self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
+
+        #Notification about loss of information
+        self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
+        #delete tail opportunities
+        self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
+
+        #open first opportunity
+        self.case_open(cr, uid, [first_opportunity.id])
+        return first_opportunity.id
+
+    def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
+        crm_stage = self.pool.get('crm.case.stage')
+        contact_id = False
+        if customer:
+            contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
+        if not section_id:
+            section_id = lead.section_id and lead.section_id.id or False
+        if section_id:
+            stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
+        else:
+            stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
+        stage_id = stage_ids and stage_ids[0] or False
+        return {
+                'planned_revenue': lead.planned_revenue,
+                'probability': lead.probability,
+                'name': lead.name,
+                'partner_id': customer and customer.id or False,
+                'user_id': (lead.user_id and lead.user_id.id),
+                'type': 'opportunity',
+                'stage_id': stage_id or False,
+                'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
+                'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
+                'partner_address_id': contact_id,
+        }
 
-        if 'stage_id' in vals and vals['stage_id']:
-            stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
-            text = _("Changed Stage to: %s") % stage_obj.name
-            self.message_append(cr, uid, ids, text, body_text=text, context=context)
-            message=''
-            for case in self.browse(cr, uid, ids, context=context):
-                if case.type == 'lead' or  context.get('stage_type',False)=='lead':
-                    message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
-                elif case.type == 'opportunity':
-                    message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
-                self.log(cr, uid, case.id, message)
-        return super(crm_lead,self).write(cr, uid, ids, vals, context)
+    def _convert_opportunity_notification(self, cr, uid, lead, context=None):
+        success_message = _("Lead is <b>converted to an opportunity</b>.")
+        lead.message_append_note(success_message ,success_message, need_action_user_id=lead.user_id.id)
+        return True
+
+    def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
+        partner = self.pool.get('res.partner')
+        mail_message = self.pool.get('mail.message')
+        customer = False
+        if partner_id:
+            customer = partner.browse(cr, uid, partner_id, context=context)
+        for lead in self.browse(cr, uid, ids, context=context):
+            if lead.state in ('done', 'cancel'):
+                continue
+            if user_ids or section_id:
+                self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
+
+            vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
+            self.write(cr, uid, [lead.id], vals, context=context)
+
+            self._convert_opportunity_notification(cr, uid, lead, context=context)
+            #TOCHECK: why need to change partner details in all messages of lead ?
+            if lead.partner_id:
+                msg_ids = [ x.id for x in lead.message_ids]
+                mail_message.write(cr, uid, msg_ids, {
+                        'partner_id': lead.partner_id.id
+                    }, context=context)
+        return True
+
+    def _lead_create_partner(self, cr, uid, lead, context=None):
+        partner = self.pool.get('res.partner')
+        partner_id = partner.create(cr, uid, {
+                    'name': lead.partner_name or lead.contact_name or lead.name,
+                    'user_id': lead.user_id.id,
+                    'comment': lead.description,
+                    'section_id': lead.section_id.id or False,
+                    'address': []
+        })
+        return partner_id
+
+    def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
+        res = False
+        res_partner = self.pool.get('res.partner')
+        if partner_id:
+            res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
+            contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
+            res = lead.write({'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
 
-    def stage_next(self, cr, uid, ids, context=None):
-        stage = super(crm_lead, self).stage_next(cr, uid, ids, context=context)
-        if stage:
-            stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
-            if stage_obj.on_change:
-                data = {'probability': stage_obj.probability}
-                self.write(cr, uid, ids, data)
-        return stage
-
-    def stage_previous(self, cr, uid, ids, context=None):
-        stage = super(crm_lead, self).stage_previous(cr, uid, ids, context=context)
-        if stage:
-            stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
-            if stage_obj.on_change:
-                data = {'probability': stage_obj.probability}
-                self.write(cr, uid, ids, data)
-        return stage
+        return res
+
+    def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
+        address = self.pool.get('res.partner.address')
+        return address.create(cr, uid, {
+                    'partner_id': partner_id,
+                    'name': lead.contact_name,
+                    'phone': lead.phone,
+                    'mobile': lead.mobile,
+                    'email': lead.email_from and to_email(lead.email_from)[0],
+                    'fax': lead.fax,
+                    'title': lead.title and lead.title.id or False,
+                    'function': lead.function,
+                    'street': lead.street,
+                    'street2': lead.street2,
+                    'zip': lead.zip,
+                    'city': lead.city,
+                    'country_id': lead.country_id and lead.country_id.id or False,
+                    'state_id': lead.state_id and lead.state_id.id or False,
+                })
+
+    def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
+        """
+        This function convert partner based on action.
+        if action is 'create', create new partner with contact and assign lead to new partner_id.
+        otherwise assign lead to specified partner_id
+        """
+        if context is None:
+            context = {}
+        partner_ids = {}
+        for lead in self.browse(cr, uid, ids, context=context):
+            if action == 'create':
+                if not partner_id:
+                    partner_id = self._lead_create_partner(cr, uid, lead, context=context)
+                self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
+            self._lead_set_partner(cr, uid, lead, partner_id, context=context)
+            partner_ids[lead.id] = partner_id
+        return partner_ids
+
+    def _send_mail_to_salesman(self, cr, uid, lead, context=None):
+        """
+        Send mail to salesman with updated Lead details.
+        @ lead: browse record of 'crm.lead' object.
+        """
+        #TOFIX: mail template should be used here instead of fix subject, body text.
+        message = self.pool.get('mail.message')
+        email_to = lead.user_id and lead.user_id.user_email
+        if not email_to:
+            return False
+
+        email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
+        partner = lead.partner_id and lead.partner_id.name or lead.partner_name
+        subject = "lead %s converted into opportunity" % lead.name
+        body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
+        return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
+
+
+    def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
+        index = 0
+        for lead_id in ids:
+            value = {}
+            if team_id:
+                value['section_id'] = team_id
+            if index < len(user_ids):
+                value['user_id'] = user_ids[index]
+                index += 1
+            if value:
+                self.write(cr, uid, [lead_id], value, context=context)
+        return True
+
+    def schedule_phonecall(self, cr, uid, ids, schedule_time, call_summary, desc, phone, contact_name, user_id=False, section_id=False, categ_id=False, action='schedule', context=None):
+        """
+        action :('schedule','Schedule a call'), ('log','Log a call')
+        """
+        phonecall = self.pool.get('crm.phonecall')
+        model_data = self.pool.get('ir.model.data')
+        phonecall_dict = {}
+        if not categ_id:
+            res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
+            if res_id:
+                categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
+        for lead in self.browse(cr, uid, ids, context=context):
+            if not section_id:
+                section_id = lead.section_id and lead.section_id.id or False
+            if not user_id:
+                user_id = lead.user_id and lead.user_id.id or False
+            vals = {
+                    'name' : call_summary,
+                    'opportunity_id' : lead.id,
+                    'user_id' : user_id or False,
+                    'categ_id' : categ_id or False,
+                    'description' : desc or '',
+                    'date' : schedule_time,
+                    'section_id' : section_id or False,
+                    'partner_id': lead.partner_id and lead.partner_id.id or False,
+                    'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
+                    'partner_phone' : phone or lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
+                    'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
+                    'priority': lead.priority,
+            }
+
+            new_id = phonecall.create(cr, uid, vals, context=context)
+            phonecall.case_open(cr, uid, [new_id])
+            if action == 'log':
+                phonecall.case_close(cr, uid, [new_id])
+            phonecall_dict[lead.id] = new_id
+            self._case_phonecall_notification(cr, uid, [new_id], lead, phonecall, action, context=context)
+        return phonecall_dict
+
+
+    def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
+        models_data = self.pool.get('ir.model.data')
+
+        # Get Opportunity views
+        form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
+        tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
+        return {
+                'name': _('Opportunity'),
+                'view_type': 'form',
+                'view_mode': 'tree, form',
+                'res_model': 'crm.lead',
+                'domain': [('type', '=', 'opportunity')],
+                'res_id': int(opportunity_id),
+                'view_id': False,
+                'views': [(form_view and form_view[1] or False, 'form'),
+                          (tree_view and tree_view[1] or False, 'tree'),
+                          (False, 'calendar'), (False, 'graph')],
+                'type': 'ir.actions.act_window',
+        }
 
-    def unlink(self, cr, uid, ids, context=None):
-        for lead in self.browse(cr, uid, ids, context):
-            if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
-                raise osv.except_osv(_('Warning !'),
-                    _('You can not delete this lead. You should better cancel it.'))
-        return super(crm_lead, self).unlink(cr, uid, ids, context)
 
     def message_new(self, cr, uid, msg, custom_values=None, context=None):
         """Automatically calls when new email message arrives"""
         res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
         subject = msg.get('subject')  or _("No Subject")
         body = msg.get('body_text')
+
         msg_from = msg.get('from')
         priority = msg.get('priority')
         vals = {
@@ -352,16 +806,15 @@ class crm_lead(crm_case, osv.osv):
         if priority:
             vals['priority'] = priority
         vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
-        res_id = self.write(cr, uid, [res_id], vals, context)
+        self.write(cr, uid, [res_id], vals, context)
         return res_id
 
-    def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
+    def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
         if isinstance(ids, (str, int, long)):
             ids = [ids]
-
-        super(crm_lead, self).message_update(cr, uid, msg,
-                                             custom_values=custom_values,
-                                             context=context)
+        if vals == None:
+            vals = {}
+        super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
 
         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
             vals['priority'] = msg.get('priority')
@@ -385,15 +838,84 @@ class crm_lead(crm_case, osv.osv):
         for case in self.browse(cr, uid, ids, context=context):
             values = dict(vals)
             if case.state in CRM_LEAD_PENDING_STATES:
-                values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
+                #re-open
+                values.update(state=crm.AVAILABLE_STATES[1][0])
+                if not case.date_open:
+                    values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
             res = self.write(cr, uid, [case.id], values, context=context)
         return res
 
-    def on_change_optin(self, cr, uid, ids, optin):
-        return {'value':{'optin':optin,'optout':False}}
+    def action_makeMeeting(self, cr, uid, ids, context=None):
+        """
+        This opens Meeting's calendar view to schedule meeting on current Opportunity
+        @return : Dictionary value for created Meeting view
+        """
+        if context is None:
+            context = {}
+        value = {}
+        data_obj = self.pool.get('ir.model.data')
+        for opp in self.browse(cr, uid, ids, context=context):
+            # Get meeting views
+            tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
+            form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
+            calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
+            search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
+            context.update({
+                'default_opportunity_id': opp.id,
+                'default_partner_id': opp.partner_id and opp.partner_id.id or False,
+                'default_user_id': uid,
+                'default_section_id': opp.section_id and opp.section_id.id or False,
+                'default_email_from': opp.email_from,
+                'default_state': 'open',
+                'default_name': opp.name
+            })
+            value = {
+                'name': _('Meetings'),
+                'context': context,
+                'view_type': 'form',
+                'view_mode': 'calendar,form,tree',
+                'res_model': 'crm.meeting',
+                'view_id': False,
+                'views': [(calander_view and calander_view[1] or False, 'calendar'), (form_view and form_view[1] or False, 'form'), (tree_view and tree_view[1] or False, 'tree')],
+                'type': 'ir.actions.act_window',
+                'search_view_id': search_view and search_view[1] or False,
+                'nodestroy': True
+            }
+        return value
 
-    def on_change_optout(self, cr, uid, ids, optout):
-        return {'value':{'optout':optout,'optin':False}}
+
+    def unlink(self, cr, uid, ids, context=None):
+        for lead in self.browse(cr, uid, ids, context):
+            if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
+                raise osv.except_osv(_('Error'),
+                    _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
+                      "You should better cancel it, instead of deleting it.") % lead.name)
+        return super(crm_lead, self).unlink(cr, uid, ids, context)
+
+
+    def write(self, cr, uid, ids, vals, context=None):
+        if not context:
+            context = {}
+
+        if 'date_closed' in vals:
+            return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
+
+        if vals.get('stage_id'):
+            stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
+            # change probability of lead(s) if required by stage
+            if not vals.get('probability') and stage.on_change:
+                vals['probability'] = stage.probability
+            text = _("Changed Stage to: %s") % stage.name
+
+            for case in self.browse(cr, uid, ids, context=context):
+                if case.type == 'lead' or context.get('stage_type') == 'lead':
+                    message = _("The stage of lead has been changed to <b>%s</b>.") % (stage.name)
+                    case.message_append_note(text, message)
+                elif case.type == 'opportunity':
+                    message = _("The stage of opportunity has been changed to <b>%s</b>.") % (stage.name)
+                    case.message_append_note(text, message)
+
+        return super(crm_lead,self).write(cr, uid, ids, vals, context)
 
 crm_lead()