[IMP] notification with need action in phonecall when schedule a call.
[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._case_create_notification(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 _case_create_notification(self, cr, uid, ids, context=None):
275         for obj in self.browse(cr, uid, ids, context=context):
276             self.message_subscribe(cr, uid, ids, [obj.user_id.id], context=context)
277             if obj.type=="opportunity" and obj.state=="draft":
278                 message = _("Opportunity is <b>created</b>.")
279             elif obj.type=="lead" :
280                 message = _("Lead is <b>created</b>.")
281             else:
282                 message = _("The case has been <b>created</b>.")
283             self.message_append_note(cr, uid, ids, _('System notification'),
284                         message, type='notification', need_action_user_id=obj.user_id.id, context=context)
285         return True
286
287     def _case_open_notification(self, lead, context=None):
288         if lead.state != 'draft' and lead.state != 'pending':
289             return False
290         if lead.type == 'lead':
291             message = _("The lead has been <b>opened</b>.")
292         elif lead.type == 'opportunity':
293             message = _("The opportunity has been <b>opened</b>.")
294         else:
295             message = _("The case has been <b>opened</b>.")
296         lead.message_append_note('' ,message, need_action_user_id=lead.user_id.id)
297
298     def _case_close_notification(self, lead, context=None):
299         lead[0].message_mark_done(context)
300         if lead[0].type == 'lead':
301             message = _("The lead has been <b>closed</b>.")
302         elif lead[0].type == 'opportunity':
303             message = _("The opportunity has been <b>closed</b>.")
304         else:
305             message = _("The case has been <b>closed</b>.")
306         lead[0].message_append_note('' ,message)
307
308     def _case_mark_lost_notification(self, lead, context=None):
309         lead.message_mark_done(context)
310         message = _("The opportunity has been <b>marked as lost</b>.")
311         lead.message_append_note('' ,message)
312
313     def _case_mark_won_notification(self, lead, context=None):
314         lead.message_mark_done(context)
315         message = _("The opportunity has been <b>won</b>.")
316         lead.message_append_note('' ,message)
317
318     def _case_cancel_notification(self, lead, context=None):
319         lead[0].message_mark_done(context)
320         if lead[0].type == 'lead':
321             message = _("The lead has been <b>cancelled</b>.")
322         elif lead[0].type == 'opportunity':
323             message = _("The opportunity has been <b>cancelled</b>.")
324         lead[0].message_append_note('' ,message)
325
326     def _case_pending_notification(self, case, context=None):
327         if case[0].type == 'lead':
328             message = _("The lead is <b>pending</b>.")
329         elif case[0].type == 'opportunity':
330             message = _("The opportunity is <b>pending</b>.")
331         case[0].message_append_note('' ,message)
332
333     def _case_escalate_notification(self, case, context=None):
334         message = _("The lead is <b>escalated</b>.")
335         case.message_append_note('' ,message)
336
337     def _case_phonecall_notification(self, cr, uid, ids, case, phonecall, action, context=None):
338         for obj in phonecall.browse(cr, uid, ids, context=context):
339             if action == "schedule" :
340                 message = _("<b>%s a call</b> for the %s.") % (action, obj.date)
341             else :
342                 message = _("<b>%s a call</b>.") % (action)
343             case.message_append_note('', message)
344             if action == "schedule" :
345                 phonecall.message_append_note(cr, uid, ids, '', message, need_action_user_id=obj.user_id.id)
346
347     def case_open(self, cr, uid, ids, context=None):
348         res = super(crm_lead, self).case_open(cr, uid, ids, context)
349         for lead in self.browse(cr, uid, ids, context=context):
350             value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
351             self.write(cr, uid, [lead.id], value)
352             if lead.type == 'opportunity' and not lead.stage_id:
353                 stage_id = self.stage_find(cr, uid, lead.section_id.id or False, [('sequence','>',0)])
354                 if stage_id:
355                     self.stage_set(cr, uid, [lead.id], stage_id)
356         return res
357
358     def case_close(self, cr, uid, ids, context=None):
359         res = super(crm_lead, self).case_close(cr, uid, ids, context)
360         self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
361         return res
362
363     def case_cancel(self, cr, uid, ids, context=None):
364         """Overrides cancel for crm_case for setting probability
365         """
366         res = super(crm_lead, self).case_cancel(cr, uid, ids, context)
367         self.write(cr, uid, ids, {'probability' : 0.0})
368         return res
369
370     def case_reset(self, cr, uid, ids, context=None):
371         """Overrides reset as draft in order to set the stage field as empty
372         """
373         res = super(crm_lead, self).case_reset(cr, uid, ids, context)
374         self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
375         return res
376
377     def case_mark_lost(self, cr, uid, ids, context=None):
378         """Mark the case as lost: state = done and probability = 0%
379         """
380         res = super(crm_lead, self).case_close(cr, uid, ids, context)
381         self.write(cr, uid, ids, {'probability' : 0.0})
382         for lead in self.browse(cr, uid, ids):
383             stage_id = self.stage_find_lost(cr, uid, lead.section_id.id or False)
384             if stage_id:
385                 self.stage_set(cr, uid, [lead.id], stage_id)
386         return res
387
388     def case_mark_won(self, cr, uid, ids, context=None):
389         """Mark the case as lost: state = done and probability = 0%
390         """
391         res = super(crm_lead, self).case_close(cr, uid, ids, context=None)
392         self.write(cr, uid, ids, {'probability' : 100.0})
393         for lead in self.browse(cr, uid, ids):
394             stage_id = self.stage_find_won(cr, uid, lead.section_id.id or False)
395             if stage_id:
396                 self.stage_set(cr, uid, [lead.id], stage_id)
397             self._case_mark_won_notification(lead, context=context)
398         return res
399
400     def set_priority(self, cr, uid, ids, priority):
401         """Set lead priority
402         """
403         return self.write(cr, uid, ids, {'priority' : priority})
404
405     def set_high_priority(self, cr, uid, ids, context=None):
406         """Set lead priority to high
407         """
408         return self.set_priority(cr, uid, ids, '1')
409
410     def set_normal_priority(self, cr, uid, ids, context=None):
411         """Set lead priority to normal
412         """
413         return self.set_priority(cr, uid, ids, '3')
414
415
416     def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
417         # prepare opportunity data into dictionary for merging
418         opportunities = self.browse(cr, uid, ids, context=context)
419         def _get_first_not_null(attr):
420             if hasattr(oldest, attr):
421                 return getattr(oldest, attr)
422             for opportunity in opportunities:
423                 if hasattr(opportunity, attr):
424                     return getattr(opportunity, attr)
425             return False
426
427         def _get_first_not_null_id(attr):
428             res = _get_first_not_null(attr)
429             return res and res.id or False
430
431         def _concat_all(attr):
432             return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
433
434         data = {}
435         for field_name in fields:
436             field_info = self._all_columns.get(field_name)
437             if field_info is None:
438                 continue
439             field = field_info.column
440             if field._type in ('many2many', 'one2many'):
441                 continue
442             elif field._type == 'many2one':
443                 data[field_name] = _get_first_not_null_id(field_name)  # !!
444             elif field._type == 'text':
445                 data[field_name] = _concat_all(field_name)  #not lost
446             else:
447                 data[field_name] = _get_first_not_null(field_name)  #not lost
448         return data
449
450     def _merge_find_oldest(self, cr, uid, ids, context=None):
451         if context is None:
452             context = {}
453         #TOCHECK: where pass 'convert' in context ?
454         if context.get('convert'):
455             ids = list(set(ids) - set(context.get('lead_ids', False)) )
456
457         #search opportunities order by create date
458         opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
459         oldest_id = opportunity_ids[0]
460         return self.browse(cr, uid, oldest_id, context=context)
461
462     def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
463         body = []
464         if title:
465             body.append("%s\n" % (title))
466         for field_name in fields:
467             field_info = self._all_columns.get(field_name)
468             if field_info is None:
469                 continue
470             field = field_info.column
471             value = None
472
473             if field._type == 'selection':
474                 if hasattr(field.selection, '__call__'):
475                     key = field.selection(self, cr, uid, context=context)
476                 else:
477                     key = field.selection
478                 value = dict(key).get(lead[field_name], lead[field_name])
479             elif field._type == 'many2one':
480                 if lead[field_name]:
481                     value = lead[field_name].name_get()[0][1]
482             else:
483                 value = lead[field_name]
484
485             body.append("%s: %s" % (field.string, value or ''))
486         return "\n".join(body + ['---'])
487
488     def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
489         #TOFIX: mail template should be used instead of fix body, subject text
490         details = []
491         merge_message = _('Merged opportunities')
492         subject = [merge_message]
493         fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
494                   'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
495                   'country_id', 'city', 'street', 'street2', 'zip']
496         for opportunity in opportunities:
497             subject.append(opportunity.name)
498             title = "%s : %s" % (merge_message, opportunity.name)
499             details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
500
501         subject = subject[0] + ", ".join(subject[1:])
502         details = "\n\n".join(details)
503         return opportunity.message_append_note(subject, body=details, need_action_user_id=opportunity.user_id.id)
504
505     def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
506         message = self.pool.get('mail.message')
507         for opportunity in opportunities:
508             for history in opportunity.message_ids:
509                 message.write(cr, uid, history.id, {
510                         'res_id': opportunity_id,
511                         'subject' : _("From %s : %s") % (opportunity.name, history.subject)
512                 }, context=context)
513
514         return True
515
516     def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
517         attachment = self.pool.get('ir.attachment')
518
519         # return attachments of opportunity
520         def _get_attachments(opportunity_id):
521             attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
522             return attachment.browse(cr, uid, attachment_ids, context=context)
523
524         count = 1
525         first_attachments = _get_attachments(opportunity_id)
526         for opportunity in opportunities:
527             attachments = _get_attachments(opportunity.id)
528             for first in first_attachments:
529                 for attachment in attachments:
530                     if attachment.name == first.name:
531                         values = dict(
532                             name = "%s (%s)" % (attachment.name, count,),
533                             res_id = opportunity_id,
534                         )
535                         attachment.write(values)
536                         count+=1
537
538         return True
539
540     def merge_opportunity(self, cr, uid, ids, context=None):
541         """
542         To merge opportunities
543             :param ids: list of opportunities ids to merge
544         """
545         if context is None: context = {}
546
547         #TOCHECK: where pass lead_ids in context?
548         lead_ids = context and context.get('lead_ids', []) or []
549
550         if len(ids) <= 1:
551             raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
552
553         ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
554         opportunities = self.browse(cr, uid, ids, context=context)
555         opportunities_list = list(set(opportunities) - set(ctx_opportunities))
556         oldest = self._merge_find_oldest(cr, uid, ids, context=context)
557         if ctx_opportunities :
558             first_opportunity = ctx_opportunities[0]
559             tail_opportunities = opportunities_list
560         else:
561             first_opportunity = opportunities_list[0]
562             tail_opportunities = opportunities_list[1:]
563
564         fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id',
565             'partner_address_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
566             'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
567             'date_action_next', 'email_from', 'email_cc', 'partner_name']
568
569         data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
570
571         # merge data into first opportunity
572         self.write(cr, uid, [first_opportunity.id], data, context=context)
573
574         #copy message and attachements into the first opportunity
575         self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
576         self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
577
578         #Notification about loss of information
579         self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
580         #delete tail opportunities
581         self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
582
583         #open first opportunity
584         self.case_open(cr, uid, [first_opportunity.id])
585         return first_opportunity.id
586
587     def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
588         crm_stage = self.pool.get('crm.case.stage')
589         contact_id = False
590         if customer:
591             contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
592         if not section_id:
593             section_id = lead.section_id and lead.section_id.id or False
594         if section_id:
595             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
596         else:
597             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
598         stage_id = stage_ids and stage_ids[0] or False
599         return {
600                 'planned_revenue': lead.planned_revenue,
601                 'probability': lead.probability,
602                 'name': lead.name,
603                 'partner_id': customer and customer.id or False,
604                 'user_id': (lead.user_id and lead.user_id.id),
605                 'type': 'opportunity',
606                 'stage_id': stage_id or False,
607                 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
608                 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
609                 'partner_address_id': contact_id,
610         }
611
612     def _convert_opportunity_notification(self, cr, uid, lead, context=None):
613         success_message = _("Lead is <b>converted to an opportunity</b>.")
614         lead.message_append_note(success_message ,success_message, need_action_user_id=lead.user_id.id)
615         return True
616
617     def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
618         partner = self.pool.get('res.partner')
619         mail_message = self.pool.get('mail.message')
620         customer = False
621         if partner_id:
622             customer = partner.browse(cr, uid, partner_id, context=context)
623         for lead in self.browse(cr, uid, ids, context=context):
624             if lead.state in ('done', 'cancel'):
625                 continue
626             if user_ids or section_id:
627                 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
628
629             vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
630             self.write(cr, uid, [lead.id], vals, context=context)
631
632             self._convert_opportunity_notification(cr, uid, lead, context=context)
633             #TOCHECK: why need to change partner details in all messages of lead ?
634             if lead.partner_id:
635                 msg_ids = [ x.id for x in lead.message_ids]
636                 mail_message.write(cr, uid, msg_ids, {
637                         'partner_id': lead.partner_id.id
638                     }, context=context)
639         return True
640
641     def _lead_create_partner(self, cr, uid, lead, context=None):
642         partner = self.pool.get('res.partner')
643         partner_id = partner.create(cr, uid, {
644                     'name': lead.partner_name or lead.contact_name or lead.name,
645                     'user_id': lead.user_id.id,
646                     'comment': lead.description,
647                     'section_id': lead.section_id.id or False,
648                     'address': []
649         })
650         return partner_id
651
652     def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
653         res = False
654         res_partner = self.pool.get('res.partner')
655         if partner_id:
656             res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
657             contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
658             res = lead.write({'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
659
660         return res
661
662     def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
663         address = self.pool.get('res.partner.address')
664         return address.create(cr, uid, {
665                     'partner_id': partner_id,
666                     'name': lead.contact_name,
667                     'phone': lead.phone,
668                     'mobile': lead.mobile,
669                     'email': lead.email_from and to_email(lead.email_from)[0],
670                     'fax': lead.fax,
671                     'title': lead.title and lead.title.id or False,
672                     'function': lead.function,
673                     'street': lead.street,
674                     'street2': lead.street2,
675                     'zip': lead.zip,
676                     'city': lead.city,
677                     'country_id': lead.country_id and lead.country_id.id or False,
678                     'state_id': lead.state_id and lead.state_id.id or False,
679                 })
680
681     def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
682         """
683         This function convert partner based on action.
684         if action is 'create', create new partner with contact and assign lead to new partner_id.
685         otherwise assign lead to specified partner_id
686         """
687         if context is None:
688             context = {}
689         partner_ids = {}
690         for lead in self.browse(cr, uid, ids, context=context):
691             if action == 'create':
692                 if not partner_id:
693                     partner_id = self._lead_create_partner(cr, uid, lead, context=context)
694                 self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
695             self._lead_set_partner(cr, uid, lead, partner_id, context=context)
696             partner_ids[lead.id] = partner_id
697         return partner_ids
698
699     def _send_mail_to_salesman(self, cr, uid, lead, context=None):
700         """
701         Send mail to salesman with updated Lead details.
702         @ lead: browse record of 'crm.lead' object.
703         """
704         #TOFIX: mail template should be used here instead of fix subject, body text.
705         message = self.pool.get('mail.message')
706         email_to = lead.user_id and lead.user_id.user_email
707         if not email_to:
708             return False
709
710         email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
711         partner = lead.partner_id and lead.partner_id.name or lead.partner_name
712         subject = "lead %s converted into opportunity" % lead.name
713         body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
714         return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
715
716
717     def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
718         index = 0
719         for lead_id in ids:
720             value = {}
721             if team_id:
722                 value['section_id'] = team_id
723             if index < len(user_ids):
724                 value['user_id'] = user_ids[index]
725                 index += 1
726             if value:
727                 self.write(cr, uid, [lead_id], value, context=context)
728         return True
729
730     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):
731         """
732         action :('schedule','Schedule a call'), ('log','Log a call')
733         """
734         phonecall = self.pool.get('crm.phonecall')
735         model_data = self.pool.get('ir.model.data')
736         phonecall_dict = {}
737         if not categ_id:
738             res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
739             if res_id:
740                 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
741         for lead in self.browse(cr, uid, ids, context=context):
742             if not section_id:
743                 section_id = lead.section_id and lead.section_id.id or False
744             if not user_id:
745                 user_id = lead.user_id and lead.user_id.id or False
746             vals = {
747                     'name' : call_summary,
748                     'opportunity_id' : lead.id,
749                     'user_id' : user_id or False,
750                     'categ_id' : categ_id or False,
751                     'description' : desc or '',
752                     'date' : schedule_time,
753                     'section_id' : section_id or False,
754                     'partner_id': lead.partner_id and lead.partner_id.id or False,
755                     'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
756                     'partner_phone' : phone or lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
757                     'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
758                     'priority': lead.priority,
759             }
760
761             new_id = phonecall.create(cr, uid, vals, context=context)
762             phonecall.case_open(cr, uid, [new_id])
763             if action == 'log':
764                 phonecall.case_close(cr, uid, [new_id])
765             phonecall_dict[lead.id] = new_id
766             self._case_phonecall_notification(cr, uid, [new_id], lead, phonecall, action, context=context)
767         return phonecall_dict
768
769
770     def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
771         models_data = self.pool.get('ir.model.data')
772
773         # Get Opportunity views
774         form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
775         tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
776         return {
777                 'name': _('Opportunity'),
778                 'view_type': 'form',
779                 'view_mode': 'tree, form',
780                 'res_model': 'crm.lead',
781                 'domain': [('type', '=', 'opportunity')],
782                 'res_id': int(opportunity_id),
783                 'view_id': False,
784                 'views': [(form_view and form_view[1] or False, 'form'),
785                           (tree_view and tree_view[1] or False, 'tree'),
786                           (False, 'calendar'), (False, 'graph')],
787                 'type': 'ir.actions.act_window',
788         }
789
790
791     def message_new(self, cr, uid, msg, custom_values=None, context=None):
792         """Automatically calls when new email message arrives"""
793         res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
794         subject = msg.get('subject')  or _("No Subject")
795         body = msg.get('body_text')
796
797         msg_from = msg.get('from')
798         priority = msg.get('priority')
799         vals = {
800             'name': subject,
801             'email_from': msg_from,
802             'email_cc': msg.get('cc'),
803             'description': body,
804             'user_id': False,
805         }
806         if priority:
807             vals['priority'] = priority
808         vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
809         self.write(cr, uid, [res_id], vals, context)
810         return res_id
811
812     def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
813         if isinstance(ids, (str, int, long)):
814             ids = [ids]
815         if vals == None:
816             vals = {}
817         super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
818
819         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
820             vals['priority'] = msg.get('priority')
821         maps = {
822             'cost':'planned_cost',
823             'revenue': 'planned_revenue',
824             'probability':'probability'
825         }
826         vls = {}
827         for line in msg['body_text'].split('\n'):
828             line = line.strip()
829             res = tools.misc.command_re.match(line)
830             if res and maps.get(res.group(1).lower()):
831                 key = maps.get(res.group(1).lower())
832                 vls[key] = res.group(2).lower()
833         vals.update(vls)
834
835         # Unfortunately the API is based on lists
836         # but we want to update the state based on the
837         # previous state, so we have to loop:
838         for case in self.browse(cr, uid, ids, context=context):
839             values = dict(vals)
840             if case.state in CRM_LEAD_PENDING_STATES:
841                 #re-open
842                 values.update(state=crm.AVAILABLE_STATES[1][0])
843                 if not case.date_open:
844                     values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
845             res = self.write(cr, uid, [case.id], values, context=context)
846         return res
847
848     def action_makeMeeting(self, cr, uid, ids, context=None):
849         """
850         This opens Meeting's calendar view to schedule meeting on current Opportunity
851         @return : Dictionary value for created Meeting view
852         """
853         if context is None:
854             context = {}
855         value = {}
856         data_obj = self.pool.get('ir.model.data')
857         for opp in self.browse(cr, uid, ids, context=context):
858             # Get meeting views
859             tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
860             form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
861             calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
862             search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
863             context.update({
864                 'default_opportunity_id': opp.id,
865                 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
866                 'default_user_id': uid,
867                 'default_section_id': opp.section_id and opp.section_id.id or False,
868                 'default_email_from': opp.email_from,
869                 'default_state': 'open',
870                 'default_name': opp.name
871             })
872             value = {
873                 'name': _('Meetings'),
874                 'context': context,
875                 'view_type': 'form',
876                 'view_mode': 'calendar,form,tree',
877                 'res_model': 'crm.meeting',
878                 'view_id': False,
879                 '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')],
880                 'type': 'ir.actions.act_window',
881                 'search_view_id': search_view and search_view[1] or False,
882                 'nodestroy': True
883             }
884         return value
885
886
887     def unlink(self, cr, uid, ids, context=None):
888         for lead in self.browse(cr, uid, ids, context):
889             if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
890                 raise osv.except_osv(_('Error'),
891                     _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
892                       "You should better cancel it, instead of deleting it.") % lead.name)
893         return super(crm_lead, self).unlink(cr, uid, ids, context)
894
895
896     def write(self, cr, uid, ids, vals, context=None):
897         if not context:
898             context = {}
899
900         if 'date_closed' in vals:
901             return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
902
903         if vals.get('stage_id'):
904             stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
905             # change probability of lead(s) if required by stage
906             if not vals.get('probability') and stage.on_change:
907                 vals['probability'] = stage.probability
908             text = _("Changed Stage to: %s") % stage.name
909
910             for case in self.browse(cr, uid, ids, context=context):
911                 if case.type == 'lead' or context.get('stage_type') == 'lead':
912                     message = _("The stage of lead has been changed to <b>%s</b>.") % (stage.name)
913                     case.message_append_note(text, message)
914                 elif case.type == 'opportunity':
915                     message = _("The stage of opportunity has been changed to <b>%s</b>.") % (stage.name)
916                     case.message_append_note(text, message)
917
918         return super(crm_lead,self).write(cr, uid, ids, vals, context)
919
920 crm_lead()
921
922 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: