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 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
232 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
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),
254 'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
255 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
256 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
257 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
258 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
259 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
263 def get_needaction_user_ids(self, cr, uid, ids, context=None):
264 result = dict.fromkeys(ids, [])
265 for obj in self.browse(cr, uid, ids, context=context):
266 # salesman must perform an action when in draft mode
267 if obj.state == 'draft' and obj.user_id:
268 result[obj.id] = [obj.user_id.id]
271 def create(self, cr, uid, vals, context=None):
272 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
273 self.create_send_note(cr, uid, [obj_id], context=context)
276 def on_change_opt_in(self, cr, uid, ids, opt_in):
277 return {'value':{'opt_in':opt_in,'opt_out':False}}
279 def on_change_opt_out(self, cr, uid, ids, opt_out):
280 return {'value':{'opt_out':opt_out,'opt_in':False}}
282 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
285 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
286 if not stage.on_change:
288 return {'value':{'probability': stage.probability}}
290 def _check(self, cr, uid, ids=False, context=None):
291 """ Override of the base.stage method.
292 Function called by the scheduler to process cases for date actions
293 Only works on not done and cancelled cases
295 cr.execute('select * from crm_case \
296 where (date_action_last<%s or date_action_last is null) \
297 and (date_action_next<=%s or date_action_next is null) \
298 and state not in (\'cancel\',\'done\')',
299 (time.strftime("%Y-%m-%d %H:%M:%S"),
300 time.strftime('%Y-%m-%d %H:%M:%S')))
302 ids2 = map(lambda x: x[0], cr.fetchall() or [])
303 cases = self.browse(cr, uid, ids2, context=context)
304 return self._action(cr, uid, cases, False, context=context)
306 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
307 """ Override of the base.stage method
308 Parameter of the stage search taken from the lead:
309 - type: stage type must be the same or 'both'
310 - section_id: if set, stages must belong to this section or
311 be a default stage; if not set, stages must be default
314 if isinstance(cases, (int, long)):
315 cases = self.browse(cr, uid, cases, context=context)
316 domain = list(domain)
318 domain += ['|', ('section_ids', '=', section_id)]
319 domain.append(('case_default', '=', True))
321 domain += ['|', ('type', '=', lead.type), ('type', '=', 'both')]
322 lead_section_id = lead.section_id.id if lead.section_id else None
324 domain += ['|', ('section_ids', '=', lead_section_id), ('case_default', '=', True)]
325 stage_ids = self.pool.get('crm.case.stage').search(cr, uid, domain, order=order, context=context)
330 def case_cancel(self, cr, uid, ids, context=None):
331 """ Overrides case_cancel from base_stage to set probability """
332 res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
333 self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
336 def case_reset(self, cr, uid, ids, context=None):
337 """ Overrides case_reset from base_stage to set probability """
338 res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
339 self.write(cr, uid, ids, {'probability': 0.0}, context=context)
342 def case_mark_lost(self, cr, uid, ids, context=None):
343 """ Mark the case as lost: state=cancel and probability=0 """
344 for lead in self.browse(cr, uid, ids):
345 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
347 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
348 self.case_mark_lost_send_note(cr, uid, ids, context=context)
351 def case_mark_won(self, cr, uid, ids, context=None):
352 """ Mark the case as lost: state=done and probability=100 """
353 for lead in self.browse(cr, uid, ids):
354 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
356 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
357 self.case_mark_won_send_note(cr, uid, ids, context=context)
360 def set_priority(self, cr, uid, ids, priority):
361 """ Set lead priority
363 return self.write(cr, uid, ids, {'priority' : priority})
365 def set_high_priority(self, cr, uid, ids, context=None):
366 """ Set lead priority to high
368 return self.set_priority(cr, uid, ids, '1')
370 def set_normal_priority(self, cr, uid, ids, context=None):
371 """ Set lead priority to normal
373 return self.set_priority(cr, uid, ids, '3')
375 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
376 # prepare opportunity data into dictionary for merging
377 opportunities = self.browse(cr, uid, ids, context=context)
378 def _get_first_not_null(attr):
379 if hasattr(oldest, attr):
380 return getattr(oldest, attr)
381 for opportunity in opportunities:
382 if hasattr(opportunity, attr):
383 return getattr(opportunity, attr)
386 def _get_first_not_null_id(attr):
387 res = _get_first_not_null(attr)
388 return res and res.id or False
390 def _concat_all(attr):
391 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
394 for field_name in fields:
395 field_info = self._all_columns.get(field_name)
396 if field_info is None:
398 field = field_info.column
399 if field._type in ('many2many', 'one2many'):
401 elif field._type == 'many2one':
402 data[field_name] = _get_first_not_null_id(field_name) # !!
403 elif field._type == 'text':
404 data[field_name] = _concat_all(field_name) #not lost
406 data[field_name] = _get_first_not_null(field_name) #not lost
409 def _merge_find_oldest(self, cr, uid, ids, context=None):
412 #TOCHECK: where pass 'convert' in context ?
413 if context.get('convert'):
414 ids = list(set(ids) - set(context.get('lead_ids', False)) )
416 #search opportunities order by create date
417 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
418 oldest_id = opportunity_ids[0]
419 return self.browse(cr, uid, oldest_id, context=context)
421 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
424 body.append("%s\n" % (title))
425 for field_name in fields:
426 field_info = self._all_columns.get(field_name)
427 if field_info is None:
429 field = field_info.column
432 if field._type == 'selection':
433 if hasattr(field.selection, '__call__'):
434 key = field.selection(self, cr, uid, context=context)
436 key = field.selection
437 value = dict(key).get(lead[field_name], lead[field_name])
438 elif field._type == 'many2one':
440 value = lead[field_name].name_get()[0][1]
442 value = lead[field_name]
444 body.append("%s: %s" % (field.string, value or ''))
445 return "\n".join(body + ['---'])
447 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
448 #TOFIX: mail template should be used instead of fix body, subject text
450 merge_message = _('Merged opportunities')
451 subject = [merge_message]
452 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
453 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
454 'country_id', 'city', 'street', 'street2', 'zip']
455 for opportunity in opportunities:
456 subject.append(opportunity.name)
457 title = "%s : %s" % (merge_message, opportunity.name)
458 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
460 subject = subject[0] + ", ".join(subject[1:])
461 details = "\n\n".join(details)
462 return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
464 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
465 message = self.pool.get('mail.message')
466 for opportunity in opportunities:
467 for history in opportunity.message_ids:
468 message.write(cr, uid, history.id, {
469 'res_id': opportunity_id,
470 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
475 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
476 attachment = self.pool.get('ir.attachment')
478 # return attachments of opportunity
479 def _get_attachments(opportunity_id):
480 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
481 return attachment.browse(cr, uid, attachment_ids, context=context)
484 first_attachments = _get_attachments(opportunity_id)
485 for opportunity in opportunities:
486 attachments = _get_attachments(opportunity.id)
487 for first in first_attachments:
488 for attachment in attachments:
489 if attachment.name == first.name:
491 name = "%s (%s)" % (attachment.name, count,),
492 res_id = opportunity_id,
494 attachment.write(values)
499 def merge_opportunity(self, cr, uid, ids, context=None):
501 To merge opportunities
502 :param ids: list of opportunities ids to merge
504 if context is None: context = {}
506 #TOCHECK: where pass lead_ids in context?
507 lead_ids = context and context.get('lead_ids', []) or []
510 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
512 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
513 opportunities = self.browse(cr, uid, ids, context=context)
514 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
515 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
516 if ctx_opportunities :
517 first_opportunity = ctx_opportunities[0]
518 tail_opportunities = opportunities_list
520 first_opportunity = opportunities_list[0]
521 tail_opportunities = opportunities_list[1:]
523 fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
524 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
525 'date_action_next', 'email_from', 'email_cc', 'partner_name']
527 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
529 # merge data into first opportunity
530 self.write(cr, uid, [first_opportunity.id], data, context=context)
532 #copy message and attachements into the first opportunity
533 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
534 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
536 #Notification about loss of information
537 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
538 #delete tail opportunities
539 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
541 #open first opportunity
542 self.case_open(cr, uid, [first_opportunity.id])
543 return first_opportunity.id
545 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
546 crm_stage = self.pool.get('crm.case.stage')
549 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
551 section_id = lead.section_id and lead.section_id.id or False
553 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
555 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
556 stage_id = stage_ids and stage_ids[0] or False
558 'planned_revenue': lead.planned_revenue,
559 'probability': lead.probability,
561 'partner_id': customer and customer.id or False,
562 'user_id': (lead.user_id and lead.user_id.id),
563 'type': 'opportunity',
564 'stage_id': stage_id or False,
565 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
566 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
569 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
570 partner = self.pool.get('res.partner')
571 mail_message = self.pool.get('mail.message')
574 customer = partner.browse(cr, uid, partner_id, context=context)
575 for lead in self.browse(cr, uid, ids, context=context):
576 if lead.state in ('done', 'cancel'):
578 if user_ids or section_id:
579 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
581 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
582 self.write(cr, uid, [lead.id], vals, context=context)
584 self.convert_opportunity_send_note(cr, uid, lead, context=context)
585 #TOCHECK: why need to change partner details in all messages of lead ?
587 msg_ids = [ x.id for x in lead.message_ids]
588 mail_message.write(cr, uid, msg_ids, {
589 'partner_id': lead.partner_id.id
593 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
594 partner = self.pool.get('res.partner')
595 vals = { 'name': name,
596 'user_id': lead.user_id.id,
597 'comment': lead.description,
598 'section_id': lead.section_id.id or False,
599 'parent_id': parent_id,
601 'mobile': lead.mobile,
602 'email': lead.email_from and to_email(lead.email_from)[0],
604 'title': lead.title and lead.title.id or False,
605 'function': lead.function,
606 'street': lead.street,
607 'street2': lead.street2,
610 'country_id': lead.country_id and lead.country_id.id or False,
611 'state_id': lead.state_id and lead.state_id.id or False,
612 'is_company': is_company,
615 partner = partner.create(cr, uid,vals, context)
618 def _create_lead_partner(self, cr, uid, lead, context=None):
620 if lead.partner_name and lead.contact_name:
621 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
622 self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
623 elif lead.partner_name and not lead.contact_name:
624 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
625 elif not lead.partner_name and lead.contact_name:
626 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
628 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
631 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
633 res_partner = self.pool.get('res.partner')
635 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
636 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
637 res = lead.write({'partner_id' : partner_id, }, context=context)
638 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
641 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
643 This function convert partner based on action.
644 if action is 'create', create new partner with contact and assign lead to new partner_id.
645 otherwise assign lead to specified partner_id
650 for lead in self.browse(cr, uid, ids, context=context):
651 if action == 'create':
653 partner_id = self._create_lead_partner(cr, uid, lead, context)
654 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
655 partner_ids[lead.id] = partner_id
658 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
660 Send mail to salesman with updated Lead details.
661 @ lead: browse record of 'crm.lead' object.
663 #TOFIX: mail template should be used here instead of fix subject, body text.
664 message = self.pool.get('mail.message')
665 email_to = lead.user_id and lead.user_id.user_email
669 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
670 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
671 subject = "lead %s converted into opportunity" % lead.name
672 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
673 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
676 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
681 value['section_id'] = team_id
682 if index < len(user_ids):
683 value['user_id'] = user_ids[index]
686 self.write(cr, uid, [lead_id], value, context=context)
689 def schedule_phonecall(self, cr, uid, ids, schedule_time, call_summary, desc, phone, contact_name, user_id=False, section_id=False, categ_id=False, action='schedule', context=None):
691 action :('schedule','Schedule a call'), ('log','Log a call')
693 phonecall = self.pool.get('crm.phonecall')
694 model_data = self.pool.get('ir.model.data')
697 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
699 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
700 for lead in self.browse(cr, uid, ids, context=context):
702 section_id = lead.section_id and lead.section_id.id or False
704 user_id = lead.user_id and lead.user_id.id or False
706 'name' : call_summary,
707 'opportunity_id' : lead.id,
708 'user_id' : user_id or False,
709 'categ_id' : categ_id or False,
710 'description' : desc or '',
711 'date' : schedule_time,
712 'section_id' : section_id or False,
713 'partner_id': lead.partner_id and lead.partner_id.id or False,
714 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
715 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
716 'priority': lead.priority,
718 new_id = phonecall.create(cr, uid, vals, context=context)
719 phonecall.case_open(cr, uid, [new_id], context=context)
721 phonecall.case_close(cr, uid, [new_id], context=context)
722 phonecall_dict[lead.id] = new_id
723 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
724 return phonecall_dict
727 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
728 models_data = self.pool.get('ir.model.data')
730 # Get Opportunity views
731 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
732 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
734 'name': _('Opportunity'),
736 'view_mode': 'tree, form',
737 'res_model': 'crm.lead',
738 'domain': [('type', '=', 'opportunity')],
739 'res_id': int(opportunity_id),
741 'views': [(form_view and form_view[1] or False, 'form'),
742 (tree_view and tree_view[1] or False, 'tree'),
743 (False, 'calendar'), (False, 'graph')],
744 'type': 'ir.actions.act_window',
748 def message_new(self, cr, uid, msg, custom_values=None, context=None):
749 """Automatically calls when new email message arrives"""
750 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
751 subject = msg.get('subject') or _("No Subject")
752 body = msg.get('body_text')
754 msg_from = msg.get('from')
755 priority = msg.get('priority')
758 'email_from': msg_from,
759 'email_cc': msg.get('cc'),
764 vals['priority'] = priority
765 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
766 self.write(cr, uid, [res_id], vals, context)
769 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
770 if isinstance(ids, (str, int, long)):
774 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
776 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
777 vals['priority'] = msg.get('priority')
779 'cost':'planned_cost',
780 'revenue': 'planned_revenue',
781 'probability':'probability'
784 for line in msg['body_text'].split('\n'):
786 res = tools.misc.command_re.match(line)
787 if res and maps.get(res.group(1).lower()):
788 key = maps.get(res.group(1).lower())
789 vls[key] = res.group(2).lower()
792 # Unfortunately the API is based on lists
793 # but we want to update the state based on the
794 # previous state, so we have to loop:
795 for case in self.browse(cr, uid, ids, context=context):
797 if case.state in CRM_LEAD_PENDING_STATES:
799 values.update(state=crm.AVAILABLE_STATES[1][0])
800 if not case.date_open:
801 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
802 res = self.write(cr, uid, [case.id], values, context=context)
805 def action_makeMeeting(self, cr, uid, ids, context=None):
807 This opens Meeting's calendar view to schedule meeting on current Opportunity
808 @return : Dictionary value for created Meeting view
813 data_obj = self.pool.get('ir.model.data')
814 for opp in self.browse(cr, uid, ids, context=context):
816 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
817 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
818 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
819 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
821 'default_opportunity_id': opp.id,
822 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
823 'default_user_id': uid,
824 'default_section_id': opp.section_id and opp.section_id.id or False,
825 'default_email_from': opp.email_from,
826 'default_state': 'open',
827 'default_name': opp.name
830 'name': _('Meetings'),
833 'view_mode': 'calendar,form,tree',
834 'res_model': 'crm.meeting',
836 'views': [(calander_view and calander_view[1] or False, 'calendar'), (form_view and form_view[1] or False, 'form'), (tree_view and tree_view[1] or False, 'tree')],
837 'type': 'ir.actions.act_window',
838 'search_view_id': search_view and search_view[1] or False,
844 def unlink(self, cr, uid, ids, context=None):
845 for lead in self.browse(cr, uid, ids, context):
846 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
847 raise osv.except_osv(_('Error'),
848 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
849 "You should better cancel it, instead of deleting it.") % lead.name)
850 return super(crm_lead, self).unlink(cr, uid, ids, context)
852 def write(self, cr, uid, ids, vals, context=None):
853 if vals.get('stage_id') and not vals.get('probability'):
854 # change probability of lead(s) if required by stage
855 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
857 vals['probability'] = stage.probability
858 return super(crm_lead,self).write(cr, uid, ids, vals, context)
860 # ----------------------------------------
861 # OpenChatter methods and notifications
862 # ----------------------------------------
864 def message_get_subscribers(self, cr, uid, ids, context=None):
865 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
866 # add salesman to the subscribers
867 for obj in self.browse(cr, uid, ids, context=context):
869 sub_ids.append(obj.user_id.id)
870 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
872 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
873 """ Override of the (void) default notification method. """
874 stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
875 return self.message_append_note(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
877 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
878 if isinstance(lead, (int, long)):
879 lead = self.browse(cr, uid, [lead], context=context)[0]
880 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
882 def create_send_note(self, cr, uid, ids, context=None):
884 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
885 self.message_append_note(cr, uid, [id], body=message, context=context)
888 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
889 message = _("Opportunity has been <b>lost</b>.")
890 return self.message_append_note(cr, uid, ids, body=message, context=context)
892 def case_mark_won_send_note(self, cr, uid, ids, context=None):
893 message = _("Opportunity has been <b>won</b>.")
894 return self.message_append_note(cr, uid, ids, body=message, context=context)
896 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
897 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
898 if action == 'log': prefix = 'Logged'
899 else: prefix = 'Scheduled'
900 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
901 return self. message_append_note(cr, uid, ids, body=message, context=context)
903 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
904 for lead in self.browse(cr, uid, ids, context=context):
905 message = _("%s <b>partner</b> is now set to <em>%s</em>." % (self.case_get_note_msg_prefix(cr, uid, lead, context=context), lead.partner_id.name))
906 lead.message_append_note(body=message)
909 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
910 message = _("Lead has been <b>converted to an opportunity</b>.")
911 lead.message_append_note(body=message)
916 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: