Forward port of branch 7.0 up to 1933e92
[odoo/odoo.git] / addons / crm / crm_lead.py
index a2fe1aa..a5315ab 100644 (file)
@@ -23,6 +23,7 @@ import crm
 from datetime import datetime
 from operator import itemgetter
 
+import openerp
 from openerp import SUPERUSER_ID
 from openerp import tools
 from openerp.addons.base.res.res_partner import format_address
@@ -71,10 +72,11 @@ class crm_lead(format_address, osv.osv):
 
     _track = {
         'stage_id': {
-            '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.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,
+            # this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages
+            '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.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.fold,
+            'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.fold and obj.stage_id.sequence > 1,
         },
     }
 
@@ -92,7 +94,7 @@ class crm_lead(format_address, osv.osv):
     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)
+        return self.stage_find(cr, uid, [], section_id, [('fold', '=', False)], context=context)
 
     def _resolve_section_id_from_context(self, cr, uid, context=None):
         """ Returns ID of section based on the value of 'section_id'
@@ -151,6 +153,11 @@ class crm_lead(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):
+        if view_type == 'form' and context and context.get('opportunity_id'):
+            # TODO: replace by get_formview_action call
+            lead_type = self.browse(cr, user, context['opportunity_id'], context=context).type
+            view_lead_xml_id = 'crm_case_form_view_oppor' if lead_type == 'opportunity' else 'crm_case_form_view_leads'
+            _, view_id = self.pool['ir.model.data'].get_object_reference(cr, user, 'crm', view_lead_xml_id)
         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)
@@ -239,24 +246,27 @@ class crm_lead(format_address, osv.osv):
         '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', select=True, 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', select=True,
                         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),
 
+        # Messaging and marketing
+        'message_bounce': fields.integer('Bounce'),
         # Only used for type opportunity
         '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),
+        'ref': fields.reference('Reference', selection=openerp.addons.base.res.res_request.referencable_models),
+        'ref2': fields.reference('Reference 2', selection=openerp.addons.base.res.res_request.referencable_models),
         'phone': fields.char("Phone", size=64),
         'date_deadline': fields.date('Expected Closing', help="Estimate of the date on which the opportunity will be won."),
         'date_action': fields.date('Next Action Date', select=True),
@@ -295,6 +305,7 @@ class crm_lead(format_address, osv.osv):
         '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 = [
@@ -307,7 +318,10 @@ class crm_lead(format_address, osv.osv):
         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}}
+        vals = {'probability': stage.probability}
+        if stage.probability >= 100 or (stage.probability == 0 and stage.sequence > 1):
+                vals['date_closed'] = fields.datetime.now()
+        return {'value': vals}
 
     def on_change_partner_id(self, cr, uid, ids, partner_id, context=None):
         values = {}
@@ -324,6 +338,7 @@ class crm_lead(format_address, osv.osv):
                 'phone': partner.phone,
                 'mobile': partner.mobile,
                 'fax': partner.fax,
+                'zip': partner.zip,
             }
         return {'value': values}
 
@@ -337,23 +352,6 @@ class crm_lead(format_address, osv.osv):
                 section_id = section_ids[0]
         return {'value': {'section_id': section_id}}
 
-    # TDE NOTE: replaced byu base action rule fully working ?
-    # 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
-    #     """
-    #     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\')',
-    #             (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:
@@ -364,10 +362,14 @@ class crm_lead(format_address, osv.osv):
         """
         if isinstance(cases, (int, long)):
             cases = self.browse(cr, uid, cases, context=context)
+        if context is None:
+            context = {}
+        # check whether we should try to add a condition on type
+        avoid_add_type_term = any([term for term in domain if len(term) == 3 if term[0] == 'type'])
         # collect all section_ids
         section_ids = set()
         types = ['both']
-        if not cases:
+        if not cases and context.get('default_type'):
             ctx_type = context.get('default_type')
             types += [ctx_type]
         if section_id:
@@ -385,38 +387,57 @@ class crm_lead(format_address, osv.osv):
                 search_domain.append(('section_ids', '=', section_id))
         search_domain.append(('case_default', '=', True))
         # AND with cases types
-        search_domain.append(('type', 'in', types))
+        if not avoid_add_type_term:
+            search_domain.append(('type', 'in', types))
         # AND with the domain in parameter
         search_domain += list(domain)
         # perform search, return the first found
-        stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
+        stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, limit=1, context=context)
         if stage_ids:
             return stage_ids[0]
         return False
 
     def case_mark_lost(self, cr, uid, ids, context=None):
-        """ 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)
+        """ Mark the case as lost: state=cancel and probability=0
+            :deprecated: this method will be removed in OpenERP v8.
+        """
+        stages_leads = {}
+        for lead in self.browse(cr, uid, ids, context=context):
+            stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0), ('fold', '=', True), ('sequence', '>', 1)], context=context)
             if stage_id:
-                return self.write(cr, uid, [lead.id], {'stage_id': stage_id}, context=context)
+                if stages_leads.get(stage_id):
+                    stages_leads[stage_id].append(lead.id)
+                else:
+                    stages_leads[stage_id] = [lead.id]
             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.'))
+        for stage_id, lead_ids in stages_leads.items():
+            self.write(cr, uid, lead_ids, {'stage_id': stage_id}, context=context)
+        return True
 
     def case_mark_won(self, cr, uid, ids, context=None):
-        """ 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), ('sequence', '>', 1)], context=context)
+        """ Mark the case as won: state=done and probability=100
+            :deprecated: this method will be removed in OpenERP v8.
+        """
+        stages_leads = {}
+        for lead in self.browse(cr, uid, ids, context=context):
+            stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0), ('fold', '=', True)], context=context)
             if stage_id:
-                return self.write(cr, uid, [lead.id], {'stage_id': stage_id}, context=context)
+                if stages_leads.get(stage_id):
+                    stages_leads[stage_id].append(lead.id)
+                else:
+                    stages_leads[stage_id] = [lead.id]
             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.'))
+        for stage_id, lead_ids in stages_leads.items():
+            self.write(cr, uid, lead_ids, {'stage_id': stage_id}, context=context)
+        return True
 
     def case_escalate(self, cr, uid, ids, context=None):
         """ Escalates case to parent level """
@@ -602,7 +623,7 @@ class crm_lead(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
@@ -619,10 +640,12 @@ class crm_lead(format_address, osv.osv):
 
         opportunities = self.browse(cr, uid, ids, context=context)
         sequenced_opps = []
+        # Sorting the leads/opps according to the confidence level of its stage, which relates to the probability of winning it
+        # The confidence level increases with the stage sequence, except when the stage probability is 0.0 (Lost cases)
+        # An Opportunity always has higher confidence level than a lead, unless its stage probability is 0.0
         for opportunity in opportunities:
             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:
+            if opportunity.stage_id and not opportunity.stage_id.fold:
                 sequence = opportunity.stage_id.sequence
             sequenced_opps.append(((int(sequence != -1 and opportunity.type == 'opportunity'), sequence, -opportunity.id), opportunity))
 
@@ -637,6 +660,11 @@ class crm_lead(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)
@@ -644,7 +672,7 @@ class crm_lead(format_address, osv.osv):
         # Merge notifications about loss of information
         opportunities = [highest]
         opportunities.extend(opportunities_rest)
-        self._merge_notify(cr, uid, highest, opportunities, context=context)
+        self._merge_notify(cr, uid, highest.id, 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('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)
@@ -678,7 +706,7 @@ class crm_lead(format_address, osv.osv):
             '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)
+            val['stage_id'] = self.stage_find(cr, uid, [lead], section_id, [('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):
@@ -687,7 +715,8 @@ class crm_lead(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'):
+            # TDE: was if lead.state in ('done', 'cancel'):
+            if lead.probability == 100 or (lead.probability == 0 and lead.stage_id.fold):
                 continue
             vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
             self.write(cr, uid, [lead.id], vals, context=context)
@@ -741,24 +770,6 @@ class crm_lead(format_address, osv.osv):
             )
         return partner_id
 
-    def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
-        """
-        Assign a partner to a lead.
-
-        :param object lead: browse record of the lead to process
-        :param int partner_id: identifier of the partner to assign
-        :return bool: True if the partner has properly been assigned
-        """
-        res = False
-        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):
         """
         Handle partner assignation during a lead conversion.
@@ -772,13 +783,16 @@ class crm_lead(format_address, osv.osv):
         """
         #TODO this is a duplication of the handle_partner_assignation method of crm_phonecall
         partner_ids = {}
-        # If a partner_id is given, force this partner for all elements
-        force_partner_id = partner_id
         for lead in self.browse(cr, uid, ids, context=context):
             # If the action is set to 'create' and no partner_id is set, create a new one
-            if action == 'create':
-                partner_id = force_partner_id or self._create_lead_partner(cr, uid, lead, context)
-            self._lead_set_partner(cr, uid, lead, partner_id, context=context)
+            if lead.partner_id:
+                partner_ids[lead.id] = lead.partner_id.id
+                continue
+            if not partner_id and action == 'create':
+                partner_id = self._create_lead_partner(cr, uid, lead, context)
+                self.pool['res.partner'].write(cr, uid, partner_id, {'section_id': lead.section_id and lead.section_id.id or False})
+            if partner_id:
+                lead.write({'partner_id': partner_id}, context=context)
             partner_ids[lead.id] = partner_id
         return partner_ids
 
@@ -817,9 +831,11 @@ class crm_lead(format_address, osv.osv):
         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:
+            try:
+                res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
                 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
+            except ValueError:
+                pass
         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
@@ -839,9 +855,9 @@ class crm_lead(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
@@ -892,7 +908,7 @@ class crm_lead(format_address, osv.osv):
         :return dict: dictionary value for created Meeting view
         """
         opportunity = self.browse(cr, uid, ids[0], context)
-        res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
+        res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'calendar', 'action_calendar_event', context)
         res['context'] = {
             'default_opportunity_id': opportunity.id,
             'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
@@ -917,46 +933,31 @@ class crm_lead(format_address, osv.osv):
         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'):
             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:
+    def copy(self, cr, uid, id, default=None, context=None):
+        if not default:
+            default = {}
+        if not context:
             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,
-        }
+        lead = self.browse(cr, uid, id, context=context)
+        local_context = dict(context)
+        local_context.setdefault('default_type', lead.type)
+        local_context.setdefault('default_section_id', lead.section_id)
+        if lead.type == 'opportunity':
+            default['date_open'] = fields.datetime.now()
+        else:
+            default['date_open'] = False
+        default['date_closed'] = False
+        default['stage_id'] = self._get_default_stage_id(cr, uid, local_context)
+        return super(crm_lead, self).copy(cr, uid, id, default, context=context)
 
     # ----------------------------------------
     # Mail Gateway
@@ -996,10 +997,8 @@ class crm_lead(format_address, osv.osv):
         """
         if custom_values is None:
             custom_values = {}
-        desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
         defaults = {
             'name':  msg.get('subject') or _("No Subject"),
-            'description': desc,
             'email_from': msg.get('from'),
             'email_cc': msg.get('cc'),
             'partner_id': msg.get('author_id', False),
@@ -1044,11 +1043,13 @@ class crm_lead(format_address, osv.osv):
     def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
         phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
         if action == 'log':
-            prefix = 'Logged'
+            message = _('Logged a call for %(date)s. %(description)s')
         else:
-            prefix = 'Scheduled'
-        suffix = ' %s' % phonecall.description
-        message = _("%s a call for %s.%s") % (prefix, phonecall.date, suffix)
+            message = _('Scheduled a call for %(date)s. %(description)s')
+        phonecall_date = datetime.strptime(phonecall.date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
+        phonecall_usertime = fields.datetime.context_timestamp(cr, uid, phonecall_date, context=context).strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
+        html_time = "<time datetime='%s+00:00'>%s</time>" % (phonecall.date, phonecall_usertime)
+        message = message % dict(date=html_time, description=phonecall.description)
         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):