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