1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-today OpenERP SA (<http://www.openerp.com>)
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.
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.
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/>.
20 ##############################################################################
23 from base_status.base_stage import base_stage
25 from datetime import datetime
26 from mail.mail_message import to_email
27 from osv import fields, osv
30 from tools.translate import _
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
38 class crm_lead(base_stage, osv.osv):
41 _description = "Lead/Opportunity"
42 _order = "priority,date_action,id desc"
43 _inherit = ['ir.needaction_mixin', 'mail.thread']
44 _mail_compose_message = True
46 def _get_default_section_id(self, cr, uid, context=None):
47 """ Gives default section by checking if present in the context """
48 return (self._resolve_section_id_from_context(cr, uid, context=context) or False)
50 def _get_default_stage_id(self, cr, uid, context=None):
51 """ Gives default stage_id """
52 section_id = self._get_default_section_id(cr, uid, context=context)
53 return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft'), ('type', '=', 'both')], context=context)
55 def _resolve_section_id_from_context(self, cr, uid, context=None):
56 """ Returns ID of section based on the value of 'section_id'
57 context key, or None if it cannot be resolved to a single
62 if type(context.get('default_section_id')) in (int, long):
63 return context.get('default_section_id')
64 if isinstance(context.get('default_section_id'), basestring):
65 section_name = context['default_section_id']
66 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=section_name, context=context)
67 if len(section_ids) == 1:
68 return int(section_ids[0][0])
71 def _resolve_type_from_context(self, cr, uid, context=None):
72 """ Returns the type (lead or opportunity) from the type context
73 key. Returns None if it cannot be resolved.
77 return context.get('default_type')
79 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
80 access_rights_uid = access_rights_uid or uid
81 stage_obj = self.pool.get('crm.case.stage')
82 order = stage_obj._order
83 # lame hack to allow reverting search, should just work in the trivial case
84 if read_group_order == 'stage_id desc':
85 order = "%s desc" % order
86 # retrieve section_id from the context and write the domain
87 # - ('id', 'in', 'ids'): add columns that should be present
88 # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
89 # - OR ('section_ids', '=', section_id), ('fold', '=', False) if section_id: add section columns that are not folded
91 section_id = self._resolve_section_id_from_context(cr, uid, context=context)
93 search_domain += ['|', '&', ('section_ids', '=', section_id), ('fold', '=', False)]
94 search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)]
95 # retrieve type from the context (if set: choose 'type' or 'both')
96 type = self._resolve_type_from_context(cr, uid, context=context)
98 search_domain += ['|', ('type', '=', type), ('type', '=', 'both')]
100 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
101 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
102 # restore order of the search
103 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
107 'stage_id': _read_group_stage_ids
110 def _compute_day(self, cr, uid, ids, fields, args, context=None):
112 @param cr: the current row, from the database cursor,
113 @param uid: the current user’s ID for security checks,
114 @param ids: List of Openday’s IDs
115 @return: difference between current date and log date
116 @param context: A standard dictionary for contextual values
118 cal_obj = self.pool.get('resource.calendar')
119 res_obj = self.pool.get('resource.resource')
122 for lead in self.browse(cr, uid, ids, context=context):
127 if field == 'day_open':
129 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
130 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
131 ans = date_open - date_create
132 date_until = lead.date_open
133 elif field == 'day_close':
135 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
136 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
137 date_until = lead.date_closed
138 ans = date_close - date_create
142 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
143 if len(resource_ids):
144 resource_id = resource_ids[0]
146 duration = float(ans.days)
147 if lead.section_id and lead.section_id.resource_calendar_id:
148 duration = float(ans.days) * 24
149 new_dates = cal_obj.interval_get(cr,
151 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
152 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
157 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
158 for in_time, out_time in new_dates:
159 if in_time.date not in no_days:
160 no_days.append(in_time.date)
161 if out_time > date_until:
163 duration = len(no_days)
164 res[lead.id][field] = abs(int(duration))
167 def _history_search(self, cr, uid, obj, name, args, context=None):
169 msg_obj = self.pool.get('mail.message')
170 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
171 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
174 return [('id', 'in', lead_ids)]
176 return [('id', '=', '0')]
178 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
180 for obj in self.browse(cr, uid, ids, context=context):
182 for msg in obj.message_ids:
184 res[obj.id] = msg.subject
189 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
190 select=True, help="Optional linked partner, usually after conversion of the lead"),
192 'id': fields.integer('ID', readonly=True),
193 'name': fields.char('Subject', size=64, required=True, select=1),
194 'active': fields.boolean('Active', required=False),
195 'date_action_last': fields.datetime('Last Action', readonly=1),
196 'date_action_next': fields.datetime('Next Action', readonly=1),
197 'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
198 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
199 select=True, help='When sending mails, the default email address is taken from the sales team.'),
200 'create_date': fields.datetime('Creation Date' , readonly=True),
201 'email_cc': fields.text('Global CC', size=252 , help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
202 'description': fields.text('Notes'),
203 'write_date': fields.datetime('Update Date' , readonly=True),
204 'categ_id': fields.many2one('crm.case.categ', 'Category', \
205 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
206 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
207 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
208 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
209 'contact_name': fields.char('Contact Name', size=64),
210 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner company that will be created while converting the lead into opportunity', select=1),
211 'opt_in': fields.boolean('Opt-In', oldname='optin', help="If opt-in is checked, this contact has accepted to receive emails."),
212 'opt_out': fields.boolean('Opt-Out', oldname='optout', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
213 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
214 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
215 'date_closed': fields.datetime('Closed', readonly=True),
216 'stage_id': fields.many2one('crm.case.stage', 'Stage',
217 domain="['&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
218 'user_id': fields.many2one('res.users', 'Salesperson'),
219 'referred': fields.char('Referred By', size=64),
220 'date_open': fields.datetime('Opened', readonly=True),
221 'day_open': fields.function(_compute_day, string='Days to Open', \
222 multi='day_open', type="float", store=True),
223 'day_close': fields.function(_compute_day, string='Days to Close', \
224 multi='day_close', type="float", store=True),
225 'state': fields.related('stage_id', 'state', type="selection", store=True,
226 selection=crm.AVAILABLE_STATES, string="State", readonly=True,
227 help='The state is set to \'Draft\', when a case is created.\
228 If the case is in progress the state is set to \'Open\'.\
229 When the case is over, the state is set to \'Done\'.\
230 If the case needs to be reviewed then the state is \
231 set to \'Pending\'.'),
232 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
234 # Only used for type opportunity
235 'probability': fields.float('Success Rate (%)',group_operator="avg"),
236 'planned_revenue': fields.float('Expected Revenue'),
237 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
238 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
239 'phone': fields.char("Phone", size=64),
240 'date_deadline': fields.date('Expected Closing'),
241 'date_action': fields.date('Next Action Date', select=True),
242 'title_action': fields.char('Next Action', size=64),
243 'color': fields.integer('Color Index'),
244 'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
245 'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
246 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
247 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
248 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
250 # Fields for address, due to separation from crm and res.partner
251 'street': fields.char('Street', size=128),
252 'street2': fields.char('Street2', size=128),
253 'zip': fields.char('Zip', change_default=True, size=24),
254 'city': fields.char('City', size=128),
255 'state_id': fields.many2one("res.country.state", 'State', domain="[('country_id','=',country_id)]"),
256 'country_id': fields.many2one('res.country', 'Country'),
257 'phone': fields.char('Phone', size=64),
258 'fax': fields.char('Fax', size=64),
259 'mobile': fields.char('Mobile', size=64),
260 'function': fields.char('Function', size=128),
261 'title': fields.many2one('res.partner.title', 'Title'),
262 'company_id': fields.many2one('res.company', 'Company', select=1),
268 'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
269 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
270 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
271 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
272 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
273 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
277 def create(self, cr, uid, vals, context=None):
278 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
279 self.create_send_note(cr, uid, [obj_id], context=context)
282 def on_change_opt_in(self, cr, uid, ids, opt_in):
283 return {'value':{'opt_in':opt_in,'opt_out':False}}
285 def on_change_opt_out(self, cr, uid, ids, opt_out):
286 return {'value':{'opt_out':opt_out,'opt_in':False}}
288 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
291 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
292 if not stage.on_change:
294 return {'value':{'probability': stage.probability}}
296 def _check(self, cr, uid, ids=False, context=None):
297 """ Override of the base.stage method.
298 Function called by the scheduler to process cases for date actions
299 Only works on not done and cancelled cases
301 cr.execute('select * from crm_case \
302 where (date_action_last<%s or date_action_last is null) \
303 and (date_action_next<=%s or date_action_next is null) \
304 and state not in (\'cancel\',\'done\')',
305 (time.strftime("%Y-%m-%d %H:%M:%S"),
306 time.strftime('%Y-%m-%d %H:%M:%S')))
308 ids2 = map(lambda x: x[0], cr.fetchall() or [])
309 cases = self.browse(cr, uid, ids2, context=context)
310 return self._action(cr, uid, cases, False, context=context)
312 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
313 """ Override of the base.stage method
314 Parameter of the stage search taken from the lead:
315 - type: stage type must be the same or 'both'
316 - section_id: if set, stages must belong to this section or
317 be a default stage; if not set, stages must be default
320 if isinstance(cases, (int, long)):
321 cases = self.browse(cr, uid, cases, context=context)
322 # collect all section_ids
326 section_ids.append(section_id)
329 section_ids.append(lead.section_id.id)
330 if lead.type not in types:
331 types.append(lead.type)
332 # OR all section_ids and OR with case_default
335 search_domain += [('|')] * len(section_ids)
336 for section_id in section_ids:
337 search_domain.append(('section_ids', '=', section_id))
338 search_domain.append(('case_default', '=', True))
339 # AND with cases types
340 search_domain.append(('type', 'in', types))
341 # AND with the domain in parameter
342 search_domain += list(domain)
343 # perform search, return the first found
344 stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
349 def case_cancel(self, cr, uid, ids, context=None):
350 """ Overrides case_cancel from base_stage to set probability """
351 res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
352 self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
355 def case_reset(self, cr, uid, ids, context=None):
356 """ Overrides case_reset from base_stage to set probability """
357 res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
358 self.write(cr, uid, ids, {'probability': 0.0}, context=context)
361 def case_mark_lost(self, cr, uid, ids, context=None):
362 """ Mark the case as lost: state=cancel and probability=0 """
363 for lead in self.browse(cr, uid, ids):
364 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
366 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
367 self.case_mark_lost_send_note(cr, uid, ids, context=context)
370 def case_mark_won(self, cr, uid, ids, context=None):
371 """ Mark the case as lost: state=done and probability=100 """
372 for lead in self.browse(cr, uid, ids):
373 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
375 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
376 self.case_mark_won_send_note(cr, uid, ids, context=context)
379 def set_priority(self, cr, uid, ids, priority):
380 """ Set lead priority
382 return self.write(cr, uid, ids, {'priority' : priority})
384 def set_high_priority(self, cr, uid, ids, context=None):
385 """ Set lead priority to high
387 return self.set_priority(cr, uid, ids, '1')
389 def set_normal_priority(self, cr, uid, ids, context=None):
390 """ Set lead priority to normal
392 return self.set_priority(cr, uid, ids, '3')
394 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
395 # prepare opportunity data into dictionary for merging
396 opportunities = self.browse(cr, uid, ids, context=context)
397 def _get_first_not_null(attr):
398 if hasattr(oldest, attr):
399 return getattr(oldest, attr)
400 for opportunity in opportunities:
401 if hasattr(opportunity, attr):
402 return getattr(opportunity, attr)
405 def _get_first_not_null_id(attr):
406 res = _get_first_not_null(attr)
407 return res and res.id or False
409 def _concat_all(attr):
410 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
413 for field_name in fields:
414 field_info = self._all_columns.get(field_name)
415 if field_info is None:
417 field = field_info.column
418 if field._type in ('many2many', 'one2many'):
420 elif field._type == 'many2one':
421 data[field_name] = _get_first_not_null_id(field_name) # !!
422 elif field._type == 'text':
423 data[field_name] = _concat_all(field_name) #not lost
425 data[field_name] = _get_first_not_null(field_name) #not lost
428 def _merge_find_oldest(self, cr, uid, ids, context=None):
431 #TOCHECK: where pass 'convert' in context ?
432 if context.get('convert'):
433 ids = list(set(ids) - set(context.get('lead_ids', False)) )
435 #search opportunities order by create date
436 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
437 oldest_id = opportunity_ids[0]
438 return self.browse(cr, uid, oldest_id, context=context)
440 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
443 body.append("%s\n" % (title))
444 for field_name in fields:
445 field_info = self._all_columns.get(field_name)
446 if field_info is None:
448 field = field_info.column
451 if field._type == 'selection':
452 if hasattr(field.selection, '__call__'):
453 key = field.selection(self, cr, uid, context=context)
455 key = field.selection
456 value = dict(key).get(lead[field_name], lead[field_name])
457 elif field._type == 'many2one':
459 value = lead[field_name].name_get()[0][1]
461 value = lead[field_name]
463 body.append("%s: %s" % (field.string, value or ''))
464 return "\n".join(body + ['---'])
466 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
467 #TOFIX: mail template should be used instead of fix body, subject text
469 merge_message = _('Merged opportunities')
470 subject = [merge_message]
471 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
472 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
473 'country_id', 'city', 'street', 'street2', 'zip']
474 for opportunity in opportunities:
475 subject.append(opportunity.name)
476 title = "%s : %s" % (merge_message, opportunity.name)
477 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
479 subject = subject[0] + ", ".join(subject[1:])
480 details = "\n\n".join(details)
481 return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
483 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
484 message = self.pool.get('mail.message')
485 for opportunity in opportunities:
486 for history in opportunity.message_ids:
487 message.write(cr, uid, history.id, {
488 'res_id': opportunity_id,
489 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
494 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
495 attachment = self.pool.get('ir.attachment')
497 # return attachments of opportunity
498 def _get_attachments(opportunity_id):
499 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
500 return attachment.browse(cr, uid, attachment_ids, context=context)
503 first_attachments = _get_attachments(opportunity_id)
504 for opportunity in opportunities:
505 attachments = _get_attachments(opportunity.id)
506 for first in first_attachments:
507 for attachment in attachments:
508 if attachment.name == first.name:
510 name = "%s (%s)" % (attachment.name, count,),
511 res_id = opportunity_id,
513 attachment.write(values)
518 def merge_opportunity(self, cr, uid, ids, context=None):
520 To merge opportunities
521 :param ids: list of opportunities ids to merge
523 if context is None: context = {}
525 #TOCHECK: where pass lead_ids in context?
526 lead_ids = context and context.get('lead_ids', []) or []
529 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
531 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
532 opportunities = self.browse(cr, uid, ids, context=context)
533 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
534 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
535 if ctx_opportunities :
536 first_opportunity = ctx_opportunities[0]
537 tail_opportunities = opportunities_list
539 first_opportunity = opportunities_list[0]
540 tail_opportunities = opportunities_list[1:]
542 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',
543 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
544 'date_action_next', 'email_from', 'email_cc', 'partner_name']
546 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
548 # merge data into first opportunity
549 self.write(cr, uid, [first_opportunity.id], data, context=context)
551 #copy message and attachements into the first opportunity
552 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
553 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
555 #Notification about loss of information
556 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
557 #delete tail opportunities
558 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
560 #open first opportunity
561 self.case_open(cr, uid, [first_opportunity.id])
562 return first_opportunity.id
564 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
565 crm_stage = self.pool.get('crm.case.stage')
568 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
570 section_id = lead.section_id and lead.section_id.id or False
572 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
574 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
575 stage_id = stage_ids and stage_ids[0] or False
577 'planned_revenue': lead.planned_revenue,
578 'probability': lead.probability,
580 'partner_id': customer and customer.id or False,
581 'user_id': (lead.user_id and lead.user_id.id),
582 'type': 'opportunity',
583 'stage_id': stage_id or False,
584 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
585 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
588 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
589 partner = self.pool.get('res.partner')
590 mail_message = self.pool.get('mail.message')
593 customer = partner.browse(cr, uid, partner_id, context=context)
594 for lead in self.browse(cr, uid, ids, context=context):
595 if lead.state in ('done', 'cancel'):
597 if user_ids or section_id:
598 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
600 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
601 self.write(cr, uid, [lead.id], vals, context=context)
603 self.convert_opportunity_send_note(cr, uid, lead, context=context)
604 #TOCHECK: why need to change partner details in all messages of lead ?
606 msg_ids = [ x.id for x in lead.message_ids]
607 mail_message.write(cr, uid, msg_ids, {
608 'partner_id': lead.partner_id.id
612 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
613 partner = self.pool.get('res.partner')
614 vals = { 'name': name,
615 'user_id': lead.user_id.id,
616 'comment': lead.description,
617 'section_id': lead.section_id.id or False,
618 'parent_id': parent_id,
620 'mobile': lead.mobile,
621 'email': lead.email_from and to_email(lead.email_from)[0],
623 'title': lead.title and lead.title.id or False,
624 'function': lead.function,
625 'street': lead.street,
626 'street2': lead.street2,
629 'country_id': lead.country_id and lead.country_id.id or False,
630 'state_id': lead.state_id and lead.state_id.id or False,
631 'is_company': is_company,
634 partner = partner.create(cr, uid,vals, context)
637 def _create_lead_partner(self, cr, uid, lead, context=None):
639 if lead.partner_name and lead.contact_name:
640 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
641 self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
642 elif lead.partner_name and not lead.contact_name:
643 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
644 elif not lead.partner_name and lead.contact_name:
645 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
647 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
650 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
652 res_partner = self.pool.get('res.partner')
654 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
655 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
656 res = lead.write({'partner_id' : partner_id, }, context=context)
657 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
660 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
662 This function convert partner based on action.
663 if action is 'create', create new partner with contact and assign lead to new partner_id.
664 otherwise assign lead to specified partner_id
669 for lead in self.browse(cr, uid, ids, context=context):
670 if action == 'create':
672 partner_id = self._create_lead_partner(cr, uid, lead, context)
673 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
674 partner_ids[lead.id] = partner_id
677 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
679 Send mail to salesman with updated Lead details.
680 @ lead: browse record of 'crm.lead' object.
682 #TOFIX: mail template should be used here instead of fix subject, body text.
683 message = self.pool.get('mail.message')
684 email_to = lead.user_id and lead.user_id.user_email
688 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
689 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
690 subject = "lead %s converted into opportunity" % lead.name
691 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
692 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
695 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
700 value['section_id'] = team_id
701 if index < len(user_ids):
702 value['user_id'] = user_ids[index]
705 self.write(cr, uid, [lead_id], value, context=context)
708 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):
710 action :('schedule','Schedule a call'), ('log','Log a call')
712 phonecall = self.pool.get('crm.phonecall')
713 model_data = self.pool.get('ir.model.data')
716 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
718 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
719 for lead in self.browse(cr, uid, ids, context=context):
721 section_id = lead.section_id and lead.section_id.id or False
723 user_id = lead.user_id and lead.user_id.id or False
725 'name' : call_summary,
726 'opportunity_id' : lead.id,
727 'user_id' : user_id or False,
728 'categ_id' : categ_id or False,
729 'description' : desc or '',
730 'date' : schedule_time,
731 'section_id' : section_id or False,
732 'partner_id': lead.partner_id and lead.partner_id.id or False,
733 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
734 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
735 'priority': lead.priority,
737 new_id = phonecall.create(cr, uid, vals, context=context)
738 phonecall.case_open(cr, uid, [new_id], context=context)
740 phonecall.case_close(cr, uid, [new_id], context=context)
741 phonecall_dict[lead.id] = new_id
742 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
743 return phonecall_dict
746 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
747 models_data = self.pool.get('ir.model.data')
749 # Get Opportunity views
750 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
751 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
753 'name': _('Opportunity'),
755 'view_mode': 'tree, form',
756 'res_model': 'crm.lead',
757 'domain': [('type', '=', 'opportunity')],
758 'res_id': int(opportunity_id),
760 'views': [(form_view and form_view[1] or False, 'form'),
761 (tree_view and tree_view[1] or False, 'tree'),
762 (False, 'calendar'), (False, 'graph')],
763 'type': 'ir.actions.act_window',
766 def action_makeMeeting(self, cr, uid, ids, context=None):
767 """ This opens Meeting's calendar view to schedule meeting on current Opportunity
768 @return : Dictionary value for created Meeting view
770 opportunity = self.browse(cr, uid, ids[0], context)
771 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
773 'default_opportunity_id': opportunity.id,
774 'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
775 'default_user_id': uid,
776 'default_section_id': opportunity.section_id and opportunity.section_id.id or False,
777 'default_email_from': opportunity.email_from,
778 'default_state': 'open',
779 'default_name': opportunity.name,
783 def unlink(self, cr, uid, ids, context=None):
784 for lead in self.browse(cr, uid, ids, context):
785 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
786 raise osv.except_osv(_('Error'),
787 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
788 "You should better cancel it, instead of deleting it.") % lead.name)
789 return super(crm_lead, self).unlink(cr, uid, ids, context)
791 def write(self, cr, uid, ids, vals, context=None):
792 if vals.get('stage_id') and not vals.get('probability'):
793 # change probability of lead(s) if required by stage
794 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
796 vals['probability'] = stage.probability
797 return super(crm_lead,self).write(cr, uid, ids, vals, context)
799 # ----------------------------------------
801 # ----------------------------------------
803 def message_new(self, cr, uid, msg, custom_values=None, context=None):
804 """ Overrides mail_thread message_new that is called by the mailgateway
805 through message_process.
806 This override updates the document according to the email.
808 if custom_values is None: custom_values = {}
809 custom_values.update({
810 'name': msg.get('subject') or _("No Subject"),
811 'description': msg.get('body_text'),
812 'email_from': msg.get('from'),
813 'email_cc': msg.get('cc'),
816 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
817 custom_values['priority'] = msg.get('priority')
818 custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from', False), context=context))
819 return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
821 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
822 """ Overrides mail_thread message_update that is called by the mailgateway
823 through message_process.
824 This method updates the document according to the email.
826 if isinstance(ids, (str, int, long)):
828 if update_vals is None: update_vals = {}
830 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
831 vals['priority'] = msg.get('priority')
833 'cost':'planned_cost',
834 'revenue': 'planned_revenue',
835 'probability':'probability',
837 for line in msg.get('body_text', '').split('\n'):
839 res = tools.misc.command_re.match(line)
840 if res and maps.get(res.group(1).lower()):
841 key = maps.get(res.group(1).lower())
842 vals[key] = res.group(2).lower()
844 return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
846 # ----------------------------------------
847 # OpenChatter methods and notifications
848 # ----------------------------------------
850 def message_get_subscribers(self, cr, uid, ids, context=None):
851 """ Override to add the salesman. """
852 user_ids = super(crm_lead, self).message_get_subscribers(cr, uid, ids, context=context)
853 for obj in self.browse(cr, uid, ids, context=context):
854 if obj.user_id and not obj.user_id.id in user_ids:
855 user_ids.append(obj.user_id.id)
858 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
859 """ Override of the (void) default notification method. """
860 stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
861 return self.message_append_note(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
863 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
864 if isinstance(lead, (int, long)):
865 lead = self.browse(cr, uid, [lead], context=context)[0]
866 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
868 def create_send_note(self, cr, uid, ids, context=None):
870 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
871 self.message_append_note(cr, uid, [id], body=message, subtype='create', context=context)
874 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
875 message = _("Opportunity has been <b>lost</b>.")
876 return self.message_append_note(cr, uid, ids, body=message, subtype='lost', context=context)
878 def case_mark_won_send_note(self, cr, uid, ids, context=None):
879 message = _("Opportunity has been <b>won</b>.")
880 return self.message_append_note(cr, uid, ids, body=message, subtype='won', context=context)
882 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
883 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
884 if action == 'log': prefix = 'Logged'
885 else: prefix = 'Scheduled'
886 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
887 return self.message_append_note(cr, uid, ids, body=message, context=context)
889 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
890 for lead in self.browse(cr, uid, ids, context=context):
891 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))
892 lead.message_append_note(body=message)
895 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
896 message = _("Lead has been <b>converted to an opportunity</b>.")
897 lead.message_append_note(body=message)
902 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: