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