[CLEAN] CRM: cleaned a bit message_post and fixed subtype names.
[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 from openerp.addons.base_status.base_stage import base_stage
23 import crm
24 from datetime import datetime
25 from openerp.osv import fields, osv
26 import time
27 from openerp import tools
28 from openerp.tools.translate import _
29 from openerp.tools import html2plaintext
30
31 from base.res.res_partner import format_address
32
33 CRM_LEAD_FIELDS_TO_MERGE = ['name',
34     'partner_id',
35     'channel_id',
36     'company_id',
37     'country_id',
38     'section_id',
39     'stage_id',
40     'state_id',
41     'type_id',
42     'user_id',
43     'title',
44     'city',
45     'contact_name',
46     'description',
47     'email',
48     'fax',
49     'mobile',
50     'partner_name',
51     'phone',
52     'probability',
53     'planned_revenue',
54     'street',
55     'street2',
56     'zip',
57     'create_date',
58     'date_action_last',
59     'date_action_next',
60     'email_from',
61     'email_cc',
62     'partner_name']
63 CRM_LEAD_PENDING_STATES = (
64     crm.AVAILABLE_STATES[2][0], # Cancelled
65     crm.AVAILABLE_STATES[3][0], # Done
66     crm.AVAILABLE_STATES[4][0], # Pending
67 )
68
69 class crm_lead(base_stage, format_address, osv.osv):
70     """ CRM Lead Case """
71     _name = "crm.lead"
72     _description = "Lead/Opportunity"
73     _order = "priority,date_action,id desc"
74     _inherit = ['mail.thread', 'ir.needaction_mixin']
75
76     _track = {
77         'stage_id': {
78             'crm.mt_lead_won': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.state == 'done' and obj.probability == 100.0,
79             'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.state == 'cancel' and obj.probability == 0.0,
80             'crm.mt_lead_stage': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.state not in ['cancel', 'done'],
81         },
82     }
83
84     def _get_default_section_id(self, cr, uid, context=None):
85         """ Gives default section by checking if present in the context """
86         return (self._resolve_section_id_from_context(cr, uid, context=context) or False)
87
88     def _get_default_stage_id(self, cr, uid, context=None):
89         """ Gives default stage_id """
90         section_id = self._get_default_section_id(cr, uid, context=context)
91         return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft')], context=context)
92
93     def _resolve_section_id_from_context(self, cr, uid, context=None):
94         """ Returns ID of section based on the value of 'section_id'
95             context key, or None if it cannot be resolved to a single
96             Sales Team.
97         """
98         if context is None:
99             context = {}
100         if type(context.get('default_section_id')) in (int, long):
101             return context.get('default_section_id')
102         if isinstance(context.get('default_section_id'), basestring):
103             section_name = context['default_section_id']
104             section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=section_name, context=context)
105             if len(section_ids) == 1:
106                 return int(section_ids[0][0])
107         return None
108
109     def _resolve_type_from_context(self, cr, uid, context=None):
110         """ Returns the type (lead or opportunity) from the type context
111             key. Returns None if it cannot be resolved.
112         """
113         if context is None:
114             context = {}
115         return context.get('default_type')
116
117     def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
118         access_rights_uid = access_rights_uid or uid
119         stage_obj = self.pool.get('crm.case.stage')
120         order = stage_obj._order
121         # lame hack to allow reverting search, should just work in the trivial case
122         if read_group_order == 'stage_id desc':
123             order = "%s desc" % order
124         # retrieve section_id from the context and write the domain
125         # - ('id', 'in', 'ids'): add columns that should be present
126         # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
127         # - OR ('section_ids', '=', section_id), ('fold', '=', False) if section_id: add section columns that are not folded
128         search_domain = []
129         section_id = self._resolve_section_id_from_context(cr, uid, context=context)
130         if section_id:
131             search_domain += ['|', ('section_ids', '=', section_id)]
132             search_domain += [('id', 'in', ids)]
133         else:
134             search_domain += ['|', ('id', 'in', ids), ('case_default', '=', True)]
135         # retrieve type from the context (if set: choose 'type' or 'both')
136         type = self._resolve_type_from_context(cr, uid, context=context)
137         if type:
138             search_domain += ['|', ('type', '=', type), ('type', '=', 'both')]
139         # perform search
140         stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
141         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
142         # restore order of the search
143         result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
144
145         fold = {}
146         for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
147             fold[stage.id] = stage.fold or False
148         return result, fold
149
150     def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
151         res = super(crm_lead,self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
152         if view_type == 'form':
153             res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
154         return res
155
156     _group_by_full = {
157         'stage_id': _read_group_stage_ids
158     }
159
160     def _compute_day(self, cr, uid, ids, fields, args, context=None):
161         """
162         :return dict: difference between current date and log date
163         """
164         cal_obj = self.pool.get('resource.calendar')
165         res_obj = self.pool.get('resource.resource')
166
167         res = {}
168         for lead in self.browse(cr, uid, ids, context=context):
169             for field in fields:
170                 res[lead.id] = {}
171                 duration = 0
172                 ans = False
173                 if field == 'day_open':
174                     if lead.date_open:
175                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
176                         date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
177                         ans = date_open - date_create
178                         date_until = lead.date_open
179                 elif field == 'day_close':
180                     if lead.date_closed:
181                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
182                         date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
183                         date_until = lead.date_closed
184                         ans = date_close - date_create
185                 if ans:
186                     resource_id = False
187                     if lead.user_id:
188                         resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
189                         if len(resource_ids):
190                             resource_id = resource_ids[0]
191
192                     duration = float(ans.days)
193                     if lead.section_id and lead.section_id.resource_calendar_id:
194                         duration =  float(ans.days) * 24
195                         new_dates = cal_obj.interval_get(cr,
196                             uid,
197                             lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
198                             datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
199                             duration,
200                             resource=resource_id
201                         )
202                         no_days = []
203                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
204                         for in_time, out_time in new_dates:
205                             if in_time.date not in no_days:
206                                 no_days.append(in_time.date)
207                             if out_time > date_until:
208                                 break
209                         duration =  len(no_days)
210                 res[lead.id][field] = abs(int(duration))
211         return res
212
213     def _history_search(self, cr, uid, obj, name, args, context=None):
214         res = []
215         msg_obj = self.pool.get('mail.message')
216         message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
217         lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
218
219         if lead_ids:
220             return [('id', 'in', lead_ids)]
221         else:
222             return [('id', '=', '0')]
223
224     _columns = {
225         'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
226             select=True, help="Linked partner (optional). Usually created when converting the lead."),
227
228         'id': fields.integer('ID', readonly=True),
229         'name': fields.char('Subject', size=64, required=True, select=1),
230         'active': fields.boolean('Active', required=False),
231         'date_action_last': fields.datetime('Last Action', readonly=1),
232         'date_action_next': fields.datetime('Next Action', readonly=1),
233         'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
234         'section_id': fields.many2one('crm.case.section', 'Sales Team',
235                         select=True, track_visibility=1, help='When sending mails, the default email address is taken from the sales team.'),
236         'create_date': fields.datetime('Creation Date' , readonly=True),
237         '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"),
238         'description': fields.text('Notes'),
239         'write_date': fields.datetime('Update Date' , readonly=True),
240         'categ_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Categories', \
241             domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
242         'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
243             domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
244         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
245         'contact_name': fields.char('Contact Name', size=64),
246         '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),
247         'opt_out': fields.boolean('Opt-Out', oldname='optout', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
248         'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
249         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
250         'date_closed': fields.datetime('Closed', readonly=True),
251         'stage_id': fields.many2one('crm.case.stage', 'Stage', track_visibility=1,
252                         domain="[('fold', '=', False), ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"),
253         'user_id': fields.many2one('res.users', 'Salesperson', track_visibility=1),
254         'referred': fields.char('Referred By', size=64),
255         'date_open': fields.datetime('Opened', readonly=True),
256         'day_open': fields.function(_compute_day, string='Days to Open', \
257                                 multi='day_open', type="float", store=True),
258         'day_close': fields.function(_compute_day, string='Days to Close', \
259                                 multi='day_close', type="float", store=True),
260         'state': fields.related('stage_id', 'state', type="selection", store=True,
261                 selection=crm.AVAILABLE_STATES, string="Status", readonly=True,
262                 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\'.'),
263
264         # Only used for type opportunity
265         'probability': fields.float('Success Rate (%)',group_operator="avg"),
266         'planned_revenue': fields.float('Expected Revenue', track_visibility=2),
267         'ref': fields.reference('Reference', selection=crm._links_get, size=128),
268         'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
269         'phone': fields.char("Phone", size=64),
270         'date_deadline': fields.date('Expected Closing', help="Estimate of the date on which the opportunity will be won."),
271         'date_action': fields.date('Next Action Date', select=True),
272         'title_action': fields.char('Next Action', size=64),
273         'color': fields.integer('Color Index'),
274         'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
275         'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
276         'company_currency': fields.related('company_id', 'currency_id', type='many2one', string='Currency', readonly=True, relation="res.currency"),
277         'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
278         'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
279
280         # Fields for address, due to separation from crm and res.partner
281         'street': fields.char('Street', size=128),
282         'street2': fields.char('Street2', size=128),
283         'zip': fields.char('Zip', change_default=True, size=24),
284         'city': fields.char('City', size=128),
285         'state_id': fields.many2one("res.country.state", 'State'),
286         'country_id': fields.many2one('res.country', 'Country'),
287         'phone': fields.char('Phone', size=64),
288         'fax': fields.char('Fax', size=64),
289         'mobile': fields.char('Mobile', size=64),
290         'function': fields.char('Function', size=128),
291         'title': fields.many2one('res.partner.title', 'Title'),
292         'company_id': fields.many2one('res.company', 'Company', select=1),
293         'payment_mode': fields.many2one('crm.payment.mode', 'Payment Mode', \
294                             domain="[('section_id','=',section_id)]"),
295         'planned_cost': fields.float('Planned Costs'),
296     }
297
298     _defaults = {
299         'active': 1,
300         'type': 'lead',
301         'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
302         'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
303         'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
304         'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
305         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
306         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
307         'color': 0,
308     }
309
310     _sql_constraints = [
311         ('check_probability', 'check(probability >= 0 and probability <= 100)', 'The probability of closing the deal should be between 0% and 100%!')
312     ]
313
314     def create(self, cr, uid, vals, context=None):
315         obj_id = super(crm_lead, self).create(cr, uid, vals, context)
316         section_id = self.browse(cr, uid, obj_id, context=context).section_id
317         if section_id:
318             # subscribe salesteam followers & subtypes to the lead
319             self._subscribe_followers_subtype(cr, uid, [obj_id], section_id, 'crm.case.section', context=context)
320         return obj_id
321
322     def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
323         if not stage_id:
324             return {'value':{}}
325         stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
326         if not stage.on_change:
327             return {'value':{}}
328         return {'value':{'probability': stage.probability}}
329
330     def on_change_partner(self, cr, uid, ids, partner_id, context=None):
331         result = {}
332         values = {}
333         if partner_id:
334             partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
335             values = {
336                 'partner_name' : partner.name,
337                 'street' : partner.street,
338                 'street2' : partner.street2,
339                 'city' : partner.city,
340                 'state_id' : partner.state_id and partner.state_id.id or False,
341                 'country_id' : partner.country_id and partner.country_id.id or False,
342                 'email_from' : partner.email,
343                 'phone' : partner.phone,
344                 'mobile' : partner.mobile,
345                 'fax' : partner.fax,
346             }
347         return {'value' : values}
348
349     def _check(self, cr, uid, ids=False, context=None):
350         """ Override of the base.stage method.
351             Function called by the scheduler to process cases for date actions
352             Only works on not done and cancelled cases
353         """
354         cr.execute('select * from crm_case \
355                 where (date_action_last<%s or date_action_last is null) \
356                 and (date_action_next<=%s or date_action_next is null) \
357                 and state not in (\'cancel\',\'done\')',
358                 (time.strftime("%Y-%m-%d %H:%M:%S"),
359                     time.strftime('%Y-%m-%d %H:%M:%S')))
360
361         ids2 = map(lambda x: x[0], cr.fetchall() or [])
362         cases = self.browse(cr, uid, ids2, context=context)
363         return self._action(cr, uid, cases, False, context=context)
364
365     def stage_find(self, cr, uid, cases, section_id, domain=None, order='sequence', context=None):
366         """ Override of the base.stage method
367             Parameter of the stage search taken from the lead:
368             - type: stage type must be the same or 'both'
369             - section_id: if set, stages must belong to this section or
370               be a default stage; if not set, stages must be default
371               stages
372         """
373         if isinstance(cases, (int, long)):
374             cases = self.browse(cr, uid, cases, context=context)
375         # collect all section_ids
376         section_ids = []
377         types = ['both']
378         if not cases :
379             type = context.get('default_type')
380             types += [type]
381         if section_id:
382             section_ids.append(section_id)
383         for lead in cases:
384             if lead.section_id:
385                 section_ids.append(lead.section_id.id)
386             if lead.type not in types:
387                 types.append(lead.type)
388         # OR all section_ids and OR with case_default
389         search_domain = []
390         if section_ids:
391             search_domain += [('|')] * len(section_ids)
392             for section_id in section_ids:
393                 search_domain.append(('section_ids', '=', section_id))
394         search_domain.append(('case_default', '=', True))
395         # AND with cases types
396         search_domain.append(('type', 'in', types))
397         # AND with the domain in parameter
398         search_domain += list(domain)
399         # perform search, return the first found
400         stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
401         if stage_ids:
402             return stage_ids[0]
403         return False
404
405     def case_cancel(self, cr, uid, ids, context=None):
406         """ Overrides case_cancel from base_stage to set probability """
407         res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
408         self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
409         return res
410
411     def case_reset(self, cr, uid, ids, context=None):
412         """ Overrides case_reset from base_stage to set probability """
413         res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
414         self.write(cr, uid, ids, {'probability': 0.0}, context=context)
415         return res
416
417     def case_mark_lost(self, cr, uid, ids, context=None):
418         """ Mark the case as lost: state=cancel and probability=0 """
419         for lead in self.browse(cr, uid, ids):
420             stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
421             if stage_id:
422                 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
423         return True
424
425     def case_mark_won(self, cr, uid, ids, context=None):
426         """ Mark the case as lost: state=done and probability=100 """
427         for lead in self.browse(cr, uid, ids):
428             stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
429             if stage_id:
430                 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
431         return True
432
433     def set_priority(self, cr, uid, ids, priority):
434         """ Set lead priority
435         """
436         return self.write(cr, uid, ids, {'priority' : priority})
437
438     def set_high_priority(self, cr, uid, ids, context=None):
439         """ Set lead priority to high
440         """
441         return self.set_priority(cr, uid, ids, '1')
442
443     def set_normal_priority(self, cr, uid, ids, context=None):
444         """ Set lead priority to normal
445         """
446         return self.set_priority(cr, uid, ids, '3')
447
448     def _merge_get_result_type(self, cr, uid, opps, context=None):
449         """
450         Define the type of the result of the merge.  If at least one of the
451         element to merge is an opp, the resulting new element will be an opp.
452         Otherwise it will be a lead.
453
454         We'll directly use a list of browse records instead of a list of ids
455         for performances' sake: it will spare a second browse of the
456         leads/opps.
457
458         :param list opps: list of browse records containing the leads/opps to process
459         :return string type: the type of the final element
460         """
461         for opp in opps:
462             if (opp.type == 'opportunity'):
463                 return 'opportunity'
464
465         return 'lead'
466
467     def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
468         """
469         Prepare lead/opp data into a dictionary for merging.  Different types
470         of fields are processed in different ways:
471         - text: all the values are concatenated
472         - m2m and o2m: those fields aren't processed
473         - m2o: the first not null value prevails (the other are dropped)
474         - any other type of field: same as m2o
475
476         :param list ids: list of ids of the leads to process
477         :param list fields: list of leads' fields to process
478         :return dict data: contains the merged values
479         """
480         opportunities = self.browse(cr, uid, ids, context=context)
481
482         def _get_first_not_null(attr):
483             if hasattr(oldest, attr):
484                 return getattr(oldest, attr)
485             for opp in opportunities:
486                 if hasattr(opp, attr):
487                     return getattr(opp, attr)
488             return False
489
490         def _get_first_not_null_id(attr):
491             res = _get_first_not_null(attr)
492             return res and res.id or False
493
494         def _concat_all(attr):
495             return ', '.join(filter(lambda x: x, [getattr(opp, attr) or '' for opp in opportunities if hasattr(opp, attr)]))
496
497         # Process the fields' values
498         data = {}
499         for field_name in fields:
500             field_info = self._all_columns.get(field_name)
501             if field_info is None:
502                 continue
503             field = field_info.column
504             if field._type in ('many2many', 'one2many'):
505                 continue
506             elif field._type == 'many2one':
507                 data[field_name] = _get_first_not_null_id(field_name)  # !!
508             elif field._type == 'text':
509                 data[field_name] = _concat_all(field_name)  #not lost
510             else:
511                 data[field_name] = _get_first_not_null(field_name)  #not lost
512
513         # Define the resulting type ('lead' or 'opportunity')
514         data['type'] = self._merge_get_result_type(cr, uid, opportunities, context)
515
516         return data
517
518     def _merge_find_oldest(self, cr, uid, ids, context=None):
519         """
520         Return the oldest lead found among ids.
521
522         :param list ids: list of ids of the leads to inspect
523         :return object: browse record of the oldest of the leads
524         """
525         if context is None:
526             context = {}
527
528         if context.get('convert'):
529             ids = list(set(ids) - set(context.get('lead_ids', [])))
530
531         # Search opportunities order by create date
532         opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date', context=context)
533         oldest_opp_id = opportunity_ids[0]
534         return self.browse(cr, uid, oldest_opp_id, context=context)
535
536     def _mail_body(self, cr, uid, lead, fields, title=False, context=None):
537         body = []
538         if title:
539             body.append("%s\n" % (title))
540
541         for field_name in fields:
542             field_info = self._all_columns.get(field_name)
543             if field_info is None:
544                 continue
545             field = field_info.column
546             value = ''
547
548             if field._type == 'selection':
549                 if hasattr(field.selection, '__call__'):
550                     key = field.selection(self, cr, uid, context=context)
551                 else:
552                     key = field.selection
553                 value = dict(key).get(lead[field_name], lead[field_name])
554             elif field._type == 'many2one':
555                 if lead[field_name]:
556                     value = lead[field_name].name_get()[0][1]
557             elif field._type == 'many2many':
558                 if lead[field_name]:
559                     for val in lead[field_name]:
560                         field_value = val.name_get()[0][1]
561                         value += field_value + ","
562             else:
563                 value = lead[field_name]
564
565             body.append("%s: %s" % (field.string, value or ''))
566         return "<br/>".join(body + ['<br/>'])
567
568     def _merge_notify(self, cr, uid, opportunity_id, opportunities, context=None):
569         """
570         Create a message gathering merged leads/opps information.
571         """
572         #TOFIX: mail template should be used instead of fix body, subject text
573         details = []
574         result_type = self._merge_get_result_type(cr, uid, opportunities, context)
575         if result_type == 'lead':
576             merge_message = _('Merged leads')
577         else:
578             merge_message = _('Merged opportunities')
579         subject = [merge_message]
580         for opportunity in opportunities:
581             subject.append(opportunity.name)
582             title = "%s : %s" % (merge_message, opportunity.name)
583             details.append(self._mail_body(cr, uid, opportunity, CRM_LEAD_FIELDS_TO_MERGE, title=title, context=context))
584
585         # Chatter message's subject
586         subject = subject[0] + ": " + ", ".join(subject[1:])
587         details = "\n\n".join(details)
588         return self.message_post(cr, uid, [opportunity_id], body=details, subject=subject, context=context)
589
590     def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
591         message = self.pool.get('mail.message')
592         for opportunity in opportunities:
593             for history in opportunity.message_ids:
594                 message.write(cr, uid, history.id, {
595                         'res_id': opportunity_id,
596                         'subject' : _("From %s : %s") % (opportunity.name, history.subject)
597                 }, context=context)
598
599         return True
600
601     def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
602         attachment = self.pool.get('ir.attachment')
603
604         # return attachments of opportunity
605         def _get_attachments(opportunity_id):
606             attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
607             return attachment.browse(cr, uid, attachment_ids, context=context)
608
609         count = 1
610         first_attachments = _get_attachments(opportunity_id)
611         for opportunity in opportunities:
612             attachments = _get_attachments(opportunity.id)
613             for first in first_attachments:
614                 for attachment in attachments:
615                     if attachment.name == first.name:
616                         values = dict(
617                             name = "%s (%s)" % (attachment.name, count,),
618                             res_id = opportunity_id,
619                         )
620                         attachment.write(values)
621                         count+=1
622
623         return True
624
625     def merge_opportunity(self, cr, uid, ids, context=None):
626         """
627         Different cases of merge:
628         - merge leads together = 1 new lead
629         - merge at least 1 opp with anything else (lead or opp) = 1 new opp
630
631         :param list ids: leads/opportunities ids to merge
632         :return int id: id of the resulting lead/opp
633         """
634         if context is None: context = {}
635
636         if len(ids) <= 1:
637             raise osv.except_osv(_('Warning!'),_('Please select more than one element (lead or opportunity) from the list view.'))
638
639         lead_ids = context.get('lead_ids', [])
640
641         ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
642         opportunities = self.browse(cr, uid, ids, context=context)
643         opportunities_list = list(set(opportunities) - set(ctx_opportunities))
644         oldest = self._merge_find_oldest(cr, uid, ids, context=context)
645         if ctx_opportunities:
646             first_opportunity = ctx_opportunities[0]
647             tail_opportunities = opportunities_list + ctx_opportunities[1:]
648         else:
649             first_opportunity = opportunities_list[0]
650             tail_opportunities = opportunities_list[1:]
651
652         merged_data = self._merge_data(cr, uid, ids, oldest, CRM_LEAD_FIELDS_TO_MERGE, context=context)
653
654         # Merge messages and attachements into the first opportunity
655         self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
656         self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
657
658         # Merge notifications about loss of information
659         self._merge_notify(cr, uid, first_opportunity, opportunities, context=context)
660         # Write merged data into first opportunity
661         self.write(cr, uid, [first_opportunity.id], merged_data, context=context)
662         # Delete tail opportunities
663         self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
664
665         # Open first opportunity
666         self.case_open(cr, uid, [first_opportunity.id])
667         return first_opportunity.id
668
669     def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
670         crm_stage = self.pool.get('crm.case.stage')
671         contact_id = False
672         if customer:
673             contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
674
675         if not section_id:
676             section_id = lead.section_id and lead.section_id.id or False
677
678         if section_id:
679             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
680         else:
681             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
682         stage_id = stage_ids and stage_ids[0] or False
683
684         return {
685             'planned_revenue': lead.planned_revenue,
686             'probability': lead.probability,
687             'name': lead.name,
688             'partner_id': customer and customer.id or False,
689             'user_id': (lead.user_id and lead.user_id.id),
690             'type': 'opportunity',
691             'stage_id': stage_id or False,
692             'date_action': fields.datetime.now(),
693             'date_open': fields.datetime.now(),
694             'email_from': customer and customer.email or lead.email_from,
695             'phone': customer and customer.phone or lead.phone,
696         }
697
698     def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
699         customer = False
700         if partner_id:
701             partner = self.pool.get('res.partner')
702             customer = partner.browse(cr, uid, partner_id, context=context)
703         for lead in self.browse(cr, uid, ids, context=context):
704             if lead.state in ('done', 'cancel'):
705                 continue
706             vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
707             self.write(cr, uid, [lead.id], vals, context=context)
708         self.message_post(cr, uid, ids, body=_("Lead has been <b>converted to an opportunity</b>."), subtype="crm.mt_lead_convert_to_opportunity", context=context)
709
710         if user_ids or section_id:
711             self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
712
713         return True
714
715     def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
716         partner = self.pool.get('res.partner')
717         vals = { 'name': name,
718             'user_id': lead.user_id.id,
719             'comment': lead.description,
720             'section_id': lead.section_id.id or False,
721             'parent_id': parent_id,
722             'phone': lead.phone,
723             'mobile': lead.mobile,
724             'email': lead.email_from and tools.email_split(lead.email_from)[0],
725             'fax': lead.fax,
726             'title': lead.title and lead.title.id or False,
727             'function': lead.function,
728             'street': lead.street,
729             'street2': lead.street2,
730             'zip': lead.zip,
731             'city': lead.city,
732             'country_id': lead.country_id and lead.country_id.id or False,
733             'state_id': lead.state_id and lead.state_id.id or False,
734             'is_company': is_company,
735             'type': 'contact'
736         }
737         partner = partner.create(cr, uid,vals, context)
738         return partner
739
740     def _create_lead_partner(self, cr, uid, lead, context=None):
741         partner_id =  False
742         if lead.partner_name and lead.contact_name:
743             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
744             partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
745         elif lead.partner_name and not lead.contact_name:
746             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
747         elif not lead.partner_name and lead.contact_name:
748             partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
749         else:
750             partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
751         return partner_id
752
753     def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
754         """
755         Assign a partner to a lead.
756
757         :param object lead: browse record of the lead to process
758         :param int partner_id: identifier of the partner to assign
759         :return bool: True if the partner has properly been assigned
760         """
761         res = False
762         res_partner = self.pool.get('res.partner')
763         if partner_id:
764             res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
765             contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
766             res = lead.write({'partner_id': partner_id}, context=context)
767             message = _("%s <b>partner</b> is now set to <em>%s</em>." % ('Opportunity' if lead.type == 'opportunity' else 'Lead', lead.partner_id.name))
768             self.message_post(cr, uid, [lead.id], body=message, context=context)
769         return res
770
771     def handle_partner_assignation(self, cr, uid, ids, action='create', partner_id=False, context=None):
772         """
773         Handle partner assignation during a lead conversion.
774         if action is 'create', create new partner with contact and assign lead to new partner_id.
775         otherwise assign lead to the specified partner_id
776
777         :param list ids: leads/opportunities ids to process
778         :param string action: what has to be done regarding partners (create it, assign an existing one, or nothing)
779         :param int partner_id: partner to assign if any
780         :return dict: dictionary organized as followed: {lead_id: partner_assigned_id}
781         """
782         #TODO this is a duplication of the handle_partner_assignation method of crm_phonecall
783         partner_ids = {}
784         # If a partner_id is given, force this partner for all elements
785         force_partner_id = partner_id
786         for lead in self.browse(cr, uid, ids, context=context):
787             # If the action is set to 'create' and no partner_id is set, create a new one
788             if action == 'create':
789                 partner_id = force_partner_id or self._create_lead_partner(cr, uid, lead, context)
790             self._lead_set_partner(cr, uid, lead, partner_id, context=context)
791             partner_ids[lead.id] = partner_id
792         return partner_ids
793
794     def allocate_salesman(self, cr, uid, ids, user_ids=None, team_id=False, context=None):
795         """
796         Assign salesmen and salesteam to a batch of leads.  If there are more
797         leads than salesmen, these salesmen will be assigned in round-robin.
798         E.g.: 4 salesmen (S1, S2, S3, S4) for 6 leads (L1, L2, ... L6).  They
799         will be assigned as followed: L1 - S1, L2 - S2, L3 - S3, L4 - S4,
800         L5 - S1, L6 - S2.
801
802         :param list ids: leads/opportunities ids to process
803         :param list user_ids: salesmen to assign
804         :param int team_id: salesteam to assign
805         :return bool
806         """
807         index = 0
808
809         for lead_id in ids:
810             value = {}
811             if team_id:
812                 value['section_id'] = team_id
813             if user_ids:
814                 value['user_id'] = user_ids[index]
815                 # Cycle through user_ids
816                 index = (index + 1) % len(user_ids)
817             if value:
818                 self.write(cr, uid, [lead_id], value, context=context)
819         return True
820
821     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):
822         """
823         :param string action: ('schedule','Schedule a call'), ('log','Log a call')
824         """
825         phonecall = self.pool.get('crm.phonecall')
826         model_data = self.pool.get('ir.model.data')
827         phonecall_dict = {}
828         if not categ_id:
829             res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
830             if res_id:
831                 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
832         for lead in self.browse(cr, uid, ids, context=context):
833             if not section_id:
834                 section_id = lead.section_id and lead.section_id.id or False
835             if not user_id:
836                 user_id = lead.user_id and lead.user_id.id or False
837             vals = {
838                 'name': call_summary,
839                 'opportunity_id': lead.id,
840                 'user_id': user_id or False,
841                 'categ_id': categ_id or False,
842                 'description': desc or '',
843                 'date': schedule_time,
844                 'section_id': section_id or False,
845                 'partner_id': lead.partner_id and lead.partner_id.id or False,
846                 'partner_phone': phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
847                 'partner_mobile': lead.partner_id and lead.partner_id.mobile or False,
848                 'priority': lead.priority,
849             }
850             new_id = phonecall.create(cr, uid, vals, context=context)
851             phonecall.case_open(cr, uid, [new_id], context=context)
852             if action == 'log':
853                 phonecall.case_close(cr, uid, [new_id], context=context)
854             phonecall_dict[lead.id] = new_id
855             self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
856         return phonecall_dict
857
858     def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
859         models_data = self.pool.get('ir.model.data')
860
861         # Get opportunity views
862         dummy, form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
863         dummy, tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
864         return {
865             'name': _('Opportunity'),
866             'view_type': 'form',
867             'view_mode': 'tree, form',
868             'res_model': 'crm.lead',
869             'domain': [('type', '=', 'opportunity')],
870             'res_id': int(opportunity_id),
871             'view_id': False,
872             'views': [(form_view or False, 'form'),
873                       (tree_view or False, 'tree'),
874                       (False, 'calendar'), (False, 'graph')],
875             'type': 'ir.actions.act_window',
876         }
877
878     def redirect_lead_view(self, cr, uid, lead_id, context=None):
879         models_data = self.pool.get('ir.model.data')
880
881         # Get lead views
882         dummy, form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_leads')
883         dummy, tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_leads')
884         return {
885             'name': _('Lead'),
886             'view_type': 'form',
887             'view_mode': 'tree, form',
888             'res_model': 'crm.lead',
889             'domain': [('type', '=', 'lead')],
890             'res_id': int(lead_id),
891             'view_id': False,
892             'views': [(form_view or False, 'form'),
893                       (tree_view or False, 'tree'),
894                       (False, 'calendar'), (False, 'graph')],
895             'type': 'ir.actions.act_window',
896         }
897
898     def action_makeMeeting(self, cr, uid, ids, context=None):
899         """
900         Open meeting's calendar view to schedule meeting on current opportunity.
901         :return dict: dictionary value for created Meeting view
902         """
903         opportunity = self.browse(cr, uid, ids[0], context)
904         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
905         res['context'] = {
906             'default_opportunity_id': opportunity.id,
907             'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
908             'default_partner_ids' : opportunity.partner_id and [opportunity.partner_id.id] or False,
909             'default_user_id': uid,
910             'default_section_id': opportunity.section_id and opportunity.section_id.id or False,
911             'default_email_from': opportunity.email_from,
912             'default_name': opportunity.name,
913         }
914         return res
915
916     def write(self, cr, uid, ids, vals, context=None):
917         if vals.get('stage_id') and not vals.get('probability'):
918             # change probability of lead(s) if required by stage
919             stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
920             if stage.on_change:
921                 vals['probability'] = stage.probability
922         if vals.get('section_id'):
923             section_id = self.pool.get('crm.case.section').browse(cr, uid, vals.get('section_id'), context=context)
924             vals.setdefault('message_follower_ids', [])
925             vals['message_follower_ids'] += [(6, 0,[follower.id]) for follower in section_id.message_follower_ids]
926         res = super(crm_lead,self).write(cr, uid, ids, vals, context)
927         # subscribe new salesteam followers & subtypes to the lead
928         if vals.get('section_id'):
929             self._subscribe_followers_subtype(cr, uid, ids, vals.get('section_id'), 'crm.case.section', context=context)
930         return res
931
932     # ----------------------------------------
933     # Mail Gateway
934     # ----------------------------------------
935
936     def message_new(self, cr, uid, msg, custom_values=None, context=None):
937         """ Overrides mail_thread message_new that is called by the mailgateway
938             through message_process.
939             This override updates the document according to the email.
940         """
941         if custom_values is None: custom_values = {}
942
943         desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
944         custom_values.update({
945             'name':  msg.get('subject') or _("No Subject"),
946             'description': desc,
947             'email_from': msg.get('from'),
948             'email_cc': msg.get('cc'),
949             'user_id': False,
950         })
951         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
952             custom_values['priority'] = msg.get('priority')
953         return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
954
955     def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
956         """ Overrides mail_thread message_update that is called by the mailgateway
957             through message_process.
958             This method updates the document according to the email.
959         """
960         if isinstance(ids, (str, int, long)):
961             ids = [ids]
962         if update_vals is None: update_vals = {}
963
964         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
965             update_vals['priority'] = msg.get('priority')
966         maps = {
967             'cost':'planned_cost',
968             'revenue': 'planned_revenue',
969             'probability':'probability',
970         }
971         for line in msg.get('body', '').split('\n'):
972             line = line.strip()
973             res = tools.command_re.match(line)
974             if res and maps.get(res.group(1).lower()):
975                 key = maps.get(res.group(1).lower())
976                 update_vals[key] = res.group(2).lower()
977
978         return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
979
980     # ----------------------------------------
981     # OpenChatter methods and notifications
982     # ----------------------------------------
983
984     def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
985         phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
986         if action == 'log': prefix = 'Logged'
987         else: prefix = 'Scheduled'
988         message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
989         return self.message_post(cr, uid, ids, body=message, context=context)
990
991     def onchange_state(self, cr, uid, ids, state_id, context=None):
992         if state_id:
993             country_id=self.pool.get('res.country.state').browse(cr, uid, state_id, context).country_id.id
994             return {'value':{'country_id':country_id}}
995         return {}
996
997 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: