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