[REM]: Remove python action field from stage
[odoo/odoo.git] / addons / crm / crm_lead.py
index ee2253f..1981757 100644 (file)
 from openerp.addons.base_status.base_stage import base_stage
 import crm
 from datetime import datetime
-from openerp.osv import fields, osv
+from operator import itemgetter
+from openerp.osv import fields, osv, orm
 import time
+from openerp import SUPERUSER_ID
 from openerp import tools
 from openerp.tools.translate import _
 from openerp.tools import html2plaintext
@@ -60,11 +62,7 @@ CRM_LEAD_FIELDS_TO_MERGE = ['name',
     'email_from',
     'email_cc',
     'partner_name']
-CRM_LEAD_PENDING_STATES = (
-    crm.AVAILABLE_STATES[2][0], # Cancelled
-    crm.AVAILABLE_STATES[3][0], # Done
-    crm.AVAILABLE_STATES[4][0], # Pending
-)
+
 
 class crm_lead(base_stage, format_address, osv.osv):
     """ CRM Lead Case """
@@ -74,14 +72,12 @@ class crm_lead(base_stage, format_address, osv.osv):
     _inherit = ['mail.thread', 'ir.needaction_mixin']
 
     _track = {
-        'state': {
-            'crm.mt_lead_create': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'new',
-            'crm.mt_lead_won': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done',
-            'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'cancel',
-        },
         'stage_id': {
-            'crm.mt_lead_stage': lambda self, cr, uid, obj, ctx=None: obj['state'] not in ['new', 'cancel', 'done'],
-        },
+            'crm.mt_lead_create': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.sequence == 1,
+            'crm.mt_lead_stage': lambda self, cr, uid, obj, ctx=None: obj.probability > 0 and obj.probability < 100,
+            'crm.mt_lead_won': lambda self, cr, uid, obj, ctx=None: obj.probability == 100,
+            '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):
@@ -91,13 +87,6 @@ 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 onchange_user_id(self, cr, uid, ids, section_id, user_id, context=None):
-        if user_id:
-            section_ids = self.pool.get('crm.case.section').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context)
-            if section_id and len(section_ids) > 0 and section_id not in section_ids:
-                section_id = section_ids[0]
-        return {'value': {'section_id': section_id}}
-
     def create(self, cr, uid, vals, context=None):
         if context is None:
             context = {}
@@ -108,17 +97,14 @@ class crm_lead(base_stage, format_address, osv.osv):
             if vals.get('type'):
                 ctx['default_type'] = vals['type']
             vals['stage_id'] = self._get_default_stage_id(cr, uid, context=ctx)
-        return super(crm_lead, self).create(cr, uid, vals, context=context)
+        # 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, [('state', '=', 'draft')], 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
@@ -169,7 +155,7 @@ class crm_lead(base_stage, format_address, osv.osv):
         stage_ids = stage_obj._search(cr, uid, search_domain, 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])))
+        result.sort(lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
 
         fold = {}
         for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
@@ -177,7 +163,7 @@ class crm_lead(base_stage, format_address, osv.osv):
         return result, fold
 
     def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
-        res = super(crm_lead,self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
+        res = super(crm_lead, self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
         if view_type == 'form':
             res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
         return res
@@ -239,17 +225,6 @@ class crm_lead(base_stage, format_address, osv.osv):
                 res[lead.id][field] = abs(int(duration))
         return res
 
-    def _history_search(self, cr, uid, obj, name, args, context=None):
-        res = []
-        msg_obj = self.pool.get('mail.message')
-        message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
-        lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
-
-        if lead_ids:
-            return [('id', 'in', lead_ids)]
-        else:
-            return [('id', '=', '0')]
-
     _columns = {
         'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null', track_visibility='onchange',
             select=True, help="Linked partner (optional). Usually created when converting the lead."),
@@ -262,25 +237,24 @@ class crm_lead(base_stage, format_address, osv.osv):
         'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
         '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"),
+        '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),
+        'write_date': fields.datetime('Update Date', readonly=True),
         'categ_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Categories', \
             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('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',
-                        domain="['&', '&', ('fold', '=', False), ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"),
+        'stage_id': fields.many2one('crm.case.stage', 'Stage', track_visibility='onchange',),
         '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),
@@ -288,12 +262,9 @@ class crm_lead(base_stage, format_address, osv.osv):
                                 multi='day_open', type="float", store=True),
         'day_close': fields.function(_compute_day, string='Days to Close', \
                                 multi='day_close', type="float", store=True),
-        'state': fields.related('stage_id', 'state', type="selection", store=True,
-                selection=crm.AVAILABLE_STATES, string="Status", readonly=True,
-                help='The Status is set to \'Draft\', when a case is created. If the case is in progress the Status is set to \'Open\'. When the case is over, the Status is set to \'Done\'. If the case needs to be reviewed then the Status is  set to \'Pending\'.'),
 
         # Only used for type opportunity
-        'probability': fields.float('Success Rate (%)',group_operator="avg"),
+        'probability': fields.float('Success Rate (%)', group_operator="avg"),
         'planned_revenue': fields.float('Expected Revenue', track_visibility='always'),
         'ref': fields.reference('Reference', selection=crm._links_get, size=128),
         'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
@@ -344,11 +315,11 @@ class crm_lead(base_stage, format_address, osv.osv):
 
     def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
         if not stage_id:
-            return {'value':{}}
-        stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
+            return {'value': {}}
+        stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context=context)
         if not stage.on_change:
-            return {'value':{}}
-        return {'value':{'probability': stage.probability}}
+            return {'value': {}}
+        return {'value': {'probability': stage.probability}}
 
     def on_change_partner(self, cr, uid, ids, partner_id, context=None):
         result = {}
@@ -369,15 +340,25 @@ class crm_lead(base_stage, format_address, osv.osv):
             }
         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. """
+        section_id = self._get_default_section_id(cr, uid, context=context) or False
+        if user_id and not section_id:
+            section_ids = self.pool.get('crm.case.section').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context)
+            if section_ids:
+                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
-            Only works on not done and cancelled cases
+            Only works on not won and lost 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 state not in (\'cancel\',\'done\')',
+                and probability not in (0,100)',
                 (time.strftime("%Y-%m-%d %H:%M:%S"),
                     time.strftime('%Y-%m-%d %H:%M:%S')))
 
@@ -396,16 +377,16 @@ class crm_lead(base_stage, format_address, osv.osv):
         if isinstance(cases, (int, long)):
             cases = self.browse(cr, uid, cases, context=context)
         # collect all section_ids
-        section_ids = []
+        section_ids = set()
         types = ['both']
-        if not cases :
-            type = context.get('default_type')
-            types += [type]
+        if not cases:
+            ctx_type = context.get('default_type')
+            types += [ctx_type]
         if section_id:
-            section_ids.append(section_id)
+            section_ids.add(section_id)
         for lead in cases:
             if lead.section_id:
-                section_ids.append(lead.section_id.id)
+                section_ids.add(lead.section_id.id)
             if lead.type not in types:
                 types.append(lead.type)
         # OR all section_ids and OR with case_default
@@ -414,8 +395,7 @@ class crm_lead(base_stage, format_address, osv.osv):
             search_domain += [('|')] * len(section_ids)
             for section_id in section_ids:
                 search_domain.append(('section_ids', '=', section_id))
-        else:
-            search_domain.append(('case_default', '=', True))
+        search_domain.append(('case_default', '=', True))
         # AND with cases types
         search_domain.append(('type', 'in', types))
         # AND with the domain in parameter
@@ -426,38 +406,34 @@ class crm_lead(base_stage, format_address, osv.osv):
             return stage_ids[0]
         return False
 
-    def case_cancel(self, cr, uid, ids, context=None):
-        """ Overrides case_cancel from base_stage to set probability """
-        res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
-        self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
-        return res
-
-    def case_reset(self, cr, uid, ids, context=None):
-        """ Overrides case_reset from base_stage to set probability """
-        res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
-        self.write(cr, uid, ids, {'probability': 0.0}, context=context)
-        return res
-
     def case_mark_lost(self, cr, uid, ids, context=None):
-        """ Mark the case as lost: state=cancel and probability=0 """
+        """ Mark the case as lost: stage with probability=0, on_change=True """
         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)], context=context)
+            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], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
+                self.case_set(cr, uid, [lead.id], new_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: state=done and probability=100 """
+        """ Mark the case as won: stage with probability=100, , on_change=True """
         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)], context=context)
             if stage_id:
-                self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
+                self.case_set(cr, uid, [lead.id], new_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.'))
         return True
 
     def set_priority(self, cr, uid, ids, priority):
         """ Set lead priority
         """
-        return self.write(cr, uid, ids, {'priority' : priority})
+        return self.write(cr, uid, ids, {'priority': priority})
 
     def set_high_priority(self, cr, uid, ids, context=None):
         """ Set lead priority to high
@@ -642,12 +618,13 @@ class crm_lead(base_stage, format_address, osv.osv):
         opportunities = self.browse(cr, uid, ids, context=context)
         sequenced_opps = []
         for opportunity in opportunities:
-            if opportunity.stage_id and opportunity.stage_id.state != 'cancel':
-                sequenced_opps.append((opportunity.stage_id.sequence, opportunity))
-            else:
-                sequenced_opps.append((-1, opportunity))
-        sequenced_opps.sort(key=lambda tup: tup[0], reverse=True)
-        opportunities = [opportunity for sequence, opportunity in sequenced_opps]
+            sequence = -1
+            if opportunity.stage_id and (opportunity.probability == 0 and opportunity.stage_id and opportunity.stage_id.sequence != 1):
+                sequence = opportunity.stage_id.sequence
+            sequenced_opps.append(((int(sequence != -1 and opportunity.type == 'opportunity'), sequence, -opportunity.id), opportunity))
+
+        sequenced_opps.sort(reverse=True)
+        opportunities = map(itemgetter(1), sequenced_opps)
         ids = [opportunity.id for opportunity in opportunities]
         highest = opportunities[0]
         opportunities_rest = opportunities[1:]
@@ -666,15 +643,15 @@ class crm_lead(base_stage, format_address, osv.osv):
         opportunities.extend(opportunities_rest)
         self._merge_notify(cr, uid, highest, opportunities, context=context)
         # Check if the stage is in the stages of the sales team. If not, assign the stage with the lowest sequence
-        if merged_data.get('type') == 'opportunity' and merged_data.get('section_id'):
-            section_stages = self.pool.get('crm.case.section').read(cr, uid, merged_data['section_id'], ['stage_ids'], context=context)
-            if merged_data.get('stage_id') not in section_stages['stage_ids']:
-                stages_sequences = self.pool.get('crm.case.stage').search(cr, uid, [('id','in',section_stages['stage_ids'])], order='sequence', limit=1, context=context)
-                merged_data['stage_id'] = stages_sequences and stages_sequences[0] or False
+        if merged_data.get('section_id'):
+            section_stage_ids = self.pool.get('crm.case.stage').search(cr, uid, [('section_ids', 'in', merged_data['section_id']), ('type', '=', merged_data.get('type'))], order='sequence', context=context)
+            if merged_data.get('stage_id') not in section_stage_ids:
+                merged_data['stage_id'] = section_stage_ids and section_stage_ids[0] or False
         # Write merged data into first opportunity
         self.write(cr, uid, [highest.id], merged_data, context=context)
-        # Delete tail opportunities
-        self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
+        # Delete tail opportunities 
+        # We use the SUPERUSER to avoid access rights issues because as the user had the rights to see the records it should be safe to do so
+        self.unlink(cr, SUPERUSER_ID, [x.id for x in tail_opportunities], context=context)
 
         return highest.id
 
@@ -697,8 +674,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, [('state', '=', 'draft'),('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):
@@ -707,7 +684,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):
-            if lead.state in ('done', 'cancel'):
+            # avoid done / cancelled leads
+            if lead.probability == 100 or (lead.probability == 0 and lead.stage_id 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)
@@ -926,11 +904,14 @@ class crm_lead(base_stage, format_address, osv.osv):
         return res
 
     def write(self, cr, uid, ids, vals, context=None):
+        stage_pool=self.pool.get('crm.case.stage')
         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)
+            stage = stage_pool.browse(cr, uid, vals['stage_id'], context=context)
             if stage.on_change:
                 vals['probability'] = stage.probability
+            if vals.get('probability') == 100:
+                vals['stage_id'] = stage_pool.search(cr, uid, [('probability','=',100.0)],order='sequence')[0]
         return super(crm_lead, self).write(cr, uid, ids, vals, context=context)
 
     def new_mail_send(self, cr, uid, ids, context=None):
@@ -976,15 +957,28 @@ class crm_lead(base_stage, format_address, osv.osv):
     def message_get_reply_to(self, cr, uid, ids, context=None):
         """ Override to get the reply_to of the parent project. """
         return [lead.section_id.message_get_reply_to()[0] if lead.section_id else False
-                    for lead in self.browse(cr, uid, ids, context=context)]
+                    for lead in self.browse(cr, SUPERUSER_ID, ids, context=context)]
+
+    def _get_formview_action(self, cr, uid, id, context=None):
+        action = super(crm_lead, self)._get_formview_action(cr, uid, id, context=context)
+        obj = self.browse(cr, uid, id, context=context)
+        if obj.type == 'opportunity':
+            model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
+            action.update({
+                'views': [(view_id, 'form')],
+                })
+        return action
 
     def message_get_suggested_recipients(self, cr, uid, ids, context=None):
         recipients = super(crm_lead, self).message_get_suggested_recipients(cr, uid, ids, context=context)
-        for lead in self.browse(cr, uid, ids, context=context):
-            if lead.partner_id:
-                self._message_add_suggested_recipient(cr, uid, recipients, lead, partner=lead.partner_id, reason=_('Customer'))
-            elif lead.email_from:
-                self._message_add_suggested_recipient(cr, uid, recipients, lead, email=lead.email_from, reason=_('Customer Email'))
+        try:
+            for lead in self.browse(cr, uid, ids, context=context):
+                if lead.partner_id:
+                    self._message_add_suggested_recipient(cr, uid, recipients, lead, partner=lead.partner_id, reason=_('Customer'))
+                elif lead.email_from:
+                    self._message_add_suggested_recipient(cr, uid, recipients, lead, email=lead.email_from, reason=_('Customer Email'))
+        except (osv.except_osv, orm.except_orm):  # no read access rights -> just ignore suggested recipients because this imply modifying followers
+            pass
         return recipients
 
     def message_new(self, cr, uid, msg, custom_values=None, context=None):
@@ -1004,7 +998,7 @@ class crm_lead(base_stage, format_address, osv.osv):
             'user_id': False,
         }
         if msg.get('author_id'):
-            defaults.update(self.on_change_partner(cr, uid, None, msg.get('author_id'), context=context)['value'])
+            defaults.update(self.onchange_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)
@@ -1017,14 +1011,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()
@@ -1049,6 +1043,14 @@ class crm_lead(base_stage, format_address, osv.osv):
         message = _("%s a call for %s.%s") % (prefix, phonecall.date, suffix)
         return self.message_post(cr, uid, ids, body=message, context=context)
 
+    def log_meeting(self, cr, uid, ids, meeting_subject, meeting_date, duration, context=None):
+        if not duration:
+            duration = _('unknown')
+        else:
+            duration = str(duration)
+        message = _("Meeting scheduled at '%s'<br> Subject: %s <br> Duration: %s hour(s)") % (meeting_date, meeting_subject, duration)
+        return self.message_post(cr, uid, ids, body=message, context=context)
+
     def onchange_state(self, cr, uid, ids, state_id, context=None):
         if state_id:
             country_id=self.pool.get('res.country.state').browse(cr, uid, state_id, context).country_id.id