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