[FIX] addons: incorrect new-api invocation of method write() with context
[odoo/odoo.git] / addons / crm / crm_lead.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-today OpenERP SA (<http://www.openerp.com>)
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import crm
23 from datetime import datetime
24 from operator import itemgetter
25
26 import openerp
27 from openerp import SUPERUSER_ID
28 from openerp import tools
29 from openerp.addons.base.res.res_partner import format_address
30 from openerp.osv import fields, osv, orm
31 from openerp.tools.translate import _
32 from openerp.tools import email_re, email_split
33
34
35 CRM_LEAD_FIELDS_TO_MERGE = ['name',
36     'partner_id',
37     'campaign_id',
38     'company_id',
39     'country_id',
40     'section_id',
41     'state_id',
42     'stage_id',
43     'medium_id',
44     'source_id',
45     'user_id',
46     'title',
47     'city',
48     'contact_name',
49     'description',
50     'email',
51     'fax',
52     'mobile',
53     'partner_name',
54     'phone',
55     'probability',
56     'planned_revenue',
57     'street',
58     'street2',
59     'zip',
60     'create_date',
61     'date_action_last',
62     'date_action_next',
63     'email_from',
64     'email_cc',
65     'partner_name']
66
67
68 class crm_lead(format_address, osv.osv):
69     """ CRM Lead Case """
70     _name = "crm.lead"
71     _description = "Lead/Opportunity"
72     _order = "priority desc,date_action,id desc"
73     _inherit = ['mail.thread', 'ir.needaction_mixin', 'crm.tracking.mixin']
74
75     _track = {
76         'stage_id': {
77             # this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages
78             'crm.mt_lead_create': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.sequence <= 1,
79             'crm.mt_lead_stage': lambda self, cr, uid, obj, ctx=None: (obj.stage_id and obj.stage_id.sequence > 1) and obj.probability < 100,
80             'crm.mt_lead_won': lambda self, cr, uid, obj, ctx=None: obj.probability == 100 and obj.stage_id and obj.stage_id.fold,
81             '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,
82         },
83     }
84     _mail_mass_mailing = _('Leads / Opportunities')
85
86     def get_empty_list_help(self, cr, uid, help, context=None):
87         context = dict(context or {})
88         if context.get('default_type') == 'lead':
89             context['empty_list_help_model'] = 'crm.case.section'
90             context['empty_list_help_id'] = context.get('default_section_id')
91         context['empty_list_help_document_name'] = _("leads")
92         return super(crm_lead, self).get_empty_list_help(cr, uid, help, context=context)
93
94     def _get_default_section_id(self, cr, uid, user_id=False, context=None):
95         """ Gives default section by checking if present in the context """
96         section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
97         if not section_id:
98             section_id = self.pool.get('res.users').browse(cr, uid, user_id or uid, context).default_section_id.id or False
99         return section_id
100
101     def _get_default_stage_id(self, cr, uid, context=None):
102         """ Gives default stage_id """
103         section_id = self._get_default_section_id(cr, uid, context=context)
104         return self.stage_find(cr, uid, [], section_id, [('fold', '=', False)], context=context)
105
106     def _resolve_section_id_from_context(self, cr, uid, context=None):
107         """ Returns ID of section based on the value of 'section_id'
108             context key, or None if it cannot be resolved to a single
109             Sales Team.
110         """
111         if context is None:
112             context = {}
113         if type(context.get('default_section_id')) in (int, long):
114             return context.get('default_section_id')
115         if isinstance(context.get('default_section_id'), basestring):
116             section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
117             if len(section_ids) == 1:
118                 return int(section_ids[0][0])
119         return None
120
121     def _resolve_type_from_context(self, cr, uid, context=None):
122         """ Returns the type (lead or opportunity) from the type context
123             key. Returns None if it cannot be resolved.
124         """
125         if context is None:
126             context = {}
127         return context.get('default_type')
128
129     def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
130         access_rights_uid = access_rights_uid or uid
131         stage_obj = self.pool.get('crm.case.stage')
132         order = stage_obj._order
133         # lame hack to allow reverting search, should just work in the trivial case
134         if read_group_order == 'stage_id desc':
135             order = "%s desc" % order
136         # retrieve section_id from the context and write the domain
137         # - ('id', 'in', 'ids'): add columns that should be present
138         # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
139         # - OR ('section_ids', '=', section_id), ('fold', '=', False) if section_id: add section columns that are not folded
140         search_domain = []
141         section_id = self._resolve_section_id_from_context(cr, uid, context=context)
142         if section_id:
143             search_domain += ['|', ('section_ids', '=', section_id)]
144             search_domain += [('id', 'in', ids)]
145         else:
146             search_domain += ['|', ('id', 'in', ids), ('case_default', '=', True)]
147         # retrieve type from the context (if set: choose 'type' or 'both')
148         type = self._resolve_type_from_context(cr, uid, context=context)
149         if type:
150             search_domain += ['|', ('type', '=', type), ('type', '=', 'both')]
151         # perform search
152         stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
153         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
154         # restore order of the search
155         result.sort(lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
156
157         fold = {}
158         for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
159             fold[stage.id] = stage.fold or False
160         return result, fold
161
162     def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
163         if context and context.get('opportunity_id'):
164             action = self._get_formview_action(cr, user, context['opportunity_id'], context=context)
165             if action.get('views') and any(view_id for view_id in action['views'] if view_id[1] == view_type):
166                 view_id = next(view_id[0] for view_id in action['views'] if view_id[1] == view_type)
167         res = super(crm_lead, self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
168         if view_type == 'form':
169             res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
170         return res
171
172     _group_by_full = {
173         'stage_id': _read_group_stage_ids
174     }
175
176     def _compute_day(self, cr, uid, ids, fields, args, context=None):
177         """
178         :return dict: difference between current date and log date
179         """
180         res = {}
181         for lead in self.browse(cr, uid, ids, context=context):
182             for field in fields:
183                 res[lead.id] = {}
184                 duration = 0
185                 ans = False
186                 if field == 'day_open':
187                     if lead.date_open:
188                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
189                         date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
190                         ans = date_open - date_create
191                 elif field == 'day_close':
192                     if lead.date_closed:
193                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
194                         date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
195                         ans = date_close - date_create
196                 if ans:
197                     duration = abs(int(ans.days))
198                 res[lead.id][field] = duration
199         return res
200     def _meeting_count(self, cr, uid, ids, field_name, arg, context=None):
201         Event = self.pool['calendar.event']
202         return {
203             opp_id: Event.search_count(cr,uid, [('opportunity_id', '=', opp_id)], context=context)
204             for opp_id in ids
205         }
206     _columns = {
207         'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null', track_visibility='onchange',
208             select=True, help="Linked partner (optional). Usually created when converting the lead."),
209
210         'id': fields.integer('ID', readonly=True),
211         'name': fields.char('Subject', required=True, select=1),
212         'active': fields.boolean('Active', required=False),
213         'date_action_last': fields.datetime('Last Action', readonly=1),
214         'date_action_next': fields.datetime('Next Action', readonly=1),
215         'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
216         'section_id': fields.many2one('crm.case.section', 'Sales Team',
217                         select=True, track_visibility='onchange', help='When sending mails, the default email address is taken from the sales team.'),
218         'create_date': fields.datetime('Creation Date', readonly=True),
219         'email_cc': fields.text('Global CC', help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
220         'description': fields.text('Notes'),
221         'write_date': fields.datetime('Update Date', readonly=True),
222         'categ_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Tags', \
223             domain="['|', ('section_id', '=', section_id), ('section_id', '=', False), ('object_id.model', '=', 'crm.lead')]", help="Classify and analyze your lead/opportunity categories like: Training, Service"),
224         'contact_name': fields.char('Contact Name', size=64),
225         '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),
226         'opt_out': fields.boolean('Opt-Out', oldname='optout',
227             help="If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. "
228                     "Filter 'Available for Mass Mailing' allows users to filter the leads when performing mass mailing."),
229         'type': fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', select=True, help="Type is used to separate Leads and Opportunities"),
230         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
231         'date_closed': fields.datetime('Closed', readonly=True, copy=False),
232         'stage_id': fields.many2one('crm.case.stage', 'Stage', track_visibility='onchange', select=True,
233                         domain="['&', ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"),
234         'user_id': fields.many2one('res.users', 'Salesperson', select=True, track_visibility='onchange'),
235         'referred': fields.char('Referred By'),
236         'date_open': fields.datetime('Assigned', readonly=True),
237         'day_open': fields.function(_compute_day, string='Days to Assign',
238                                     multi='day_open', type="float",
239                                     store={'crm.lead': (lambda self, cr, uid, ids, c={}: ids, ['date_open'], 10)}),
240         'day_close': fields.function(_compute_day, string='Days to Close',
241                                      multi='day_open', type="float",
242                                      store={'crm.lead': (lambda self, cr, uid, ids, c={}: ids, ['date_closed'], 10)}),
243         'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
244
245         # Messaging and marketing
246         'message_bounce': fields.integer('Bounce'),
247         # Only used for type opportunity
248         'probability': fields.float('Success Rate (%)', group_operator="avg"),
249         'planned_revenue': fields.float('Expected Revenue', track_visibility='always'),
250         'ref': fields.reference('Reference', selection=openerp.addons.base.res.res_request.referencable_models),
251         'ref2': fields.reference('Reference 2', selection=openerp.addons.base.res.res_request.referencable_models),
252         'phone': fields.char("Phone", size=64),
253         'date_deadline': fields.date('Expected Closing', help="Estimate of the date on which the opportunity will be won."),
254         'date_action': fields.date('Next Action Date', select=True),
255         'title_action': fields.char('Next Action'),
256         'color': fields.integer('Color Index'),
257         'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
258         'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
259         'company_currency': fields.related('company_id', 'currency_id', type='many2one', string='Currency', readonly=True, relation="res.currency"),
260         'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
261         'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
262
263         # Fields for address, due to separation from crm and res.partner
264         'street': fields.char('Street'),
265         'street2': fields.char('Street2'),
266         'zip': fields.char('Zip', change_default=True, size=24),
267         'city': fields.char('City'),
268         'state_id': fields.many2one("res.country.state", 'State'),
269         'country_id': fields.many2one('res.country', 'Country'),
270         'phone': fields.char('Phone'),
271         'fax': fields.char('Fax'),
272         'mobile': fields.char('Mobile'),
273         'function': fields.char('Function'),
274         'title': fields.many2one('res.partner.title', 'Title'),
275         'company_id': fields.many2one('res.company', 'Company', select=1),
276         'payment_mode': fields.many2one('crm.payment.mode', 'Payment Mode', \
277                             domain="[('section_id','=',section_id)]"),
278         'planned_cost': fields.float('Planned Costs'),
279         'meeting_count': fields.function(_meeting_count, string='# Meetings', type='integer'),
280     }
281
282     _defaults = {
283         'active': 1,
284         'type': 'lead',
285         'user_id': lambda s, cr, uid, c: uid,
286         'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
287         'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, context=c),
288         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
289         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
290         'color': 0,
291         'date_last_stage_update': fields.datetime.now,
292     }
293
294     _sql_constraints = [
295         ('check_probability', 'check(probability >= 0 and probability <= 100)', 'The probability of closing the deal should be between 0% and 100%!')
296     ]
297
298     def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
299         if not stage_id:
300             return {'value': {}}
301         stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context=context)
302         if not stage.on_change:
303             return {'value': {}}
304         vals = {'probability': stage.probability}
305         if stage.probability >= 100 or (stage.probability == 0 and stage.sequence > 1):
306                 vals['date_closed'] = fields.datetime.now()
307         return {'value': vals}
308
309     def on_change_partner_id(self, cr, uid, ids, partner_id, context=None):
310         values = {}
311         if partner_id:
312             partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
313             values = {
314                 'partner_name': partner.parent_id.name if partner.parent_id else partner.name,
315                 'contact_name': partner.name if partner.parent_id else False,
316                 'street': partner.street,
317                 'street2': partner.street2,
318                 'city': partner.city,
319                 'state_id': partner.state_id and partner.state_id.id or False,
320                 'country_id': partner.country_id and partner.country_id.id or False,
321                 'email_from': partner.email,
322                 'phone': partner.phone,
323                 'mobile': partner.mobile,
324                 'fax': partner.fax,
325                 'zip': partner.zip,
326             }
327         return {'value': values}
328
329     def on_change_user(self, cr, uid, ids, user_id, context=None):
330         """ When changing the user, also set a section_id or restrict section id
331             to the ones user_id is member of. """
332         section_id = self._get_default_section_id(cr, uid, user_id=user_id, context=context) or False
333         if user_id and not section_id:
334             section_ids = self.pool.get('crm.case.section').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context)
335             if section_ids:
336                 section_id = section_ids[0]
337         return {'value': {'section_id': section_id}}
338
339     def stage_find(self, cr, uid, cases, section_id, domain=None, order='sequence', context=None):
340         """ Override of the base.stage method
341             Parameter of the stage search taken from the lead:
342             - type: stage type must be the same or 'both'
343             - section_id: if set, stages must belong to this section or
344               be a default stage; if not set, stages must be default
345               stages
346         """
347         if isinstance(cases, (int, long)):
348             cases = self.browse(cr, uid, cases, context=context)
349         if context is None:
350             context = {}
351         # check whether we should try to add a condition on type
352         avoid_add_type_term = any([term for term in domain if len(term) == 3 if term[0] == 'type'])
353         # collect all section_ids
354         section_ids = set()
355         types = ['both']
356         if not cases and context.get('default_type'):
357             ctx_type = context.get('default_type')
358             types += [ctx_type]
359         if section_id:
360             section_ids.add(section_id)
361         for lead in cases:
362             if lead.section_id:
363                 section_ids.add(lead.section_id.id)
364             if lead.type not in types:
365                 types.append(lead.type)
366         # OR all section_ids and OR with case_default
367         search_domain = []
368         if section_ids:
369             search_domain += [('|')] * len(section_ids)
370             for section_id in section_ids:
371                 search_domain.append(('section_ids', '=', section_id))
372         search_domain.append(('case_default', '=', True))
373         # AND with cases types
374         if not avoid_add_type_term:
375             search_domain.append(('type', 'in', types))
376         # AND with the domain in parameter
377         search_domain += list(domain)
378         # perform search, return the first found
379         stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, limit=1, context=context)
380         if stage_ids:
381             return stage_ids[0]
382         return False
383
384     def case_mark_lost(self, cr, uid, ids, context=None):
385         """ Mark the case as lost: state=cancel and probability=0
386         """
387         stages_leads = {}
388         for lead in self.browse(cr, uid, ids, context=context):
389             stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0), ('fold', '=', True), ('sequence', '>', 1)], context=context)
390             if stage_id:
391                 if stages_leads.get(stage_id):
392                     stages_leads[stage_id].append(lead.id)
393                 else:
394                     stages_leads[stage_id] = [lead.id]
395             else:
396                 raise osv.except_osv(_('Warning!'),
397                     _('To relieve your sales pipe and group all Lost opportunities, configure one of your sales stage as follow:\n'
398                         'probability = 0 %, select "Change Probability Automatically".\n'
399                         'Create a specific stage or edit an existing one by editing columns of your opportunity pipe.'))
400         for stage_id, lead_ids in stages_leads.items():
401             self.write(cr, uid, lead_ids, {'stage_id': stage_id}, context=context)
402         return True
403
404     def case_mark_won(self, cr, uid, ids, context=None):
405         """ Mark the case as won: state=done and probability=100
406         """
407         stages_leads = {}
408         for lead in self.browse(cr, uid, ids, context=context):
409             stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0), ('fold', '=', True)], context=context)
410             if stage_id:
411                 if stages_leads.get(stage_id):
412                     stages_leads[stage_id].append(lead.id)
413                 else:
414                     stages_leads[stage_id] = [lead.id]
415             else:
416                 raise osv.except_osv(_('Warning!'),
417                     _('To relieve your sales pipe and group all Won opportunities, configure one of your sales stage as follow:\n'
418                         'probability = 100 % and select "Change Probability Automatically".\n'
419                         'Create a specific stage or edit an existing one by editing columns of your opportunity pipe.'))
420         for stage_id, lead_ids in stages_leads.items():
421             self.write(cr, uid, lead_ids, {'stage_id': stage_id}, context=context)
422         return True
423
424     def case_escalate(self, cr, uid, ids, context=None):
425         """ Escalates case to parent level """
426         for case in self.browse(cr, uid, ids, context=context):
427             data = {'active': True}
428             if case.section_id.parent_id:
429                 data['section_id'] = case.section_id.parent_id.id
430                 if case.section_id.parent_id.change_responsible:
431                     if case.section_id.parent_id.user_id:
432                         data['user_id'] = case.section_id.parent_id.user_id.id
433             else:
434                 raise osv.except_osv(_('Error!'), _("You are already at the top level of your sales-team category.\nTherefore you cannot escalate furthermore."))
435             self.write(cr, uid, [case.id], data, context=context)
436         return True
437
438     def _merge_get_result_type(self, cr, uid, opps, context=None):
439         """
440         Define the type of the result of the merge.  If at least one of the
441         element to merge is an opp, the resulting new element will be an opp.
442         Otherwise it will be a lead.
443
444         We'll directly use a list of browse records instead of a list of ids
445         for performances' sake: it will spare a second browse of the
446         leads/opps.
447
448         :param list opps: list of browse records containing the leads/opps to process
449         :return string type: the type of the final element
450         """
451         for opp in opps:
452             if (opp.type == 'opportunity'):
453                 return 'opportunity'
454
455         return 'lead'
456
457     def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
458         """
459         Prepare lead/opp data into a dictionary for merging.  Different types
460         of fields are processed in different ways:
461         - text: all the values are concatenated
462         - m2m and o2m: those fields aren't processed
463         - m2o: the first not null value prevails (the other are dropped)
464         - any other type of field: same as m2o
465
466         :param list ids: list of ids of the leads to process
467         :param list fields: list of leads' fields to process
468         :return dict data: contains the merged values
469         """
470         opportunities = self.browse(cr, uid, ids, context=context)
471
472         def _get_first_not_null(attr):
473             for opp in opportunities:
474                 if hasattr(opp, attr) and bool(getattr(opp, attr)):
475                     return getattr(opp, attr)
476             return False
477
478         def _get_first_not_null_id(attr):
479             res = _get_first_not_null(attr)
480             return res and res.id or False
481
482         def _concat_all(attr):
483             return '\n\n'.join(filter(lambda x: x, [getattr(opp, attr) or '' for opp in opportunities if hasattr(opp, attr)]))
484
485         # Process the fields' values
486         data = {}
487         for field_name in fields:
488             field = self._fields.get(field_name)
489             if field is None:
490                 continue
491             if field.type in ('many2many', 'one2many'):
492                 continue
493             elif field.type == 'many2one':
494                 data[field_name] = _get_first_not_null_id(field_name)  # !!
495             elif field.type == 'text':
496                 data[field_name] = _concat_all(field_name)  #not lost
497             else:
498                 data[field_name] = _get_first_not_null(field_name)  #not lost
499
500         # Define the resulting type ('lead' or 'opportunity')
501         data['type'] = self._merge_get_result_type(cr, uid, opportunities, context)
502         return data
503
504     def _mail_body(self, cr, uid, lead, fields, title=False, context=None):
505         body = []
506         if title:
507             body.append("%s\n" % (title))
508
509         for field_name in fields:
510             field = self._fields.get(field_name)
511             if field is None:
512                 continue
513             value = ''
514
515             if field.type == 'selection':
516                 if callable(field.selection):
517                     key = field.selection(self, cr, uid, context=context)
518                 else:
519                     key = field.selection
520                 value = dict(key).get(lead[field_name], lead[field_name])
521             elif field.type == 'many2one':
522                 if lead[field_name]:
523                     value = lead[field_name].name_get()[0][1]
524             elif field.type == 'many2many':
525                 if lead[field_name]:
526                     for val in lead[field_name]:
527                         field_value = val.name_get()[0][1]
528                         value += field_value + ","
529             else:
530                 value = lead[field_name]
531
532             body.append("%s: %s" % (field.string, value or ''))
533         return "<br/>".join(body + ['<br/>'])
534
535     def _merge_notify(self, cr, uid, opportunity_id, opportunities, context=None):
536         """
537         Create a message gathering merged leads/opps information.
538         """
539         #TOFIX: mail template should be used instead of fix body, subject text
540         details = []
541         result_type = self._merge_get_result_type(cr, uid, opportunities, context)
542         if result_type == 'lead':
543             merge_message = _('Merged leads')
544         else:
545             merge_message = _('Merged opportunities')
546         subject = [merge_message]
547         for opportunity in opportunities:
548             subject.append(opportunity.name)
549             title = "%s : %s" % (opportunity.type == 'opportunity' and _('Merged opportunity') or _('Merged lead'), opportunity.name)
550             fields = list(CRM_LEAD_FIELDS_TO_MERGE)
551             details.append(self._mail_body(cr, uid, opportunity, fields, title=title, context=context))
552
553         # Chatter message's subject
554         subject = subject[0] + ": " + ", ".join(subject[1:])
555         details = "\n\n".join(details)
556         return self.message_post(cr, uid, [opportunity_id], body=details, subject=subject, context=context)
557
558     def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
559         message = self.pool.get('mail.message')
560         for opportunity in opportunities:
561             for history in opportunity.message_ids:
562                 message.write(cr, uid, history.id, {
563                         'res_id': opportunity_id,
564                         'subject' : _("From %s : %s") % (opportunity.name, history.subject)
565                 }, context=context)
566
567         return True
568
569     def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
570         attach_obj = self.pool.get('ir.attachment')
571
572         # return attachments of opportunity
573         def _get_attachments(opportunity_id):
574             attachment_ids = attach_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
575             return attach_obj.browse(cr, uid, attachment_ids, context=context)
576
577         first_attachments = _get_attachments(opportunity_id)
578         #counter of all attachments to move. Used to make sure the name is different for all attachments
579         count = 1
580         for opportunity in opportunities:
581             attachments = _get_attachments(opportunity.id)
582             for attachment in attachments:
583                 values = {'res_id': opportunity_id,}
584                 for attachment_in_first in first_attachments:
585                     if attachment.name == attachment_in_first.name:
586                         values['name'] = "%s (%s)" % (attachment.name, count,),
587                 count+=1
588                 attachment.write(values)
589         return True
590
591     def _merge_opportunity_phonecalls(self, cr, uid, opportunity_id, opportunities, context=None):
592         phonecall_obj = self.pool['crm.phonecall']
593         for opportunity in opportunities:
594             for phonecall_id in phonecall_obj.search(cr, uid, [('opportunity_id', '=', opportunity.id)], context=context):
595                 phonecall_obj.write(cr, uid, phonecall_id, {'opportunity_id': opportunity_id}, context=context)
596         return True
597
598     def get_duplicated_leads(self, cr, uid, ids, partner_id, include_lost=False, context=None):
599         """
600         Search for opportunities that have the same partner and that arent done or cancelled
601         """
602         lead = self.browse(cr, uid, ids[0], context=context)
603         email = lead.partner_id and lead.partner_id.email or lead.email_from
604         return self.pool['crm.lead']._get_duplicated_leads_by_emails(cr, uid, partner_id, email, include_lost=include_lost, context=context)
605
606     def _get_duplicated_leads_by_emails(self, cr, uid, partner_id, email, include_lost=False, context=None):
607         """
608         Search for opportunities that have   the same partner and that arent done or cancelled
609         """
610         final_stage_domain = [('stage_id.probability', '<', 100), '|', ('stage_id.probability', '>', 0), ('stage_id.sequence', '<=', 1)]
611         partner_match_domain = []
612         for email in set(email_split(email) + [email]):
613             partner_match_domain.append(('email_from', '=ilike', email))
614         if partner_id:
615             partner_match_domain.append(('partner_id', '=', partner_id))
616         partner_match_domain = ['|'] * (len(partner_match_domain) - 1) + partner_match_domain
617         if not partner_match_domain:
618             return []
619         domain = partner_match_domain
620         if not include_lost:
621             domain += final_stage_domain
622         return self.search(cr, uid, domain, context=context)
623
624     def merge_dependences(self, cr, uid, highest, opportunities, context=None):
625         self._merge_notify(cr, uid, highest, opportunities, context=context)
626         self._merge_opportunity_history(cr, uid, highest, opportunities, context=context)
627         self._merge_opportunity_attachments(cr, uid, highest, opportunities, context=context)
628         self._merge_opportunity_phonecalls(cr, uid, highest, opportunities, context=context)
629
630     def merge_opportunity(self, cr, uid, ids, user_id=False, section_id=False, context=None):
631         """
632         Different cases of merge:
633         - merge leads together = 1 new lead
634         - merge at least 1 opp with anything else (lead or opp) = 1 new opp
635
636         :param list ids: leads/opportunities ids to merge
637         :return int id: id of the resulting lead/opp
638         """
639         if context is None:
640             context = {}
641
642         if len(ids) <= 1:
643             raise osv.except_osv(_('Warning!'), _('Please select more than one element (lead or opportunity) from the list view.'))
644
645         opportunities = self.browse(cr, uid, ids, context=context)
646         sequenced_opps = []
647         # Sorting the leads/opps according to the confidence level of its stage, which relates to the probability of winning it
648         # The confidence level increases with the stage sequence, except when the stage probability is 0.0 (Lost cases)
649         # An Opportunity always has higher confidence level than a lead, unless its stage probability is 0.0
650         for opportunity in opportunities:
651             sequence = -1
652             if opportunity.stage_id and not opportunity.stage_id.fold:
653                 sequence = opportunity.stage_id.sequence
654             sequenced_opps.append(((int(sequence != -1 and opportunity.type == 'opportunity'), sequence, -opportunity.id), opportunity))
655
656         sequenced_opps.sort(reverse=True)
657         opportunities = map(itemgetter(1), sequenced_opps)
658         ids = [opportunity.id for opportunity in opportunities]
659         highest = opportunities[0]
660         opportunities_rest = opportunities[1:]
661
662         tail_opportunities = opportunities_rest
663
664         fields = list(CRM_LEAD_FIELDS_TO_MERGE)
665         merged_data = self._merge_data(cr, uid, ids, highest, fields, context=context)
666
667         if user_id:
668             merged_data['user_id'] = user_id
669         if section_id:
670             merged_data['section_id'] = section_id
671
672         # Merge notifications about loss of information
673         opportunities = [highest]
674         opportunities.extend(opportunities_rest)
675
676         self.merge_dependences(cr, uid, highest.id, tail_opportunities, context=context)
677
678         # Check if the stage is in the stages of the sales team. If not, assign the stage with the lowest sequence
679         if merged_data.get('section_id'):
680             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)
681             if merged_data.get('stage_id') not in section_stage_ids:
682                 merged_data['stage_id'] = section_stage_ids and section_stage_ids[0] or False
683         # Write merged data into first opportunity
684         self.write(cr, uid, [highest.id], merged_data, context=context)
685         # Delete tail opportunities 
686         # 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
687         self.unlink(cr, SUPERUSER_ID, [x.id for x in tail_opportunities], context=context)
688
689         return highest.id
690
691     def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
692         crm_stage = self.pool.get('crm.case.stage')
693         contact_id = False
694         if customer:
695             contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
696         if not section_id:
697             section_id = lead.section_id and lead.section_id.id or False
698         val = {
699             'planned_revenue': lead.planned_revenue,
700             'probability': lead.probability,
701             'name': lead.name,
702             'partner_id': customer and customer.id or False,
703             'type': 'opportunity',
704             'date_action': fields.datetime.now(),
705             'date_open': fields.datetime.now(),
706             'email_from': customer and customer.email or lead.email_from,
707             'phone': customer and customer.phone or lead.phone,
708         }
709         if not lead.stage_id or lead.stage_id.type=='lead':
710             val['stage_id'] = self.stage_find(cr, uid, [lead], section_id, [('type', 'in', ('opportunity', 'both'))], context=context)
711         return val
712
713     def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
714         customer = False
715         if partner_id:
716             partner = self.pool.get('res.partner')
717             customer = partner.browse(cr, uid, partner_id, context=context)
718         for lead in self.browse(cr, uid, ids, context=context):
719             # TDE: was if lead.state in ('done', 'cancel'):
720             if lead.probability == 100 or (lead.probability == 0 and lead.stage_id.fold):
721                 continue
722             vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
723             self.write(cr, uid, [lead.id], vals, context=context)
724
725         if user_ids or section_id:
726             self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
727
728         return True
729
730     def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
731         partner = self.pool.get('res.partner')
732         vals = {'name': name,
733             'user_id': lead.user_id.id,
734             'comment': lead.description,
735             'section_id': lead.section_id.id or False,
736             'parent_id': parent_id,
737             'phone': lead.phone,
738             'mobile': lead.mobile,
739             'email': tools.email_split(lead.email_from) and tools.email_split(lead.email_from)[0] or False,
740             'fax': lead.fax,
741             'title': lead.title and lead.title.id or False,
742             'function': lead.function,
743             'street': lead.street,
744             'street2': lead.street2,
745             'zip': lead.zip,
746             'city': lead.city,
747             'country_id': lead.country_id and lead.country_id.id or False,
748             'state_id': lead.state_id and lead.state_id.id or False,
749             'is_company': is_company,
750             'type': 'contact'
751         }
752         partner = partner.create(cr, uid, vals, context=context)
753         return partner
754
755     def _create_lead_partner(self, cr, uid, lead, context=None):
756         partner_id = False
757         if lead.partner_name and lead.contact_name:
758             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
759             partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
760         elif lead.partner_name and not lead.contact_name:
761             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
762         elif not lead.partner_name and lead.contact_name:
763             partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
764         elif lead.email_from and self.pool.get('res.partner')._parse_partner_name(lead.email_from, context=context)[0]:
765             contact_name = self.pool.get('res.partner')._parse_partner_name(lead.email_from, context=context)[0]
766             partner_id = self._lead_create_contact(cr, uid, lead, contact_name, False, context=context)
767         else:
768             raise osv.except_osv(
769                 _('Warning!'),
770                 _('No customer name defined. Please fill one of the following fields: Company Name, Contact Name or Email ("Name <email@address>")')
771             )
772         return partner_id
773
774     def handle_partner_assignation(self, cr, uid, ids, action='create', partner_id=False, context=None):
775         """
776         Handle partner assignation during a lead conversion.
777         if action is 'create', create new partner with contact and assign lead to new partner_id.
778         otherwise assign lead to the specified partner_id
779
780         :param list ids: leads/opportunities ids to process
781         :param string action: what has to be done regarding partners (create it, assign an existing one, or nothing)
782         :param int partner_id: partner to assign if any
783         :return dict: dictionary organized as followed: {lead_id: partner_assigned_id}
784         """
785         #TODO this is a duplication of the handle_partner_assignation method of crm_phonecall
786         partner_ids = {}
787         for lead in self.browse(cr, uid, ids, context=context):
788             # If the action is set to 'create' and no partner_id is set, create a new one
789             if lead.partner_id:
790                 partner_ids[lead.id] = lead.partner_id.id
791                 continue
792             if not partner_id and action == 'create':
793                 partner_id = self._create_lead_partner(cr, uid, lead, context)
794                 self.pool['res.partner'].write(cr, uid, partner_id, {'section_id': lead.section_id and lead.section_id.id or False})
795             if partner_id:
796                 lead.write({'partner_id': partner_id})
797             partner_ids[lead.id] = partner_id
798         return partner_ids
799
800     def allocate_salesman(self, cr, uid, ids, user_ids=None, team_id=False, context=None):
801         """
802         Assign salesmen and salesteam to a batch of leads.  If there are more
803         leads than salesmen, these salesmen will be assigned in round-robin.
804         E.g.: 4 salesmen (S1, S2, S3, S4) for 6 leads (L1, L2, ... L6).  They
805         will be assigned as followed: L1 - S1, L2 - S2, L3 - S3, L4 - S4,
806         L5 - S1, L6 - S2.
807
808         :param list ids: leads/opportunities ids to process
809         :param list user_ids: salesmen to assign
810         :param int team_id: salesteam to assign
811         :return bool
812         """
813         index = 0
814
815         for lead_id in ids:
816             value = {}
817             if team_id:
818                 value['section_id'] = team_id
819             if user_ids:
820                 value['user_id'] = user_ids[index]
821                 # Cycle through user_ids
822                 index = (index + 1) % len(user_ids)
823             if value:
824                 self.write(cr, uid, [lead_id], value, context=context)
825         return True
826
827     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):
828         """
829         :param string action: ('schedule','Schedule a call'), ('log','Log a call')
830         """
831         phonecall = self.pool.get('crm.phonecall')
832         model_data = self.pool.get('ir.model.data')
833         phonecall_dict = {}
834         if not categ_id:
835             try:
836                 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
837                 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
838             except ValueError:
839                 pass
840         for lead in self.browse(cr, uid, ids, context=context):
841             if not section_id:
842                 section_id = lead.section_id and lead.section_id.id or False
843             if not user_id:
844                 user_id = lead.user_id and lead.user_id.id or False
845             vals = {
846                 'name': call_summary,
847                 'opportunity_id': lead.id,
848                 'user_id': user_id or False,
849                 'categ_id': categ_id or False,
850                 'description': desc or '',
851                 'date': schedule_time,
852                 'section_id': section_id or False,
853                 'partner_id': lead.partner_id and lead.partner_id.id or False,
854                 'partner_phone': phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
855                 'partner_mobile': lead.partner_id and lead.partner_id.mobile or False,
856                 'priority': lead.priority,
857             }
858             new_id = phonecall.create(cr, uid, vals, context=context)
859             phonecall.write(cr, uid, [new_id], {'state': 'open'}, context=context)
860             if action == 'log':
861                 phonecall.write(cr, uid, [new_id], {'state': 'done'}, context=context)
862             phonecall_dict[lead.id] = new_id
863             self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
864         return phonecall_dict
865
866     def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
867         models_data = self.pool.get('ir.model.data')
868
869         # Get opportunity views
870         dummy, form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
871         dummy, tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
872         return {
873             'name': _('Opportunity'),
874             'view_type': 'form',
875             'view_mode': 'tree, form',
876             'res_model': 'crm.lead',
877             'domain': [('type', '=', 'opportunity')],
878             'res_id': int(opportunity_id),
879             'view_id': False,
880             'views': [(form_view or False, 'form'),
881                       (tree_view or False, 'tree'), (False, 'kanban'),
882                       (False, 'calendar'), (False, 'graph')],
883             'type': 'ir.actions.act_window',
884         }
885
886     def redirect_lead_view(self, cr, uid, lead_id, context=None):
887         models_data = self.pool.get('ir.model.data')
888
889         # Get lead views
890         dummy, form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_leads')
891         dummy, tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_leads')
892         return {
893             'name': _('Lead'),
894             'view_type': 'form',
895             'view_mode': 'tree, form',
896             'res_model': 'crm.lead',
897             'domain': [('type', '=', 'lead')],
898             'res_id': int(lead_id),
899             'view_id': False,
900             'views': [(form_view or False, 'form'),
901                       (tree_view or False, 'tree'),
902                       (False, 'calendar'), (False, 'graph')],
903             'type': 'ir.actions.act_window',
904         }
905
906     def action_schedule_meeting(self, cr, uid, ids, context=None):
907         """
908         Open meeting's calendar view to schedule meeting on current opportunity.
909         :return dict: dictionary value for created Meeting view
910         """
911         lead = self.browse(cr, uid, ids[0], context)
912         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'calendar', 'action_calendar_event', context)
913         partner_ids = [self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id.id]
914         if lead.partner_id:
915             partner_ids.append(lead.partner_id.id)
916         res['context'] = {
917             'default_opportunity_id': lead.type == 'opportunity' and lead.id or False,
918             'default_partner_id': lead.partner_id and lead.partner_id.id or False,
919             'default_partner_ids': partner_ids,
920             'default_section_id': lead.section_id and lead.section_id.id or False,
921             'default_name': lead.name,
922         }
923         return res
924
925     def create(self, cr, uid, vals, context=None):
926         context = dict(context or {})
927         if vals.get('type') and not context.get('default_type'):
928             context['default_type'] = vals.get('type')
929         if vals.get('section_id') and not context.get('default_section_id'):
930             context['default_section_id'] = vals.get('section_id')
931         if vals.get('user_id'):
932             vals['date_open'] = fields.datetime.now()
933
934         # context: no_log, because subtype already handle this
935         create_context = dict(context, mail_create_nolog=True)
936         return super(crm_lead, self).create(cr, uid, vals, context=create_context)
937
938     def write(self, cr, uid, ids, vals, context=None):
939         # stage change: update date_last_stage_update
940         if 'stage_id' in vals:
941             vals['date_last_stage_update'] = fields.datetime.now()
942         if vals.get('user_id'):
943             vals['date_open'] = fields.datetime.now()
944         # stage change with new stage: update probability and date_closed
945         if vals.get('stage_id') and not vals.get('probability'):
946             onchange_stage_values = self.onchange_stage_id(cr, uid, ids, vals.get('stage_id'), context=context)['value']
947             vals.update(onchange_stage_values)
948         return super(crm_lead, self).write(cr, uid, ids, vals, context=context)
949
950     def copy(self, cr, uid, id, default=None, context=None):
951         if not default:
952             default = {}
953         if not context:
954             context = {}
955         lead = self.browse(cr, uid, id, context=context)
956         local_context = dict(context)
957         local_context.setdefault('default_type', lead.type)
958         local_context.setdefault('default_section_id', lead.section_id.id)
959         if lead.type == 'opportunity':
960             default['date_open'] = fields.datetime.now()
961         else:
962             default['date_open'] = False
963         return super(crm_lead, self).copy(cr, uid, id, default, context=local_context)
964
965     def get_empty_list_help(self, cr, uid, help, context=None):
966         context = dict(context or {})
967         context['empty_list_help_model'] = 'crm.case.section'
968         context['empty_list_help_id'] = context.get('default_section_id', None)
969         context['empty_list_help_document_name'] = _("opportunity")
970         if context.get('default_type') == 'lead':
971             context['empty_list_help_document_name'] = _("lead")
972         return super(crm_lead, self).get_empty_list_help(cr, uid, help, context=context)
973
974     # ----------------------------------------
975     # Mail Gateway
976     # ----------------------------------------
977
978     def message_get_reply_to(self, cr, uid, ids, context=None):
979         """ Override to get the reply_to of the parent project. """
980         leads = self.browse(cr, SUPERUSER_ID, ids, context=context)
981         section_ids = set([lead.section_id.id for lead in leads if lead.section_id])
982         aliases = self.pool['crm.case.section'].message_get_reply_to(cr, uid, list(section_ids), context=context)
983         return dict((lead.id, aliases.get(lead.section_id and lead.section_id.id or 0, False)) for lead in leads)
984
985     def get_formview_id(self, cr, uid, id, context=None):
986         obj = self.browse(cr, uid, id, context=context)
987         if obj.type == 'opportunity':
988             model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
989         else:
990             view_id = super(crm_lead, self).get_formview_id(cr, uid, id, context=context)
991         return view_id
992
993     def message_get_suggested_recipients(self, cr, uid, ids, context=None):
994         recipients = super(crm_lead, self).message_get_suggested_recipients(cr, uid, ids, context=context)
995         try:
996             for lead in self.browse(cr, uid, ids, context=context):
997                 if lead.partner_id:
998                     self._message_add_suggested_recipient(cr, uid, recipients, lead, partner=lead.partner_id, reason=_('Customer'))
999                 elif lead.email_from:
1000                     self._message_add_suggested_recipient(cr, uid, recipients, lead, email=lead.email_from, reason=_('Customer Email'))
1001         except (osv.except_osv, orm.except_orm):  # no read access rights -> just ignore suggested recipients because this imply modifying followers
1002             pass
1003         return recipients
1004
1005     def message_new(self, cr, uid, msg, custom_values=None, context=None):
1006         """ Overrides mail_thread message_new that is called by the mailgateway
1007             through message_process.
1008             This override updates the document according to the email.
1009         """
1010         if custom_values is None:
1011             custom_values = {}
1012         defaults = {
1013             'name':  msg.get('subject') or _("No Subject"),
1014             'email_from': msg.get('from'),
1015             'email_cc': msg.get('cc'),
1016             'partner_id': msg.get('author_id', False),
1017             'user_id': False,
1018         }
1019         if msg.get('author_id'):
1020             defaults.update(self.on_change_partner_id(cr, uid, None, msg.get('author_id'), context=context)['value'])
1021         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
1022             defaults['priority'] = msg.get('priority')
1023         defaults.update(custom_values)
1024         return super(crm_lead, self).message_new(cr, uid, msg, custom_values=defaults, context=context)
1025
1026     def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
1027         """ Overrides mail_thread message_update that is called by the mailgateway
1028             through message_process.
1029             This method updates the document according to the email.
1030         """
1031         if isinstance(ids, (str, int, long)):
1032             ids = [ids]
1033         if update_vals is None: update_vals = {}
1034
1035         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
1036             update_vals['priority'] = msg.get('priority')
1037         maps = {
1038             'cost':'planned_cost',
1039             'revenue': 'planned_revenue',
1040             'probability':'probability',
1041         }
1042         for line in msg.get('body', '').split('\n'):
1043             line = line.strip()
1044             res = tools.command_re.match(line)
1045             if res and maps.get(res.group(1).lower()):
1046                 key = maps.get(res.group(1).lower())
1047                 update_vals[key] = res.group(2).lower()
1048
1049         return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
1050
1051     # ----------------------------------------
1052     # OpenChatter methods and notifications
1053     # ----------------------------------------
1054
1055     def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
1056         phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
1057         if action == 'log':
1058             message = _('Logged a call for %(date)s. %(description)s')
1059         else:
1060             message = _('Scheduled a call for %(date)s. %(description)s')
1061         phonecall_date = datetime.strptime(phonecall.date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
1062         phonecall_usertime = fields.datetime.context_timestamp(cr, uid, phonecall_date, context=context).strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
1063         html_time = "<time datetime='%s+00:00'>%s</time>" % (phonecall.date, phonecall_usertime)
1064         message = message % dict(date=html_time, description=phonecall.description)
1065         return self.message_post(cr, uid, ids, body=message, context=context)
1066
1067     def log_meeting(self, cr, uid, ids, meeting_subject, meeting_date, duration, context=None):
1068         if not duration:
1069             duration = _('unknown')
1070         else:
1071             duration = str(duration)
1072         message = _("Meeting scheduled at '%s'<br> Subject: %s <br> Duration: %s hour(s)") % (meeting_date, meeting_subject, duration)
1073         return self.message_post(cr, uid, ids, body=message, context=context)
1074
1075     def onchange_state(self, cr, uid, ids, state_id, context=None):
1076         if state_id:
1077             country_id=self.pool.get('res.country.state').browse(cr, uid, state_id, context).country_id.id
1078             return {'value':{'country_id':country_id}}
1079         return {}
1080
1081     def message_partner_info_from_emails(self, cr, uid, id, emails, link_mail=False, context=None):
1082         res = super(crm_lead, self).message_partner_info_from_emails(cr, uid, id, emails, link_mail=link_mail, context=context)
1083         lead = self.browse(cr, uid, id, context=context)
1084         for partner_info in res:
1085             if not partner_info.get('partner_id') and (lead.partner_name or lead.contact_name):
1086                 emails = email_re.findall(partner_info['full_name'] or '')
1087                 email = emails and emails[0] or ''
1088                 if email and lead.email_from and email.lower() == lead.email_from.lower():
1089                     partner_info['full_name'] = '%s <%s>' % (lead.partner_name or lead.contact_name, email)
1090                     break
1091         return res
1092
1093 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: