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