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