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