[MERGE] merged with trunk development branch
[odoo/odoo.git] / addons / crm / crm_lead.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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 osv import fields, osv
23 from datetime import datetime
24 import crm
25 import time
26 from tools.translate import _
27 from crm import crm_case
28 import binascii
29 import tools
30 from mail.mail_message import to_email
31
32 CRM_LEAD_PENDING_STATES = (
33     crm.AVAILABLE_STATES[2][0], # Cancelled
34     crm.AVAILABLE_STATES[3][0], # Done
35     crm.AVAILABLE_STATES[4][0], # Pending
36 )
37
38 class crm_lead(crm_case, osv.osv):
39     """ CRM Lead Case """
40     _name = "crm.lead"
41     _description = "Lead/Opportunity"
42     _order = "priority,date_action,id desc"
43     _inherit = ['mail.thread','res.partner.address']
44
45     def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
46         access_rights_uid = access_rights_uid or uid
47         stage_obj = self.pool.get('crm.case.stage')
48         order = stage_obj._order
49         if read_group_order == 'stage_id desc':
50             # lame hack to allow reverting search, should just work in the trivial case
51             order = "%s desc" % order
52         stage_ids = stage_obj._search(cr, uid, ['|', ('id','in',ids),('case_default','=',1)], order=order,
53                                       access_rights_uid=access_rights_uid, context=context)
54         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
55         # restore order of the search
56         result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
57         return result
58
59     _group_by_full = {
60         'stage_id': _read_group_stage_ids
61     }
62
63     # overridden because res.partner.address has an inconvenient name_get,
64     # especially if base_contact is installed.
65     def name_get(self, cr, user, ids, context=None):
66         if isinstance(ids, (int, long)):
67             ids = [ids]
68         return [(r['id'], tools.ustr(r[self._rec_name]))
69                     for r in self.read(cr, user, ids, [self._rec_name], context)]
70
71     def _compute_day(self, cr, uid, ids, fields, args, context=None):
72         """
73         @param cr: the current row, from the database cursor,
74         @param uid: the current user’s ID for security checks,
75         @param ids: List of Openday’s IDs
76         @return: difference between current date and log date
77         @param context: A standard dictionary for contextual values
78         """
79         cal_obj = self.pool.get('resource.calendar')
80         res_obj = self.pool.get('resource.resource')
81
82         res = {}
83         for lead in self.browse(cr, uid, ids, context=context):
84             for field in fields:
85                 res[lead.id] = {}
86                 duration = 0
87                 ans = False
88                 if field == 'day_open':
89                     if lead.date_open:
90                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
91                         date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
92                         ans = date_open - date_create
93                         date_until = lead.date_open
94                 elif field == 'day_close':
95                     if lead.date_closed:
96                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
97                         date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
98                         date_until = lead.date_closed
99                         ans = date_close - date_create
100                 if ans:
101                     resource_id = False
102                     if lead.user_id:
103                         resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
104                         if len(resource_ids):
105                             resource_id = resource_ids[0]
106
107                     duration = float(ans.days)
108                     if lead.section_id and lead.section_id.resource_calendar_id:
109                         duration =  float(ans.days) * 24
110                         new_dates = cal_obj.interval_get(cr,
111                             uid,
112                             lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
113                             datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
114                             duration,
115                             resource=resource_id
116                         )
117                         no_days = []
118                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
119                         for in_time, out_time in new_dates:
120                             if in_time.date not in no_days:
121                                 no_days.append(in_time.date)
122                             if out_time > date_until:
123                                 break
124                         duration =  len(no_days)
125                 res[lead.id][field] = abs(int(duration))
126         return res
127
128     def _history_search(self, cr, uid, obj, name, args, context=None):
129         res = []
130         msg_obj = self.pool.get('mail.message')
131         message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
132         lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
133
134         if lead_ids:
135             return [('id', 'in', lead_ids)]
136         else:
137             return [('id', '=', '0')]
138
139     def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
140         res = {}
141         for obj in self.browse(cr, uid, ids, context=context):
142             res[obj.id] = ''
143             for msg in obj.message_ids:
144                 if msg.email_from:
145                     res[obj.id] = msg.subject
146                     break
147         return res
148
149     _columns = {
150         # Overridden from res.partner.address:
151         'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
152             select=True, help="Optional linked partner, usually after conversion of the lead"),
153
154         'id': fields.integer('ID', readonly=True),
155         'name': fields.char('Name', size=64, select=1),
156         'active': fields.boolean('Active', required=False),
157         'date_action_last': fields.datetime('Last Action', readonly=1),
158         'date_action_next': fields.datetime('Next Action', readonly=1),
159         'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
160         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
161                         select=True, help='When sending mails, the default email address is taken from the sales team.'),
162         'create_date': fields.datetime('Creation Date' , readonly=True),
163         '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"),
164         'description': fields.text('Notes'),
165         'write_date': fields.datetime('Update Date' , readonly=True),
166
167         'categ_id': fields.many2one('crm.case.categ', 'Category', \
168             domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
169         'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
170             domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
171         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
172         'contact_name': fields.char('Contact Name', size=64),
173         'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner that will be created while converting the into opportunity', select=1),
174         'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
175         'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
176         'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
177         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
178         'date_closed': fields.datetime('Closed', readonly=True),
179         'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
180         'user_id': fields.many2one('res.users', 'Salesman'),
181         'referred': fields.char('Referred By', size=64),
182         'date_open': fields.datetime('Opened', readonly=True),
183         'day_open': fields.function(_compute_day, string='Days to Open', \
184                                 multi='day_open', type="float", store=True),
185         'day_close': fields.function(_compute_day, string='Days to Close', \
186                                 multi='day_close', type="float", store=True),
187         'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
188                                   help='The state is set to \'Draft\', when a case is created.\
189                                   \nIf the case is in progress the state is set to \'Open\'.\
190                                   \nWhen the case is over, the state is set to \'Done\'.\
191                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
192         'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
193         'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
194
195
196         # Only used for type opportunity
197         'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"), 
198         'probability': fields.float('Probability (%)',group_operator="avg"),
199         'planned_revenue': fields.float('Expected Revenue'),
200         'ref': fields.reference('Reference', selection=crm._links_get, size=128),
201         'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
202         'phone': fields.char("Phone", size=64),
203         'date_deadline': fields.date('Expected Closing'),
204         'date_action': fields.date('Next Action Date'),
205         'title_action': fields.char('Next Action', size=64),
206         'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
207         'color': fields.integer('Color Index'),
208         'partner_address_name': fields.related('partner_address_id', 'name', type='char', string='Partner Contact Name', readonly=True),
209         'partner_address_email': fields.related('partner_address_id', 'email', type='char', string='Partner Contact Email', readonly=True),
210         'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
211         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
212         'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
213
214     }
215
216     _defaults = {
217         'active': lambda *a: 1,
218         'user_id': crm_case._get_default_user,
219         'email_from': crm_case._get_default_email,
220         'state': lambda *a: 'draft',
221         'type': lambda *a: 'lead',
222         'section_id': crm_case._get_section,
223         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
224         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
225         'color': 0,
226     }
227
228     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
229         """This function returns value of partner email based on Partner Address
230         """
231         if not add:
232             return {'value': {'email_from': False, 'country_id': False}}
233         address = self.pool.get('res.partner.address').browse(cr, uid, add)
234         return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
235
236     def on_change_optin(self, cr, uid, ids, optin):
237         return {'value':{'optin':optin,'optout':False}}
238
239     def on_change_optout(self, cr, uid, ids, optout):
240         return {'value':{'optout':optout,'optin':False}}
241
242     def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
243         if not stage_id:
244             return {'value':{}}
245         stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
246         if not stage.on_change:
247             return {'value':{}}
248         return {'value':{'probability': stage.probability}}
249
250     def stage_find_percent(self, cr, uid, percent, section_id):
251         """ Return the first stage with a probability == percent
252         """
253         stage_pool = self.pool.get('crm.case.stage')
254         if section_id :
255             ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
256         else :
257             ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
258
259         if ids:
260             return ids[0]
261         return False
262
263     def stage_find_lost(self, cr, uid, section_id):
264         return self.stage_find_percent(cr, uid, 0.0, section_id)
265
266     def stage_find_won(self, cr, uid, section_id):
267         return self.stage_find_percent(cr, uid, 100.0, section_id)
268
269     def case_open(self, cr, uid, ids, *args):
270         for l in self.browse(cr, uid, ids):
271             # When coming from draft override date and stage otherwise just set state
272             if l.state == 'draft':
273                 if l.type == 'lead':
274                     message = _("The lead '%s' has been opened.") % l.name
275                 elif l.type == 'opportunity':
276                     message = _("The opportunity '%s' has been opened.") % l.name
277                 else:
278                     message = _("The case '%s' has been opened.") % l.name
279                 self.log(cr, uid, l.id, message)
280                 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
281                 self.write(cr, uid, [l.id], value)
282                 if l.type == 'opportunity' and not l.stage_id:
283                     stage_id = self.stage_find(cr, uid, l.section_id.id or False, [('sequence','>',0)])
284                     if stage_id:
285                         self.stage_set(cr, uid, [l.id], stage_id)
286         res = super(crm_lead, self).case_open(cr, uid, ids, *args)
287         return res
288
289     def case_close(self, cr, uid, ids, *args):
290         res = super(crm_lead, self).case_close(cr, uid, ids, *args)
291         self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
292         for case in self.browse(cr, uid, ids):
293             if case.type == 'lead':
294                 message = _("The lead '%s' has been closed.") % case.name
295             else:
296                 message = _("The case '%s' has been closed.") % case.name
297             self.log(cr, uid, case.id, message)
298         return res
299
300     def case_cancel(self, cr, uid, ids, *args):
301         """Overrides cancel for crm_case for setting probability
302         """
303         res = super(crm_lead, self).case_cancel(cr, uid, ids, args)
304         self.write(cr, uid, ids, {'probability' : 0.0})
305         return res
306
307     def case_reset(self, cr, uid, ids, *args):
308         """Overrides reset as draft in order to set the stage field as empty
309         """
310         res = super(crm_lead, self).case_reset(cr, uid, ids, *args)
311         self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
312         return res
313
314     def case_mark_lost(self, cr, uid, ids, *args):
315         """Mark the case as lost: state = done and probability = 0%
316         """
317         res = super(crm_lead, self).case_close(cr, uid, ids, *args)
318         self.write(cr, uid, ids, {'probability' : 0.0})
319         for l in self.browse(cr, uid, ids):
320             stage_id = self.stage_find_lost(cr, uid, l.section_id.id or False)
321             if stage_id:
322                 self.stage_set(cr, uid, [l.id], stage_id)
323             message = _("The opportunity '%s' has been marked as lost.") % l.name
324             self.log(cr, uid, l.id, message)
325         return res
326
327     def case_mark_won(self, cr, uid, ids, *args):
328         """Mark the case as lost: state = done and probability = 0%
329         """
330         res = super(crm_lead, self).case_close(cr, uid, ids, *args)
331         self.write(cr, uid, ids, {'probability' : 100.0})
332         for l in self.browse(cr, uid, ids):
333             stage_id = self.stage_find_won(cr, uid, l.section_id.id or False)
334             if stage_id:
335                 self.stage_set(cr, uid, [l.id], stage_id)
336             message = _("The opportunity '%s' has been been won.") % l.name
337             self.log(cr, uid, l.id, message)
338         return res
339
340     def set_priority(self, cr, uid, ids, priority):
341         """Set lead priority
342         """
343         return self.write(cr, uid, ids, {'priority' : priority})
344
345     def set_high_priority(self, cr, uid, ids, *args):
346         """Set lead priority to high
347         """
348         return self.set_priority(cr, uid, ids, '1')
349
350     def set_normal_priority(self, cr, uid, ids, *args):
351         """Set lead priority to normal
352         """
353         return self.set_priority(cr, uid, ids, '3')
354
355     
356     def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
357         # prepare opportunity data into dictionary for merging
358         opportunities = self.browse(cr, uid, ids, context=context)
359         def _get_first_not_null(attr):
360             if hasattr(oldest, attr):
361                 return getattr(oldest, attr)
362             for opportunity in opportunities:
363                 if hasattr(opportunity, attr):
364                     return getattr(opportunity, attr)
365             return False
366
367         def _get_first_not_null_id(attr):
368             res = _get_first_not_null(attr)
369             return res and res.id or False
370     
371         def _concat_all(attr):
372             return ', '.join([getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)])
373
374         data = {}
375         for field_name in fields:
376             field_info = self._all_columns.get(field_name)
377             if field_info is None:
378                 continue
379             field = field_info.column
380             if field._type in ('many2many', 'one2many'):
381                 continue  
382             elif field._type == 'many2one':
383                 data[field_name] = _get_first_not_null_id(field_name)  # !!
384             elif field._type == 'text':
385                 data[field_name] = _concat_all(field_name)  #not lost
386             else:
387                 data[field_name] = _get_first_not_null(field_name)  #not lost
388         return data
389
390     def _merge_find_oldest(self, cr, uid, ids, context=None):
391         if context is None:
392             context = {}
393         #TOCHECK: where pass 'convert' in context ?
394         if context.get('convert'):
395             ids = list(set(ids) - set(context.get('lead_ids', False)) )
396
397         #search opportunities order by create date
398         opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
399         oldest_id = opportunity_ids[0]
400         return self.browse(cr, uid, oldest_id, context=context)
401
402     def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
403         body = []
404         if title:
405             body.append("%s\n" % (title))
406         for field_name in fields:
407             field_info = self._all_columns.get(field_name)
408             if field_info is None:
409                 continue
410             field = field_info.column
411             value = None
412
413             if field._type == 'selection':
414                 if hasattr(field.selection, '__call__'):
415                     key = field.selection(self, cr, uid, context=context)
416                 else:
417                     key = field.selection
418                 value = dict(key).get(lead[field_name], lead[field_name])
419             elif field._type == 'many2one':
420                 if lead[field_name]:
421                     value = lead[field_name].name_get()[0][1]
422             else:
423                 value = lead[field_name]
424
425             body.append("%s: %s\n" % (field.string, value or ''))
426         return "\n".join(body + ['---'])
427
428     def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
429         #TOFIX: mail template should be used instead of fix body, subject text
430         details = []
431         merge_message = _('Merged opportunities')
432         subject = [merge_message]
433         fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
434                   'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
435                   'country_id', 'city', 'street', 'street2', 'zip']
436         for opportunity in opportunities:
437             subject.append(opportunity.name)
438             title = "%s : %s" % (merge_message, opportunity.name)
439             details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
440             
441         subject = subject[0] + ", ".join(subject[1:])
442         details = "\n\n".join(details)
443         return self.message_append(cr, uid, [opportunity_id], subject, body_text=details, context=context)
444         
445     def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
446         message = self.pool.get('mail.message')
447         for opportunity in opportunities:
448             for history in opportunity.message_ids:
449                 message.write(cr, uid, history.id, {
450                         'res_id': opportunity_id, 
451                         'subject' : _("From %s : %s") % (opportunity.name, history.subject) 
452                 }, context=context)
453
454         return True
455
456     def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
457         attachment = self.pool.get('ir.attachment')
458
459         # return attachments of opportunity
460         def _get_attachments(opportunity_id):
461             attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
462             return attachment.browse(cr, uid, attachment_ids, context=context)
463
464         count = 1
465         first_attachments = _get_attachments(opportunity_id)
466         for opportunity in opportunities:
467             attachments = _get_attachments(opportunity.id)
468             for first in first_attachments:
469                 for attachment in attachments:
470                     if attachment.name == first.name:
471                         values = dict(
472                             name = "%s (%s)" % (attachment.name, count,),
473                             res_id = opportunity_id,
474                         )
475                         attachment.write(values)
476                         count+=1
477                     
478         return True    
479
480     def merge_opportunity(self, cr, uid, ids, context=None):
481         """
482         To merge opportunities
483             :param ids: list of opportunities ids to merge
484         """
485         if context is None: context = {}
486         
487         #TOCHECK: where pass lead_ids in context?
488         lead_ids = context and context.get('lead_ids', []) or []
489
490         if len(ids) <= 1:
491             raise osv.except_osv(_('Warning !'),_('Please select more than one opportunities.'))
492
493         ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
494         opportunities = self.browse(cr, uid, ids, context=context)
495         opportunities_list = list(set(opportunities) - set(ctx_opportunities))
496         oldest = self._merge_find_oldest(cr, uid, ids, context=context)
497         if ctx_opportunities :
498             first_opportunity = ctx_opportunities[0]
499             tail_opportunities = opportunities_list
500         else:
501             first_opportunity = opportunities_list[0]
502             tail_opportunities = opportunities_list[1:]
503
504         fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id', 
505             'partner_address_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
506             'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
507             'date_action_next', 'email_from', 'email_cc', 'partner_name']
508         
509         data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
510
511         # merge data into first opportunity
512         self.write(cr, uid, [first_opportunity.id], data, context=context)
513
514         #copy message and attachements into the first opportunity
515         self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
516         self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)        
517         
518         #Notification about loss of information
519         self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
520         #delete tail opportunities
521         self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
522
523         #open first opportunity
524         self.case_open(cr, uid, [first_opportunity.id])
525         return first_opportunity.id
526
527     def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
528         crm_stage = self.pool.get('crm.case.stage')
529         contact_id = False
530         if customer:
531             contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
532         if not section_id:
533             section_id = lead.section_id and lead.section_id.id or False
534         if section_id:
535             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
536         else:
537             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])            
538         stage_id = stage_ids and stage_ids[0] or False
539         return {
540                 'planned_revenue': lead.planned_revenue,
541                 'probability': lead.probability,
542                 'name': lead.name,
543                 'partner_id': customer and customer.id or False,
544                 'user_id': (lead.user_id and lead.user_id.id),
545                 'type': 'opportunity',
546                 'stage_id': stage_id or False,
547                 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
548                 'partner_address_id': contact_id
549         }
550     def _convert_opportunity_notification(self, cr, uid, lead, context=None):
551         success_message = _("Lead '%s' has been converted to an opportunity.") % lead.name
552         self.message_append(cr, uid, [lead.id], success_message, body_text=success_message, context=context)
553         self.log(cr, uid, lead.id, success_message)
554         self._send_mail_to_salesman(cr, uid, lead, context=context)
555         return True
556
557     def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
558         partner = self.pool.get('res.partner')
559         mail_message = self.pool.get('mail.message')
560         customer = False
561         if partner_id:
562             customer = partner.browse(cr, uid, partner_id, context=context)
563         for lead in self.browse(cr, uid, ids, context=context):
564             if lead.state in ('done', 'cancel'):
565                 continue
566             if user_ids or section_id:
567                 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
568             
569             vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
570             self.write(cr, uid, [lead.id], vals, context=context)
571             
572             self._convert_opportunity_notification(cr, uid, lead, context=context)
573             #TOCHECK: why need to change partner details in all messages of lead ?
574             if lead.partner_id:
575                 msg_ids = [ x.id for x in lead.message_ids]
576                 mail_message.write(cr, uid, msg_ids, {
577                         'partner_id': lead.partner_id.id
578                     }, context=context)
579         return True
580
581     def _lead_create_partner(self, cr, uid, lead, context=None):
582         partner = self.pool.get('res.partner')
583         partner_id = partner.create(cr, uid, {
584                     'name': lead.partner_name or lead.contact_name or lead.name,
585                     'user_id': lead.user_id.id,
586                     'comment': lead.description,
587                     'section_id': lead.section_id.id or False,
588                     'address': []
589         })
590         return partner_id
591
592     def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
593         res = False
594         res_partner = self.pool.get('res.partner')
595         if partner_id:
596             res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
597             contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
598             res = lead.write({'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
599             
600         return res
601
602     def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
603         address = self.pool.get('res.partner.address')
604         return address.create(cr, uid, {
605                     'partner_id': partner_id,
606                     'name': lead.contact_name,
607                     'phone': lead.phone,
608                     'mobile': lead.mobile,
609                     'email': lead.email_from and to_email(lead.email_from)[0],
610                     'fax': lead.fax,
611                     'title': lead.title and lead.title.id or False,
612                     'function': lead.function,
613                     'street': lead.street,
614                     'street2': lead.street2,
615                     'zip': lead.zip,
616                     'city': lead.city,
617                     'country_id': lead.country_id and lead.country_id.id or False,
618                     'state_id': lead.state_id and lead.state_id.id or False,
619                 })
620
621     def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
622         """
623         This function convert partner based on action.
624         if action is 'create', create new partner with contact and assign lead to new partner_id.
625         otherwise assign lead to specified partner_id
626         """
627         if context is None:
628             context = {}
629         partner_ids = {}
630         for lead in self.browse(cr, uid, ids, context=context):
631             if action == 'create': 
632                 if not partner_id:
633                     partner_id = self._lead_create_partner(cr, uid, lead, context=context)
634                 self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
635             self._lead_set_partner(cr, uid, lead, partner_id, context=context)
636             partner_ids[lead.id] = partner_id
637         return partner_ids
638
639     def _send_mail_to_salesman(self, cr, uid, lead, context=None):
640         """
641         Send mail to salesman with updated Lead details.
642         @ lead: browse record of 'crm.lead' object.
643         """
644         #TOFIX: mail template should be used here instead of fix subject, body text. 
645         message = self.pool.get('mail.message')
646         email_to = lead.user_id and lead.user_id.user_email
647         if not email_to:
648             return False
649         
650         email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
651         partner = lead.partner_id and lead.partner_id.name or lead.partner_name
652         subject = "lead %s converted into opportunity" % lead.name
653         body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
654         return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
655
656
657     def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
658         index = 0
659         for lead_id in ids:
660             value = {}
661             if team_id:
662                 value['section_id'] = team_id
663             if index < len(user_ids):
664                 value['user_id'] = user_ids[index]
665                 index += 1
666             if value:
667                 self.write(cr, uid, [lead_id], value, context=context)
668         return True
669
670     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):
671         """
672         action :('schedule','Schedule a call'), ('log','Log a call')
673         """
674         phonecall = self.pool.get('crm.phonecall')
675         model_data = self.pool.get('ir.model.data')
676         phonecall_dict = {}
677         if not categ_id:
678             res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
679             if res_id:
680                 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
681         for lead in self.browse(cr, uid, ids, context=context):
682             if not section_id:
683                 section_id = lead.section_id and lead.section_id.id or False
684             if not user_id:
685                 user_id = lead.user_id and lead.user_id.id or False
686             vals = {
687                     'name' : call_summary,
688                     'opportunity_id' : lead.id,
689                     'user_id' : user_id or False,
690                     'categ_id' : categ_id or False,
691                     'description' : desc or '',
692                     'date' : schedule_time,
693                     'section_id' : section_id or False,
694                     'partner_id': lead.partner_id and lead.partner_id.id or False,
695                     'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
696                     'partner_phone' : phone or lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
697                     'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
698                     'priority': lead.priority,
699             }
700             
701             new_id = phonecall.create(cr, uid, vals, context=context)
702             phonecall.case_open(cr, uid, [new_id])
703             if action == 'log':
704                 phonecall.case_close(cr, uid, [new_id])
705             phonecall_dict[lead.id] = new_id
706         return phonecall_dict
707
708
709     def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
710         models_data = self.pool.get('ir.model.data')
711
712         # Get Opportunity views
713         form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
714         tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
715         return {
716                 'name': _('Opportunity'),
717                 'view_type': 'form',
718                 'view_mode': 'tree, form',
719                 'res_model': 'crm.lead',
720                 'domain': [('type', '=', 'opportunity')],
721                 'res_id': int(opportunity_id),
722                 'view_id': False,
723                 'views': [(form_view and form_view[1] or False, 'form'),
724                           (tree_view and tree_view[1] or False, 'tree'),
725                           (False, 'calendar'), (False, 'graph')],
726                 'type': 'ir.actions.act_window',
727         }
728
729
730     def message_new(self, cr, uid, msg, custom_values=None, context=None):
731         """Automatically calls when new email message arrives"""
732         res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
733         subject = msg.get('subject')  or _("No Subject")
734         body = msg.get('body_text')
735
736         msg_from = msg.get('from')
737         priority = msg.get('priority')
738         vals = {
739             'name': subject,
740             'email_from': msg_from,
741             'email_cc': msg.get('cc'),
742             'description': body,
743             'user_id': False,
744         }
745         if priority:
746             vals['priority'] = priority
747         vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
748         self.write(cr, uid, [res_id], vals, context)
749         return res_id
750
751     def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
752         if isinstance(ids, (str, int, long)):
753             ids = [ids]
754
755         super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
756
757         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
758             vals['priority'] = msg.get('priority')
759         maps = {
760             'cost':'planned_cost',
761             'revenue': 'planned_revenue',
762             'probability':'probability'
763         }
764         vls = {}
765         for line in msg['body_text'].split('\n'):
766             line = line.strip()
767             res = tools.misc.command_re.match(line)
768             if res and maps.get(res.group(1).lower()):
769                 key = maps.get(res.group(1).lower())
770                 vls[key] = res.group(2).lower()
771         vals.update(vls)
772
773         # Unfortunately the API is based on lists
774         # but we want to update the state based on the
775         # previous state, so we have to loop:
776         for case in self.browse(cr, uid, ids, context=context):
777             values = dict(vals)
778             if case.state in CRM_LEAD_PENDING_STATES:
779                 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
780             res = self.write(cr, uid, [case.id], values, context=context)
781         return res
782
783     def action_makeMeeting(self, cr, uid, ids, context=None):
784         """
785         This opens Meeting's calendar view to schedule meeting on current Opportunity
786         @return : Dictionary value for created Meeting view
787         """
788         if context is None:
789             context = {}
790         value = {}
791         data_obj = self.pool.get('ir.model.data')
792         for opp in self.browse(cr, uid, ids, context=context):
793             # Get meeting views
794             tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
795             form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
796             calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
797             search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
798             context.update({
799                 'default_opportunity_id': opp.id,
800                 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
801                 'default_user_id': uid, 
802                 'default_section_id': opp.section_id and opp.section_id.id or False,
803                 'default_email_from': opp.email_from,
804                 'default_state': 'open',  
805                 'default_name': opp.name
806             })
807             value = {
808                 'name': _('Meetings'),
809                 'context': context,
810                 'view_type': 'form',
811                 'view_mode': 'calendar,form,tree',
812                 'res_model': 'crm.meeting',
813                 'view_id': False,
814                 'views': [(calander_view and calander_view[1] or False, 'calendar'), (form_view and form_view[1] or False, 'form'), (tree_view and tree_view[1] or False, 'tree')],
815                 'type': 'ir.actions.act_window',
816                 'search_view_id': search_view and search_view[1] or False,
817                 'nodestroy': True
818             }
819         return value
820
821
822     def unlink(self, cr, uid, ids, context=None):
823         for lead in self.browse(cr, uid, ids, context):
824             if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
825                 raise osv.except_osv(_('Warning !'),
826                     _('You can not delete this lead. You should better cancel it.'))
827         return super(crm_lead, self).unlink(cr, uid, ids, context)
828
829
830     def write(self, cr, uid, ids, vals, context=None):
831         if not context:
832             context = {}
833
834         if 'date_closed' in vals:
835             return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
836
837         if 'stage_id' in vals and vals['stage_id']:
838             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
839             text = _("Changed Stage to: %s") % stage_obj.name
840             self.message_append(cr, uid, ids, text, body_text=text, context=context)
841             message=''
842             for case in self.browse(cr, uid, ids, context=context):
843                 if case.type == 'lead' or  context.get('stage_type',False)=='lead':
844                     message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
845                 elif case.type == 'opportunity':
846                     message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
847                 self.log(cr, uid, case.id, message)
848         return super(crm_lead,self).write(cr, uid, ids, vals, context)
849
850 crm_lead()
851
852 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: