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','res.partner']
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)
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)
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
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])
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.
76 return context.get('default_type')
78 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
79 access_rights_uid = access_rights_uid or uid
80 stage_obj = self.pool.get('crm.case.stage')
81 order = stage_obj._order
82 # lame hack to allow reverting search, should just work in the trivial case
83 if read_group_order == 'stage_id desc':
84 order = "%s desc" % order
85 # retrieve section_id from the context and write the domain
86 # - ('id', 'in', 'ids'): add columns that should be present
87 # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
88 # - OR ('section_ids', '=', section_id), ('fold', '=', False) if section_id: add section columns that are not folded
90 section_id = self._resolve_section_id_from_context(cr, uid, context=context)
92 search_domain += ['|', '&', ('section_ids', '=', section_id), ('fold', '=', False)]
93 search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)]
94 # retrieve type from the context (if set: choose 'type' or 'both')
95 type = self._resolve_type_from_context(cr, uid, context=context)
97 search_domain += ['|', ('type', '=', type), ('type', '=', 'both')]
99 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
100 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
101 # restore order of the search
102 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
106 'stage_id': _read_group_stage_ids
109 def _compute_day(self, cr, uid, ids, fields, args, context=None):
111 @param cr: the current row, from the database cursor,
112 @param uid: the current user’s ID for security checks,
113 @param ids: List of Openday’s IDs
114 @return: difference between current date and log date
115 @param context: A standard dictionary for contextual values
117 cal_obj = self.pool.get('resource.calendar')
118 res_obj = self.pool.get('resource.resource')
121 for lead in self.browse(cr, uid, ids, context=context):
126 if field == 'day_open':
128 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
129 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
130 ans = date_open - date_create
131 date_until = lead.date_open
132 elif field == 'day_close':
134 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
135 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
136 date_until = lead.date_closed
137 ans = date_close - date_create
141 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
142 if len(resource_ids):
143 resource_id = resource_ids[0]
145 duration = float(ans.days)
146 if lead.section_id and lead.section_id.resource_calendar_id:
147 duration = float(ans.days) * 24
148 new_dates = cal_obj.interval_get(cr,
150 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
151 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
156 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
157 for in_time, out_time in new_dates:
158 if in_time.date not in no_days:
159 no_days.append(in_time.date)
160 if out_time > date_until:
162 duration = len(no_days)
163 res[lead.id][field] = abs(int(duration))
166 def _history_search(self, cr, uid, obj, name, args, context=None):
168 msg_obj = self.pool.get('mail.message')
169 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
170 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
173 return [('id', 'in', lead_ids)]
175 return [('id', '=', '0')]
177 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
179 for obj in self.browse(cr, uid, ids, context=context):
181 for msg in obj.message_ids:
183 res[obj.id] = msg.subject
188 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
189 select=True, help="Optional linked partner, usually after conversion of the lead"),
191 'id': fields.integer('ID', readonly=True),
192 'name': fields.char('Name', size=64, select=1),
193 'active': fields.boolean('Active', required=False),
194 'date_action_last': fields.datetime('Last Action', readonly=1),
195 'date_action_next': fields.datetime('Next Action', readonly=1),
196 'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
197 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
198 select=True, help='When sending mails, the default email address is taken from the sales team.'),
199 'create_date': fields.datetime('Creation Date' , readonly=True),
200 'email_cc': fields.text('Global CC', size=252 , help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
201 'description': fields.text('Notes'),
202 'write_date': fields.datetime('Update Date' , readonly=True),
203 'categ_id': fields.many2one('crm.case.categ', 'Category', \
204 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
205 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
206 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
207 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
208 'contact_name': fields.char('Contact Name', size=64),
209 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner company that will be created while converting the lead into opportunity', select=1),
210 'opt_in': fields.boolean('Opt-In', oldname='optin', help="If opt-in is checked, this contact has accepted to receive emails."),
211 'opt_out': fields.boolean('Opt-Out', oldname='optout', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
212 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
213 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
214 'date_closed': fields.datetime('Closed', readonly=True),
215 'stage_id': fields.many2one('crm.case.stage', 'Stage',
216 domain="['&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
217 'user_id': fields.many2one('res.users', 'Salesperson'),
218 'referred': fields.char('Referred By', size=64),
219 'date_open': fields.datetime('Opened', readonly=True),
220 'day_open': fields.function(_compute_day, string='Days to Open', \
221 multi='day_open', type="float", store=True),
222 'day_close': fields.function(_compute_day, string='Days to Close', \
223 multi='day_close', type="float", store=True),
224 'state': fields.related('stage_id', 'state', type="selection", store=True,
225 selection=crm.AVAILABLE_STATES, string="State", readonly=True,
226 help='The state is set to \'Draft\', when a case is created.\
227 If the case is in progress the state is set to \'Open\'.\
228 When the case is over, the state is set to \'Done\'.\
229 If the case needs to be reviewed then the state is \
230 set to \'Pending\'.'),
231 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
233 # Only used for type opportunity
234 'probability': fields.float('Success Rate (%)',group_operator="avg"),
235 'planned_revenue': fields.float('Expected Revenue'),
236 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
237 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
238 'phone': fields.char("Phone", size=64),
239 'date_deadline': fields.date('Expected Closing'),
240 'date_action': fields.date('Next Action Date', select=True),
241 'title_action': fields.char('Next Action', size=64),
242 'color': fields.integer('Color Index'),
243 'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
244 'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
245 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
246 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
247 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
253 'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
254 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
255 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
256 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
257 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
258 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
262 def get_needaction_user_ids(self, cr, uid, ids, context=None):
263 result = dict.fromkeys(ids, [])
264 for obj in self.browse(cr, uid, ids, context=context):
265 # salesman must perform an action when in draft mode
266 if obj.state == 'draft' and obj.user_id:
267 result[obj.id] = [obj.user_id.id]
270 def create(self, cr, uid, vals, context=None):
271 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
272 self.create_send_note(cr, uid, [obj_id], context=context)
275 def on_change_opt_in(self, cr, uid, ids, opt_in):
276 return {'value':{'opt_in':opt_in,'opt_out':False}}
278 def on_change_opt_out(self, cr, uid, ids, opt_out):
279 return {'value':{'opt_out':opt_out,'opt_in':False}}
281 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
284 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
285 if not stage.on_change:
287 return {'value':{'probability': stage.probability}}
289 def _check(self, cr, uid, ids=False, context=None):
290 """ Override of the base.stage method.
291 Function called by the scheduler to process cases for date actions
292 Only works on not done and cancelled cases
294 cr.execute('select * from crm_case \
295 where (date_action_last<%s or date_action_last is null) \
296 and (date_action_next<=%s or date_action_next is null) \
297 and state not in (\'cancel\',\'done\')',
298 (time.strftime("%Y-%m-%d %H:%M:%S"),
299 time.strftime('%Y-%m-%d %H:%M:%S')))
301 ids2 = map(lambda x: x[0], cr.fetchall() or [])
302 cases = self.browse(cr, uid, ids2, context=context)
303 return self._action(cr, uid, cases, False, context=context)
305 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
306 """ Override of the base.stage method
307 Parameter of the stage search taken from the lead:
308 - type: stage type must be the same or 'both'
309 - section_id: if set, stages must belong to this section or
310 be a default stage; if not set, stages must be default
313 if isinstance(cases, (int, long)):
314 cases = self.browse(cr, uid, cases, context=context)
315 # collect all section_ids
319 section_ids.append(section_id)
322 section_ids.append(lead.section_id.id)
323 if lead.type not in types:
324 types.append(lead.type)
325 # OR all section_ids and OR with case_default
328 search_domain += [('|')] * len(section_ids)
329 for section_id in section_ids:
330 search_domain.append(('section_ids', '=', section_id))
331 search_domain.append(('case_default', '=', True))
332 # AND with cases types
333 search_domain.append(('type', 'in', types))
334 # AND with the domain in parameter
335 search_domain += list(domain)
336 # perform search, return the first found
337 stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
342 def case_cancel(self, cr, uid, ids, context=None):
343 """ Overrides case_cancel from base_stage to set probability """
344 res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
345 self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
348 def case_reset(self, cr, uid, ids, context=None):
349 """ Overrides case_reset from base_stage to set probability """
350 res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
351 self.write(cr, uid, ids, {'probability': 0.0}, context=context)
354 def case_mark_lost(self, cr, uid, ids, context=None):
355 """ Mark the case as lost: state=cancel and probability=0 """
356 for lead in self.browse(cr, uid, ids):
357 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
359 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
360 self.case_mark_lost_send_note(cr, uid, ids, context=context)
363 def case_mark_won(self, cr, uid, ids, context=None):
364 """ Mark the case as lost: state=done and probability=100 """
365 for lead in self.browse(cr, uid, ids):
366 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
368 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
369 self.case_mark_won_send_note(cr, uid, ids, context=context)
372 def set_priority(self, cr, uid, ids, priority):
373 """ Set lead priority
375 return self.write(cr, uid, ids, {'priority' : priority})
377 def set_high_priority(self, cr, uid, ids, context=None):
378 """ Set lead priority to high
380 return self.set_priority(cr, uid, ids, '1')
382 def set_normal_priority(self, cr, uid, ids, context=None):
383 """ Set lead priority to normal
385 return self.set_priority(cr, uid, ids, '3')
387 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
388 # prepare opportunity data into dictionary for merging
389 opportunities = self.browse(cr, uid, ids, context=context)
390 def _get_first_not_null(attr):
391 if hasattr(oldest, attr):
392 return getattr(oldest, attr)
393 for opportunity in opportunities:
394 if hasattr(opportunity, attr):
395 return getattr(opportunity, attr)
398 def _get_first_not_null_id(attr):
399 res = _get_first_not_null(attr)
400 return res and res.id or False
402 def _concat_all(attr):
403 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
406 for field_name in fields:
407 field_info = self._all_columns.get(field_name)
408 if field_info is None:
410 field = field_info.column
411 if field._type in ('many2many', 'one2many'):
413 elif field._type == 'many2one':
414 data[field_name] = _get_first_not_null_id(field_name) # !!
415 elif field._type == 'text':
416 data[field_name] = _concat_all(field_name) #not lost
418 data[field_name] = _get_first_not_null(field_name) #not lost
421 def _merge_find_oldest(self, cr, uid, ids, context=None):
424 #TOCHECK: where pass 'convert' in context ?
425 if context.get('convert'):
426 ids = list(set(ids) - set(context.get('lead_ids', False)) )
428 #search opportunities order by create date
429 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
430 oldest_id = opportunity_ids[0]
431 return self.browse(cr, uid, oldest_id, context=context)
433 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
436 body.append("%s\n" % (title))
437 for field_name in fields:
438 field_info = self._all_columns.get(field_name)
439 if field_info is None:
441 field = field_info.column
444 if field._type == 'selection':
445 if hasattr(field.selection, '__call__'):
446 key = field.selection(self, cr, uid, context=context)
448 key = field.selection
449 value = dict(key).get(lead[field_name], lead[field_name])
450 elif field._type == 'many2one':
452 value = lead[field_name].name_get()[0][1]
454 value = lead[field_name]
456 body.append("%s: %s" % (field.string, value or ''))
457 return "\n".join(body + ['---'])
459 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
460 #TOFIX: mail template should be used instead of fix body, subject text
462 merge_message = _('Merged opportunities')
463 subject = [merge_message]
464 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
465 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
466 'country_id', 'city', 'street', 'street2', 'zip']
467 for opportunity in opportunities:
468 subject.append(opportunity.name)
469 title = "%s : %s" % (merge_message, opportunity.name)
470 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
472 subject = subject[0] + ", ".join(subject[1:])
473 details = "\n\n".join(details)
474 return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
476 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
477 message = self.pool.get('mail.message')
478 for opportunity in opportunities:
479 for history in opportunity.message_ids:
480 message.write(cr, uid, history.id, {
481 'res_id': opportunity_id,
482 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
487 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
488 attachment = self.pool.get('ir.attachment')
490 # return attachments of opportunity
491 def _get_attachments(opportunity_id):
492 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
493 return attachment.browse(cr, uid, attachment_ids, context=context)
496 first_attachments = _get_attachments(opportunity_id)
497 for opportunity in opportunities:
498 attachments = _get_attachments(opportunity.id)
499 for first in first_attachments:
500 for attachment in attachments:
501 if attachment.name == first.name:
503 name = "%s (%s)" % (attachment.name, count,),
504 res_id = opportunity_id,
506 attachment.write(values)
511 def merge_opportunity(self, cr, uid, ids, context=None):
513 To merge opportunities
514 :param ids: list of opportunities ids to merge
516 if context is None: context = {}
518 #TOCHECK: where pass lead_ids in context?
519 lead_ids = context and context.get('lead_ids', []) or []
522 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
524 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
525 opportunities = self.browse(cr, uid, ids, context=context)
526 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
527 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
528 if ctx_opportunities :
529 first_opportunity = ctx_opportunities[0]
530 tail_opportunities = opportunities_list
532 first_opportunity = opportunities_list[0]
533 tail_opportunities = opportunities_list[1:]
535 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',
536 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
537 'date_action_next', 'email_from', 'email_cc', 'partner_name']
539 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
541 # merge data into first opportunity
542 self.write(cr, uid, [first_opportunity.id], data, context=context)
544 #copy message and attachements into the first opportunity
545 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
546 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
548 #Notification about loss of information
549 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
550 #delete tail opportunities
551 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
553 #open first opportunity
554 self.case_open(cr, uid, [first_opportunity.id])
555 return first_opportunity.id
557 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
558 crm_stage = self.pool.get('crm.case.stage')
561 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
563 section_id = lead.section_id and lead.section_id.id or False
565 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
567 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
568 stage_id = stage_ids and stage_ids[0] or False
570 'planned_revenue': lead.planned_revenue,
571 'probability': lead.probability,
573 'partner_id': customer and customer.id or False,
574 'user_id': (lead.user_id and lead.user_id.id),
575 'type': 'opportunity',
576 'stage_id': stage_id or False,
577 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
578 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
581 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
582 partner = self.pool.get('res.partner')
583 mail_message = self.pool.get('mail.message')
586 customer = partner.browse(cr, uid, partner_id, context=context)
587 for lead in self.browse(cr, uid, ids, context=context):
588 if lead.state in ('done', 'cancel'):
590 if user_ids or section_id:
591 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
593 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
594 self.write(cr, uid, [lead.id], vals, context=context)
596 self.convert_opportunity_send_note(cr, uid, lead, context=context)
597 #TOCHECK: why need to change partner details in all messages of lead ?
599 msg_ids = [ x.id for x in lead.message_ids]
600 mail_message.write(cr, uid, msg_ids, {
601 'partner_id': lead.partner_id.id
605 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
606 partner = self.pool.get('res.partner')
607 vals = { 'name': name,
608 'user_id': lead.user_id.id,
609 'comment': lead.description,
610 'section_id': lead.section_id.id or False,
611 'parent_id': parent_id,
613 'mobile': lead.mobile,
614 'email': lead.email_from and to_email(lead.email_from)[0],
616 'title': lead.title and lead.title.id or False,
617 'function': lead.function,
618 'street': lead.street,
619 'street2': lead.street2,
622 'country_id': lead.country_id and lead.country_id.id or False,
623 'state_id': lead.state_id and lead.state_id.id or False,
624 'is_company': is_company,
627 partner = partner.create(cr, uid,vals, context)
630 def _create_lead_partner(self, cr, uid, lead, context=None):
632 if lead.partner_name and lead.contact_name:
633 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
634 self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
635 elif lead.partner_name and not lead.contact_name:
636 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
637 elif not lead.partner_name and lead.contact_name:
638 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
640 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
643 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
645 res_partner = self.pool.get('res.partner')
647 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
648 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
649 res = lead.write({'partner_id' : partner_id, }, context=context)
650 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
653 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
655 This function convert partner based on action.
656 if action is 'create', create new partner with contact and assign lead to new partner_id.
657 otherwise assign lead to specified partner_id
662 for lead in self.browse(cr, uid, ids, context=context):
663 if action == 'create':
665 partner_id = self._create_lead_partner(cr, uid, lead, context)
666 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
667 partner_ids[lead.id] = partner_id
670 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
672 Send mail to salesman with updated Lead details.
673 @ lead: browse record of 'crm.lead' object.
675 #TOFIX: mail template should be used here instead of fix subject, body text.
676 message = self.pool.get('mail.message')
677 email_to = lead.user_id and lead.user_id.user_email
681 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
682 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
683 subject = "lead %s converted into opportunity" % lead.name
684 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
685 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
688 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
693 value['section_id'] = team_id
694 if index < len(user_ids):
695 value['user_id'] = user_ids[index]
698 self.write(cr, uid, [lead_id], value, context=context)
701 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):
703 action :('schedule','Schedule a call'), ('log','Log a call')
705 phonecall = self.pool.get('crm.phonecall')
706 model_data = self.pool.get('ir.model.data')
709 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
711 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
712 for lead in self.browse(cr, uid, ids, context=context):
714 section_id = lead.section_id and lead.section_id.id or False
716 user_id = lead.user_id and lead.user_id.id or False
718 'name' : call_summary,
719 'opportunity_id' : lead.id,
720 'user_id' : user_id or False,
721 'categ_id' : categ_id or False,
722 'description' : desc or '',
723 'date' : schedule_time,
724 'section_id' : section_id or False,
725 'partner_id': lead.partner_id and lead.partner_id.id or False,
726 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
727 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
728 'priority': lead.priority,
730 new_id = phonecall.create(cr, uid, vals, context=context)
731 phonecall.case_open(cr, uid, [new_id], context=context)
733 phonecall.case_close(cr, uid, [new_id], context=context)
734 phonecall_dict[lead.id] = new_id
735 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
736 return phonecall_dict
739 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
740 models_data = self.pool.get('ir.model.data')
742 # Get Opportunity views
743 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
744 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
746 'name': _('Opportunity'),
748 'view_mode': 'tree, form',
749 'res_model': 'crm.lead',
750 'domain': [('type', '=', 'opportunity')],
751 'res_id': int(opportunity_id),
753 'views': [(form_view and form_view[1] or False, 'form'),
754 (tree_view and tree_view[1] or False, 'tree'),
755 (False, 'calendar'), (False, 'graph')],
756 'type': 'ir.actions.act_window',
759 def action_makeMeeting(self, cr, uid, ids, context=None):
761 This opens Meeting's calendar view to schedule meeting on current Opportunity
762 @return : Dictionary value for created Meeting view
767 data_obj = self.pool.get('ir.model.data')
768 for opp in self.browse(cr, uid, ids, context=context):
770 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
771 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
772 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
773 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
775 'default_opportunity_id': opp.id,
776 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
777 'default_user_id': uid,
778 'default_section_id': opp.section_id and opp.section_id.id or False,
779 'default_email_from': opp.email_from,
780 'default_state': 'open',
781 'default_name': opp.name
784 'name': _('Meetings'),
787 'view_mode': 'calendar,form,tree',
788 'res_model': 'crm.meeting',
790 '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')],
791 'type': 'ir.actions.act_window',
792 'search_view_id': search_view and search_view[1] or False,
798 def unlink(self, cr, uid, ids, context=None):
799 for lead in self.browse(cr, uid, ids, context):
800 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
801 raise osv.except_osv(_('Error'),
802 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
803 "You should better cancel it, instead of deleting it.") % lead.name)
804 return super(crm_lead, self).unlink(cr, uid, ids, context)
806 def write(self, cr, uid, ids, vals, context=None):
807 if vals.get('stage_id') and not vals.get('probability'):
808 # change probability of lead(s) if required by stage
809 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
811 vals['probability'] = stage.probability
812 return super(crm_lead,self).write(cr, uid, ids, vals, context)
814 # ----------------------------------------
816 # ----------------------------------------
818 def message_new(self, cr, uid, msg, custom_values=None, context=None):
819 """ mail_thread message_new that is called by the mailgateway:
820 - create a new record of the related model
821 - add the message to the thread
822 This override also updates the lead according to the email.
824 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
826 name = msg.get('subject') or _("No Subject")
827 description = msg.get('body')
828 email_from = msg.get('from')
829 email_cc = msg.get('cc')
832 'email_from': email_from,
833 'email_cc': email_cc,
834 'description': description,
837 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
838 vals['priority'] = msg.get('priority')
839 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False), context=context))
840 self.write(cr, uid, [res_id], vals, context=context)
843 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
844 """ mail_thread message_update that is called by the mailgateway:
845 - add the message to the thread
846 This override also updates the lead according to the email.
848 if isinstance(ids, (str, int, long)):
852 update_res = super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
854 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
855 vals['priority'] = msg.get('priority')
857 'cost':'planned_cost',
858 'revenue': 'planned_revenue',
859 'probability':'probability'
861 for line in msg['body_text'].split('\n'):
863 res = tools.misc.command_re.match(line)
864 if res and maps.get(res.group(1).lower()):
865 key = maps.get(res.group(1).lower())
866 vals[key] = res.group(2).lower()
867 self.write(cr, uid, ids, vals, context=context)
870 # ----------------------------------------
871 # OpenChatter methods and notifications
872 # ----------------------------------------
874 def message_get_subscribers(self, cr, uid, ids, context=None):
875 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
876 # add salesman to the subscribers
877 for obj in self.browse(cr, uid, ids, context=context):
879 sub_ids.append(obj.user_id.id)
880 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
882 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
883 """ Override of the (void) default notification method. """
884 stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
885 return self.message_append_note(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
887 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
888 if isinstance(lead, (int, long)):
889 lead = self.browse(cr, uid, [lead], context=context)[0]
890 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
892 def create_send_note(self, cr, uid, ids, context=None):
894 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
895 self.message_append_note(cr, uid, [id], body=message, context=context)
898 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
899 message = _("Opportunity has been <b>lost</b>.")
900 return self.message_append_note(cr, uid, ids, body=message, context=context)
902 def case_mark_won_send_note(self, cr, uid, ids, context=None):
903 message = _("Opportunity has been <b>won</b>.")
904 return self.message_append_note(cr, uid, ids, body=message, context=context)
906 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
907 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
908 if action == 'log': prefix = 'Logged'
909 else: prefix = 'Scheduled'
910 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
911 return self. message_append_note(cr, uid, ids, body=message, context=context)
913 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
914 for lead in self.browse(cr, uid, ids, context=context):
915 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))
916 lead.message_append_note(body=message)
919 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
920 message = _("Lead has been <b>converted to an opportunity</b>.")
921 lead.message_append_note(body=message)
926 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: