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