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