[IMP] crm: reformulate confusing error message
[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 lead 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', select=True),
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', select=True),
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(filter(lambda x: x, [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" % (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 opportunity from the list view.'))
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                 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
549                 'partner_address_id': contact_id,
550         }
551
552     def _convert_opportunity_notification(self, cr, uid, lead, context=None):
553         success_message = _("Lead '%s' has been converted to an opportunity.") % lead.name
554         self.message_append(cr, uid, [lead.id], success_message, body_text=success_message, context=context)
555         self.log(cr, uid, lead.id, success_message)
556         self._send_mail_to_salesman(cr, uid, lead, context=context)
557         return True
558
559     def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
560         partner = self.pool.get('res.partner')
561         mail_message = self.pool.get('mail.message')
562         customer = False
563         if partner_id:
564             customer = partner.browse(cr, uid, partner_id, context=context)
565         for lead in self.browse(cr, uid, ids, context=context):
566             if lead.state in ('done', 'cancel'):
567                 continue
568             if user_ids or section_id:
569                 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
570
571             vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
572             self.write(cr, uid, [lead.id], vals, context=context)
573
574             self._convert_opportunity_notification(cr, uid, lead, context=context)
575             #TOCHECK: why need to change partner details in all messages of lead ?
576             if lead.partner_id:
577                 msg_ids = [ x.id for x in lead.message_ids]
578                 mail_message.write(cr, uid, msg_ids, {
579                         'partner_id': lead.partner_id.id
580                     }, context=context)
581         return True
582
583     def _lead_create_partner(self, cr, uid, lead, context=None):
584         partner = self.pool.get('res.partner')
585         partner_id = partner.create(cr, uid, {
586                     'name': lead.partner_name or lead.contact_name or lead.name,
587                     'user_id': lead.user_id.id,
588                     'comment': lead.description,
589                     'section_id': lead.section_id.id or False,
590                     'address': []
591         })
592         return partner_id
593
594     def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
595         res = False
596         res_partner = self.pool.get('res.partner')
597         if partner_id:
598             res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
599             contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
600             res = lead.write({'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
601
602         return res
603
604     def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
605         address = self.pool.get('res.partner.address')
606         return address.create(cr, uid, {
607                     'partner_id': partner_id,
608                     'name': lead.contact_name,
609                     'phone': lead.phone,
610                     'mobile': lead.mobile,
611                     'email': lead.email_from and to_email(lead.email_from)[0],
612                     'fax': lead.fax,
613                     'title': lead.title and lead.title.id or False,
614                     'function': lead.function,
615                     'street': lead.street,
616                     'street2': lead.street2,
617                     'zip': lead.zip,
618                     'city': lead.city,
619                     'country_id': lead.country_id and lead.country_id.id or False,
620                     'state_id': lead.state_id and lead.state_id.id or False,
621                 })
622
623     def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
624         """
625         This function convert partner based on action.
626         if action is 'create', create new partner with contact and assign lead to new partner_id.
627         otherwise assign lead to specified partner_id
628         """
629         if context is None:
630             context = {}
631         partner_ids = {}
632         for lead in self.browse(cr, uid, ids, context=context):
633             if action == 'create':
634                 if not partner_id:
635                     partner_id = self._lead_create_partner(cr, uid, lead, context=context)
636                 self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
637             self._lead_set_partner(cr, uid, lead, partner_id, context=context)
638             partner_ids[lead.id] = partner_id
639         return partner_ids
640
641     def _send_mail_to_salesman(self, cr, uid, lead, context=None):
642         """
643         Send mail to salesman with updated Lead details.
644         @ lead: browse record of 'crm.lead' object.
645         """
646         #TOFIX: mail template should be used here instead of fix subject, body text.
647         message = self.pool.get('mail.message')
648         email_to = lead.user_id and lead.user_id.user_email
649         if not email_to:
650             return False
651
652         email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
653         partner = lead.partner_id and lead.partner_id.name or lead.partner_name
654         subject = "lead %s converted into opportunity" % lead.name
655         body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
656         return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
657
658
659     def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
660         index = 0
661         for lead_id in ids:
662             value = {}
663             if team_id:
664                 value['section_id'] = team_id
665             if index < len(user_ids):
666                 value['user_id'] = user_ids[index]
667                 index += 1
668             if value:
669                 self.write(cr, uid, [lead_id], value, context=context)
670         return True
671
672     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):
673         """
674         action :('schedule','Schedule a call'), ('log','Log a call')
675         """
676         phonecall = self.pool.get('crm.phonecall')
677         model_data = self.pool.get('ir.model.data')
678         phonecall_dict = {}
679         if not categ_id:
680             res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
681             if res_id:
682                 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
683         for lead in self.browse(cr, uid, ids, context=context):
684             if not section_id:
685                 section_id = lead.section_id and lead.section_id.id or False
686             if not user_id:
687                 user_id = lead.user_id and lead.user_id.id or False
688             vals = {
689                     'name' : call_summary,
690                     'opportunity_id' : lead.id,
691                     'user_id' : user_id or False,
692                     'categ_id' : categ_id or False,
693                     'description' : desc or '',
694                     'date' : schedule_time,
695                     'section_id' : section_id or False,
696                     'partner_id': lead.partner_id and lead.partner_id.id or False,
697                     'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
698                     'partner_phone' : phone or lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
699                     'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
700                     'priority': lead.priority,
701             }
702
703             new_id = phonecall.create(cr, uid, vals, context=context)
704             phonecall.case_open(cr, uid, [new_id])
705             if action == 'log':
706                 phonecall.case_close(cr, uid, [new_id])
707             phonecall_dict[lead.id] = new_id
708         return phonecall_dict
709
710
711     def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
712         models_data = self.pool.get('ir.model.data')
713
714         # Get Opportunity views
715         form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
716         tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
717         return {
718                 'name': _('Opportunity'),
719                 'view_type': 'form',
720                 'view_mode': 'tree, form',
721                 'res_model': 'crm.lead',
722                 'domain': [('type', '=', 'opportunity')],
723                 'res_id': int(opportunity_id),
724                 'view_id': False,
725                 'views': [(form_view and form_view[1] or False, 'form'),
726                           (tree_view and tree_view[1] or False, 'tree'),
727                           (False, 'calendar'), (False, 'graph')],
728                 'type': 'ir.actions.act_window',
729         }
730
731
732     def message_new(self, cr, uid, msg, custom_values=None, context=None):
733         """Automatically calls when new email message arrives"""
734         res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
735         subject = msg.get('subject')  or _("No Subject")
736         body = msg.get('body_text')
737
738         msg_from = msg.get('from')
739         priority = msg.get('priority')
740         vals = {
741             'name': subject,
742             'email_from': msg_from,
743             'email_cc': msg.get('cc'),
744             'description': body,
745             'user_id': False,
746         }
747         if priority:
748             vals['priority'] = priority
749         vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
750         self.write(cr, uid, [res_id], vals, context)
751         return res_id
752
753     def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
754         if isinstance(ids, (str, int, long)):
755             ids = [ids]
756         if vals == None:
757             vals = {}
758         super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
759
760         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
761             vals['priority'] = msg.get('priority')
762         maps = {
763             'cost':'planned_cost',
764             'revenue': 'planned_revenue',
765             'probability':'probability'
766         }
767         vls = {}
768         for line in msg['body_text'].split('\n'):
769             line = line.strip()
770             res = tools.misc.command_re.match(line)
771             if res and maps.get(res.group(1).lower()):
772                 key = maps.get(res.group(1).lower())
773                 vls[key] = res.group(2).lower()
774         vals.update(vls)
775
776         # Unfortunately the API is based on lists
777         # but we want to update the state based on the
778         # previous state, so we have to loop:
779         for case in self.browse(cr, uid, ids, context=context):
780             values = dict(vals)
781             if case.state in CRM_LEAD_PENDING_STATES:
782                 #re-open
783                 values.update(state=crm.AVAILABLE_STATES[1][0])
784                 if not case.date_open:
785                     values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) 
786             res = self.write(cr, uid, [case.id], values, context=context)
787         return res
788
789     def action_makeMeeting(self, cr, uid, ids, context=None):
790         """
791         This opens Meeting's calendar view to schedule meeting on current Opportunity
792         @return : Dictionary value for created Meeting view
793         """
794         if context is None:
795             context = {}
796         value = {}
797         data_obj = self.pool.get('ir.model.data')
798         for opp in self.browse(cr, uid, ids, context=context):
799             # Get meeting views
800             tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
801             form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
802             calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
803             search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
804             context.update({
805                 'default_opportunity_id': opp.id,
806                 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
807                 'default_user_id': uid,
808                 'default_section_id': opp.section_id and opp.section_id.id or False,
809                 'default_email_from': opp.email_from,
810                 'default_state': 'open',
811                 'default_name': opp.name
812             })
813             value = {
814                 'name': _('Meetings'),
815                 'context': context,
816                 'view_type': 'form',
817                 'view_mode': 'calendar,form,tree',
818                 'res_model': 'crm.meeting',
819                 'view_id': False,
820                 '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')],
821                 'type': 'ir.actions.act_window',
822                 'search_view_id': search_view and search_view[1] or False,
823                 'nodestroy': True
824             }
825         return value
826
827
828     def unlink(self, cr, uid, ids, context=None):
829         for lead in self.browse(cr, uid, ids, context):
830             if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
831                 raise osv.except_osv(_('Error'),
832                     _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
833                       "You should better cancel it, instead of deleting it.") % lead.name)
834         return super(crm_lead, self).unlink(cr, uid, ids, context)
835
836
837     def write(self, cr, uid, ids, vals, context=None):
838         if not context:
839             context = {}
840
841         if 'date_closed' in vals:
842             return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
843
844         if 'stage_id' in vals and vals['stage_id']:
845             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
846             text = _("Changed Stage to: %s") % stage_obj.name
847             self.message_append(cr, uid, ids, text, body_text=text, context=context)
848             message=''
849             for case in self.browse(cr, uid, ids, context=context):
850                 if case.type == 'lead' or  context.get('stage_type',False)=='lead':
851                     message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
852                 elif case.type == 'opportunity':
853                     message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
854                 self.log(cr, uid, case.id, message)
855         return super(crm_lead,self).write(cr, uid, ids, vals, context)
856
857 crm_lead()
858
859 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: