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