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