[MERGE]: Merge with lp:openobject-addons
[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_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Categories', \
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_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         'payment_mode': fields.many2one('crm.payment.mode', 'Payment Mode', \
263                             domain="[('section_id','=',section_id)]"),
264         'planned_cost': fields.float('Planned Costs'),
265     }
266
267     _defaults = {
268         'active': 1,
269         'type': 'lead',
270         'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
271         'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
272         'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
273         'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
274         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
275         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
276         'color': 0,
277     }
278
279     def create(self, cr, uid, vals, context=None):
280         obj_id = super(crm_lead, self).create(cr, uid, vals, context)
281         self.create_send_note(cr, uid, [obj_id], context=context)
282         return obj_id
283     
284     def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
285         if not stage_id:
286             return {'value':{}}
287         stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
288         if not stage.on_change:
289             return {'value':{}}
290         return {'value':{'probability': stage.probability}}
291
292     def on_change_partner(self, cr, uid, ids, partner_id, context=None):
293         result = {}
294         if partner_id:
295             partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
296             values = {
297                 'partner_name' : partner.name, 
298                 'street' : partner.street,
299                 'street2' : partner.street2,
300                 'city' : partner.city,
301                 'state_id' : partner.state_id and partner.state_id.id or False,
302                 'country_id' : partner.country_id and partner.country_id.id or False,
303             }
304         return {'value' : values}
305
306
307     def _check(self, cr, uid, ids=False, context=None):
308         """ Override of the base.stage method.
309             Function called by the scheduler to process cases for date actions
310             Only works on not done and cancelled cases
311         """
312         cr.execute('select * from crm_case \
313                 where (date_action_last<%s or date_action_last is null) \
314                 and (date_action_next<=%s or date_action_next is null) \
315                 and state not in (\'cancel\',\'done\')',
316                 (time.strftime("%Y-%m-%d %H:%M:%S"),
317                     time.strftime('%Y-%m-%d %H:%M:%S')))
318
319         ids2 = map(lambda x: x[0], cr.fetchall() or [])
320         cases = self.browse(cr, uid, ids2, context=context)
321         return self._action(cr, uid, cases, False, context=context)
322
323     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
324         """ Override of the base.stage method
325             Parameter of the stage search taken from the lead:
326             - type: stage type must be the same or 'both'
327             - section_id: if set, stages must belong to this section or
328               be a default stage; if not set, stages must be default
329               stages
330         """
331         if isinstance(cases, (int, long)):
332             cases = self.browse(cr, uid, cases, context=context)
333         # collect all section_ids
334         section_ids = []
335         types = ['both']
336         if section_id:
337             section_ids.append(section_id)
338         for lead in cases:
339             if lead.section_id:
340                 section_ids.append(lead.section_id.id)
341             if lead.type not in types:
342                 types.append(lead.type)
343         # OR all section_ids and OR with case_default
344         search_domain = []
345         if section_ids:
346             search_domain += [('|')] * len(section_ids)
347             for section_id in section_ids:
348                 search_domain.append(('section_ids', '=', section_id))
349         search_domain.append(('case_default', '=', True))
350         # AND with cases types
351         search_domain.append(('type', 'in', types))
352         # AND with the domain in parameter
353         search_domain += list(domain)
354         # perform search, return the first found
355         stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
356         if stage_ids:
357             return stage_ids[0]
358         return False
359
360     def case_cancel(self, cr, uid, ids, context=None):
361         """ Overrides case_cancel from base_stage to set probability """
362         res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
363         self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
364         return res
365
366     def case_reset(self, cr, uid, ids, context=None):
367         """ Overrides case_reset from base_stage to set probability """
368         res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
369         self.write(cr, uid, ids, {'probability': 0.0}, context=context)
370         return res
371
372     def case_mark_lost(self, cr, uid, ids, context=None):
373         """ Mark the case as lost: state=cancel and probability=0 """
374         for lead in self.browse(cr, uid, ids):
375             stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
376             if stage_id:
377                 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
378         self.case_mark_lost_send_note(cr, uid, ids, context=context)
379         return True
380
381     def case_mark_won(self, cr, uid, ids, context=None):
382         """ Mark the case as lost: state=done and probability=100 """
383         for lead in self.browse(cr, uid, ids):
384             stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
385             if stage_id:
386                 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
387         self.case_mark_won_send_note(cr, uid, ids, context=context)
388         return True
389
390     def set_priority(self, cr, uid, ids, priority):
391         """ Set lead priority
392         """
393         return self.write(cr, uid, ids, {'priority' : priority})
394
395     def set_high_priority(self, cr, uid, ids, context=None):
396         """ Set lead priority to high
397         """
398         return self.set_priority(cr, uid, ids, '1')
399
400     def set_normal_priority(self, cr, uid, ids, context=None):
401         """ Set lead priority to normal
402         """
403         return self.set_priority(cr, uid, ids, '3')
404
405     def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
406         # prepare opportunity data into dictionary for merging
407         opportunities = self.browse(cr, uid, ids, context=context)
408         def _get_first_not_null(attr):
409             if hasattr(oldest, attr):
410                 return getattr(oldest, attr)
411             for opportunity in opportunities:
412                 if hasattr(opportunity, attr):
413                     return getattr(opportunity, attr)
414             return False
415
416         def _get_first_not_null_id(attr):
417             res = _get_first_not_null(attr)
418             return res and res.id or False
419
420         def _concat_all(attr):
421             return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
422
423         data = {}
424         for field_name in fields:
425             field_info = self._all_columns.get(field_name)
426             if field_info is None:
427                 continue
428             field = field_info.column
429             if field._type in ('many2many', 'one2many'):
430                 continue
431             elif field._type == 'many2one':
432                 data[field_name] = _get_first_not_null_id(field_name)  # !!
433             elif field._type == 'text':
434                 data[field_name] = _concat_all(field_name)  #not lost
435             else:
436                 data[field_name] = _get_first_not_null(field_name)  #not lost
437         return data
438
439     def _merge_find_oldest(self, cr, uid, ids, context=None):
440         if context is None:
441             context = {}
442         #TOCHECK: where pass 'convert' in context ?
443         if context.get('convert'):
444             ids = list(set(ids) - set(context.get('lead_ids', False)) )
445
446         #search opportunities order by create date
447         opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
448         oldest_id = opportunity_ids[0]
449         return self.browse(cr, uid, oldest_id, context=context)
450
451     def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
452         body = []
453         if title:
454             body.append("%s\n" % (title))
455         for field_name in fields:
456             field_info = self._all_columns.get(field_name)
457             if field_info is None:
458                 continue
459             field = field_info.column
460             value = None
461
462             if field._type == 'selection':
463                 if hasattr(field.selection, '__call__'):
464                     key = field.selection(self, cr, uid, context=context)
465                 else:
466                     key = field.selection
467                 value = dict(key).get(lead[field_name], lead[field_name])
468             elif field._type == 'many2one':
469                 if lead[field_name]:
470                     value = lead[field_name].name_get()[0][1]
471             else:
472                 value = lead[field_name]
473
474             body.append("%s: %s" % (field.string, value or ''))
475         return "\n".join(body + ['---'])
476
477     def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
478         #TOFIX: mail template should be used instead of fix body, subject text
479         details = []
480         merge_message = _('Merged opportunities')
481         subject = [merge_message]
482         fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
483                   'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
484                   'country_id', 'city', 'street', 'street2', 'zip']
485         for opportunity in opportunities:
486             subject.append(opportunity.name)
487             title = "%s : %s" % (merge_message, opportunity.name)
488             details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
489
490         subject = subject[0] + ", ".join(subject[1:])
491         details = "\n\n".join(details)
492         return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
493
494     def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
495         message = self.pool.get('mail.message')
496         for opportunity in opportunities:
497             for history in opportunity.message_ids:
498                 message.write(cr, uid, history.id, {
499                         'res_id': opportunity_id,
500                         'subject' : _("From %s : %s") % (opportunity.name, history.subject)
501                 }, context=context)
502
503         return True
504
505     def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
506         attachment = self.pool.get('ir.attachment')
507
508         # return attachments of opportunity
509         def _get_attachments(opportunity_id):
510             attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
511             return attachment.browse(cr, uid, attachment_ids, context=context)
512
513         count = 1
514         first_attachments = _get_attachments(opportunity_id)
515         for opportunity in opportunities:
516             attachments = _get_attachments(opportunity.id)
517             for first in first_attachments:
518                 for attachment in attachments:
519                     if attachment.name == first.name:
520                         values = dict(
521                             name = "%s (%s)" % (attachment.name, count,),
522                             res_id = opportunity_id,
523                         )
524                         attachment.write(values)
525                         count+=1
526
527         return True
528
529     def merge_opportunity(self, cr, uid, ids, context=None):
530         """
531         To merge opportunities
532             :param ids: list of opportunities ids to merge
533         """
534         if context is None: context = {}
535
536         #TOCHECK: where pass lead_ids in context?
537         lead_ids = context and context.get('lead_ids', []) or []
538
539         if len(ids) <= 1:
540             raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
541
542         ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
543         opportunities = self.browse(cr, uid, ids, context=context)
544         opportunities_list = list(set(opportunities) - set(ctx_opportunities))
545         oldest = self._merge_find_oldest(cr, uid, ids, context=context)
546         if ctx_opportunities :
547             first_opportunity = ctx_opportunities[0]
548             tail_opportunities = opportunities_list
549         else:
550             first_opportunity = opportunities_list[0]
551             tail_opportunities = opportunities_list[1:]
552
553         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',
554             'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
555             'date_action_next', 'email_from', 'email_cc', 'partner_name']
556
557         data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
558
559         # merge data into first opportunity
560         self.write(cr, uid, [first_opportunity.id], data, context=context)
561
562         #copy message and attachements into the first opportunity
563         self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
564         self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
565
566         #Notification about loss of information
567         self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
568         #delete tail opportunities
569         self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
570
571         #open first opportunity
572         self.case_open(cr, uid, [first_opportunity.id])
573         return first_opportunity.id
574
575     def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
576         crm_stage = self.pool.get('crm.case.stage')
577         contact_id = False
578         if customer:
579             contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
580         if not section_id:
581             section_id = lead.section_id and lead.section_id.id or False
582         if section_id:
583             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
584         else:
585             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
586         stage_id = stage_ids and stage_ids[0] or False
587         return {
588                 'planned_revenue': lead.planned_revenue,
589                 'probability': lead.probability,
590                 'name': lead.name,
591                 'partner_id': customer and customer.id or False,
592                 'user_id': (lead.user_id and lead.user_id.id),
593                 'type': 'opportunity',
594                 'stage_id': stage_id or False,
595                 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
596                 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
597         }
598
599     def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
600         partner = self.pool.get('res.partner')
601         mail_message = self.pool.get('mail.message')
602         customer = False
603         if partner_id:
604             customer = partner.browse(cr, uid, partner_id, context=context)
605         for lead in self.browse(cr, uid, ids, context=context):
606             if lead.state in ('done', 'cancel'):
607                 continue
608             if user_ids or section_id:
609                 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
610
611             vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
612             self.write(cr, uid, [lead.id], vals, context=context)
613
614             self.convert_opportunity_send_note(cr, uid, lead, context=context)
615             #TOCHECK: why need to change partner details in all messages of lead ?
616             if lead.partner_id:
617                 msg_ids = [ x.id for x in lead.message_ids]
618                 mail_message.write(cr, uid, msg_ids, {
619                         'partner_id': lead.partner_id.id
620                     }, context=context)
621         return True
622
623     def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
624         partner = self.pool.get('res.partner')
625         vals = { 'name': name,
626             'user_id': lead.user_id.id,
627             'comment': lead.description,
628             'section_id': lead.section_id.id or False,
629             'parent_id': parent_id,
630             'phone': lead.phone,
631             'mobile': lead.mobile,
632             'email': lead.email_from and to_email(lead.email_from)[0],
633             'fax': lead.fax,
634             'title': lead.title and lead.title.id or False,
635             'function': lead.function,
636             'street': lead.street,
637             'street2': lead.street2,
638             'zip': lead.zip,
639             'city': lead.city,
640             'country_id': lead.country_id and lead.country_id.id or False,
641             'state_id': lead.state_id and lead.state_id.id or False,
642             'is_company': is_company,
643             'type': 'contact'
644         }
645         partner = partner.create(cr, uid,vals, context)
646         return partner
647
648     def _create_lead_partner(self, cr, uid, lead, context=None):
649         partner_id =  False
650         if lead.partner_name and lead.contact_name:
651             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
652             self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
653         elif lead.partner_name and not lead.contact_name:
654             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
655         elif not lead.partner_name and lead.contact_name:
656             partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
657         else:
658             partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
659         return partner_id
660
661     def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
662         res = False
663         res_partner = self.pool.get('res.partner')
664         if partner_id:
665             res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
666             contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
667             res = lead.write({'partner_id' : partner_id, }, context=context)
668             self._lead_set_partner_send_note(cr, uid, [lead.id], context)
669         return res
670
671     def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
672         """
673         This function convert partner based on action.
674         if action is 'create', create new partner with contact and assign lead to new partner_id.
675         otherwise assign lead to specified partner_id
676         """
677         if context is None:
678             context = {}
679         partner_ids = {}
680         for lead in self.browse(cr, uid, ids, context=context):
681             if action == 'create':
682                 if not partner_id:
683                     partner_id = self._create_lead_partner(cr, uid, lead, context)
684             self._lead_set_partner(cr, uid, lead, partner_id, context=context)
685             partner_ids[lead.id] = partner_id
686         return partner_ids
687
688     def _send_mail_to_salesman(self, cr, uid, lead, context=None):
689         """
690         Send mail to salesman with updated Lead details.
691         @ lead: browse record of 'crm.lead' object.
692         """
693         #TOFIX: mail template should be used here instead of fix subject, body text.
694         message = self.pool.get('mail.message')
695         email_to = lead.user_id and lead.user_id.user_email
696         if not email_to:
697             return False
698
699         email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
700         partner = lead.partner_id and lead.partner_id.name or lead.partner_name
701         subject = "lead %s converted into opportunity" % lead.name
702         body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
703         return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
704
705
706     def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
707         index = 0
708         for lead_id in ids:
709             value = {}
710             if team_id:
711                 value['section_id'] = team_id
712             if index < len(user_ids):
713                 value['user_id'] = user_ids[index]
714                 index += 1
715             if value:
716                 self.write(cr, uid, [lead_id], value, context=context)
717         return True
718
719     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):
720         """
721         action :('schedule','Schedule a call'), ('log','Log a call')
722         """
723         phonecall = self.pool.get('crm.phonecall')
724         model_data = self.pool.get('ir.model.data')
725         phonecall_dict = {}
726         if not categ_id:
727             res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
728             if res_id:
729                 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
730         for lead in self.browse(cr, uid, ids, context=context):
731             if not section_id:
732                 section_id = lead.section_id and lead.section_id.id or False
733             if not user_id:
734                 user_id = lead.user_id and lead.user_id.id or False
735             vals = {
736                     'name' : call_summary,
737                     'opportunity_id' : lead.id,
738                     'user_id' : user_id or False,
739                     'categ_id' : categ_id or False,
740                     'description' : desc or '',
741                     'date' : schedule_time,
742                     'section_id' : section_id or False,
743                     'partner_id': lead.partner_id and lead.partner_id.id or False,
744                     'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
745                     'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
746                     'priority': lead.priority,
747             }
748             new_id = phonecall.create(cr, uid, vals, context=context)
749             phonecall.case_open(cr, uid, [new_id], context=context)
750             if action == 'log':
751                 phonecall.case_close(cr, uid, [new_id], context=context)
752             phonecall_dict[lead.id] = new_id
753             self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
754         return phonecall_dict
755
756
757     def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
758         models_data = self.pool.get('ir.model.data')
759
760         # Get Opportunity views
761         form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
762         tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
763         return {
764                 'name': _('Opportunity'),
765                 'view_type': 'form',
766                 'view_mode': 'tree, form',
767                 'res_model': 'crm.lead',
768                 'domain': [('type', '=', 'opportunity')],
769                 'res_id': int(opportunity_id),
770                 'view_id': False,
771                 'views': [(form_view and form_view[1] or False, 'form'),
772                           (tree_view and tree_view[1] or False, 'tree'),
773                           (False, 'calendar'), (False, 'graph')],
774                 'type': 'ir.actions.act_window',
775         }
776
777     def action_makeMeeting(self, cr, uid, ids, context=None):
778         """ This opens Meeting's calendar view to schedule meeting on current Opportunity
779             @return : Dictionary value for created Meeting view
780         """
781         opportunity = self.browse(cr, uid, ids[0], context)
782         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
783         res['context'] = {
784             'default_opportunity_id': opportunity.id,
785             'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
786             'default_partner_ids' : 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: