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('Subject', size=64, required=True, 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="Email 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',
760 def message_new(self, cr, uid, msg, custom_values=None, context=None):
761 """Automatically calls when new email message arrives"""
762 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
763 subject = msg.get('subject') or _("No Subject")
764 body = msg.get('body_text')
766 msg_from = msg.get('from')
767 priority = msg.get('priority')
770 'email_from': msg_from,
771 'email_cc': msg.get('cc'),
776 vals['priority'] = priority
777 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
778 self.write(cr, uid, [res_id], vals, context)
781 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
782 if isinstance(ids, (str, int, long)):
786 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
788 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
789 vals['priority'] = msg.get('priority')
791 'cost':'planned_cost',
792 'revenue': 'planned_revenue',
793 'probability':'probability'
796 for line in msg['body_text'].split('\n'):
798 res = tools.misc.command_re.match(line)
799 if res and maps.get(res.group(1).lower()):
800 key = maps.get(res.group(1).lower())
801 vls[key] = res.group(2).lower()
804 # Unfortunately the API is based on lists
805 # but we want to update the state based on the
806 # previous state, so we have to loop:
807 for case in self.browse(cr, uid, ids, context=context):
809 if case.state in CRM_LEAD_PENDING_STATES:
811 values.update(state=crm.AVAILABLE_STATES[1][0])
812 if not case.date_open:
813 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
814 res = self.write(cr, uid, [case.id], values, context=context)
817 def action_makeMeeting(self, cr, uid, ids, context=None):
819 This opens Meeting's calendar view to schedule meeting on current Opportunity
820 @return : Dictionary value for created Meeting view
825 data_obj = self.pool.get('ir.model.data')
826 for opp in self.browse(cr, uid, ids, context=context):
828 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
829 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
830 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
831 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
833 'default_opportunity_id': opp.id,
834 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
835 'default_user_id': uid,
836 'default_section_id': opp.section_id and opp.section_id.id or False,
837 'default_email_from': opp.email_from,
838 'default_state': 'open',
839 'default_name': opp.name
842 'name': _('Meetings'),
845 'view_mode': 'calendar,form,tree',
846 'res_model': 'crm.meeting',
848 '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')],
849 'type': 'ir.actions.act_window',
850 'search_view_id': search_view and search_view[1] or False,
856 def unlink(self, cr, uid, ids, context=None):
857 for lead in self.browse(cr, uid, ids, context):
858 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
859 raise osv.except_osv(_('Error'),
860 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
861 "You should better cancel it, instead of deleting it.") % lead.name)
862 return super(crm_lead, self).unlink(cr, uid, ids, context)
864 def write(self, cr, uid, ids, vals, context=None):
865 if vals.get('stage_id') and not vals.get('probability'):
866 # change probability of lead(s) if required by stage
867 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
869 vals['probability'] = stage.probability
870 return super(crm_lead,self).write(cr, uid, ids, vals, context)
872 # ----------------------------------------
873 # OpenChatter methods and notifications
874 # ----------------------------------------
876 def message_get_subscribers(self, cr, uid, ids, context=None):
877 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
878 # add salesman to the subscribers
879 for obj in self.browse(cr, uid, ids, context=context):
881 sub_ids.append(obj.user_id.id)
882 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
884 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
885 """ Override of the (void) default notification method. """
886 stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
887 return self.message_append_note(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
889 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
890 if isinstance(lead, (int, long)):
891 lead = self.browse(cr, uid, [lead], context=context)[0]
892 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
894 def create_send_note(self, cr, uid, ids, context=None):
896 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
897 self.message_append_note(cr, uid, [id], body=message, context=context)
900 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
901 message = _("Opportunity has been <b>lost</b>.")
902 return self.message_append_note(cr, uid, ids, body=message, context=context)
904 def case_mark_won_send_note(self, cr, uid, ids, context=None):
905 message = _("Opportunity has been <b>won</b>.")
906 return self.message_append_note(cr, uid, ids, body=message, context=context)
908 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
909 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
910 if action == 'log': prefix = 'Logged'
911 else: prefix = 'Scheduled'
912 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
913 return self.message_append_note(cr, uid, ids, body=message, context=context)
915 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
916 for lead in self.browse(cr, uid, ids, context=context):
917 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))
918 lead.message_append_note(body=message)
921 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
922 message = _("Lead has been <b>converted to an opportunity</b>.")
923 lead.message_append_note(body=message)
928 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: