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