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 type from the context (if set: choose 'type' or 'both')
86 type = self._resolve_type_from_context(cr, uid, context=context)
87 # retrieve section_id from the context and write the domain
89 section_id = self._resolve_section_id_from_context(cr, uid, context=context)
91 search_domain += ['|', '&', ('section_ids', '=', section_id), ('fold', '=', True)]
93 search_domain += ['|', ('type', '=', type), ('type', '=', 'both')]
94 search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', 1), ('fold', '=', False)]
96 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
97 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
98 # restore order of the search
99 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
103 'stage_id': _read_group_stage_ids
106 def _compute_day(self, cr, uid, ids, fields, args, context=None):
108 @param cr: the current row, from the database cursor,
109 @param uid: the current user’s ID for security checks,
110 @param ids: List of Openday’s IDs
111 @return: difference between current date and log date
112 @param context: A standard dictionary for contextual values
114 cal_obj = self.pool.get('resource.calendar')
115 res_obj = self.pool.get('resource.resource')
118 for lead in self.browse(cr, uid, ids, context=context):
123 if field == 'day_open':
125 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
126 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
127 ans = date_open - date_create
128 date_until = lead.date_open
129 elif field == 'day_close':
131 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
132 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
133 date_until = lead.date_closed
134 ans = date_close - date_create
138 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
139 if len(resource_ids):
140 resource_id = resource_ids[0]
142 duration = float(ans.days)
143 if lead.section_id and lead.section_id.resource_calendar_id:
144 duration = float(ans.days) * 24
145 new_dates = cal_obj.interval_get(cr,
147 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
148 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
153 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
154 for in_time, out_time in new_dates:
155 if in_time.date not in no_days:
156 no_days.append(in_time.date)
157 if out_time > date_until:
159 duration = len(no_days)
160 res[lead.id][field] = abs(int(duration))
163 def _history_search(self, cr, uid, obj, name, args, context=None):
165 msg_obj = self.pool.get('mail.message')
166 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
167 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
170 return [('id', 'in', lead_ids)]
172 return [('id', '=', '0')]
174 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
176 for obj in self.browse(cr, uid, ids, context=context):
178 for msg in obj.message_ids:
180 res[obj.id] = msg.subject
185 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
186 select=True, help="Optional linked partner, usually after conversion of the lead"),
188 'id': fields.integer('ID', readonly=True),
189 'name': fields.char('Name', size=64, select=1),
190 'active': fields.boolean('Active', required=False),
191 'date_action_last': fields.datetime('Last Action', readonly=1),
192 'date_action_next': fields.datetime('Next Action', readonly=1),
193 'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
194 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
195 select=True, help='When sending mails, the default email address is taken from the sales team.'),
196 'create_date': fields.datetime('Creation Date' , readonly=True),
197 'email_cc': fields.text('Global CC', size=252 , help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
198 'description': fields.text('Notes'),
199 'write_date': fields.datetime('Update Date' , readonly=True),
200 'categ_id': fields.many2one('crm.case.categ', 'Category', \
201 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
202 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
203 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
204 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
205 'contact_name': fields.char('Contact Name', size=64),
206 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner company that will be created while converting the lead into opportunity', select=1),
207 'opt_in': fields.boolean('Opt-In', oldname='optin', help="If opt-in is checked, this contact has accepted to receive emails."),
208 'opt_out': fields.boolean('Opt-Out', oldname='optout', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
209 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
210 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
211 'date_closed': fields.datetime('Closed', readonly=True),
212 'stage_id': fields.many2one('crm.case.stage', 'Stage',
213 domain="['&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
214 'user_id': fields.many2one('res.users', 'Salesperson'),
215 'referred': fields.char('Referred By', size=64),
216 'date_open': fields.datetime('Opened', readonly=True),
217 'day_open': fields.function(_compute_day, string='Days to Open', \
218 multi='day_open', type="float", store=True),
219 'day_close': fields.function(_compute_day, string='Days to Close', \
220 multi='day_close', type="float", store=True),
221 'state': fields.related('stage_id', 'state', type="selection", store=True,
222 selection=crm.AVAILABLE_STATES, string="State", readonly=True,
223 help='The state is set to \'Draft\', when a case is created.\
224 If the case is in progress the state is set to \'Open\'.\
225 When the case is over, the state is set to \'Done\'.\
226 If the case needs to be reviewed then the state is \
227 set to \'Pending\'.'),
228 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
229 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
231 # Only used for type opportunity
232 'probability': fields.float('Success Rate (%)',group_operator="avg"),
233 'planned_revenue': fields.float('Expected Revenue'),
234 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
235 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
236 'phone': fields.char("Phone", size=64),
237 'date_deadline': fields.date('Expected Closing'),
238 'date_action': fields.date('Next Action Date', select=True),
239 'title_action': fields.char('Next Action', size=64),
240 'color': fields.integer('Color Index'),
241 'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
242 'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
243 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
244 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
245 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
251 'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
252 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
253 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
254 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
255 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
256 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
260 def get_needaction_user_ids(self, cr, uid, ids, context=None):
261 result = dict.fromkeys(ids, [])
262 for obj in self.browse(cr, uid, ids, context=context):
263 # salesman must perform an action when in draft mode
264 if obj.state == 'draft' and obj.user_id:
265 result[obj.id] = [obj.user_id.id]
268 def create(self, cr, uid, vals, context=None):
269 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
270 self.create_send_note(cr, uid, [obj_id], context=context)
273 def on_change_opt_in(self, cr, uid, ids, opt_in):
274 return {'value':{'opt_in':opt_in,'opt_out':False}}
276 def on_change_opt_out(self, cr, uid, ids, opt_out):
277 return {'value':{'opt_out':opt_out,'opt_in':False}}
279 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
282 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
283 if not stage.on_change:
285 return {'value':{'probability': stage.probability}}
287 def _check(self, cr, uid, ids=False, context=None):
288 """ Override of the base.stage method.
289 Function called by the scheduler to process cases for date actions
290 Only works on not done and cancelled cases
292 cr.execute('select * from crm_case \
293 where (date_action_last<%s or date_action_last is null) \
294 and (date_action_next<=%s or date_action_next is null) \
295 and state not in (\'cancel\',\'done\')',
296 (time.strftime("%Y-%m-%d %H:%M:%S"),
297 time.strftime('%Y-%m-%d %H:%M:%S')))
299 ids2 = map(lambda x: x[0], cr.fetchall() or [])
300 cases = self.browse(cr, uid, ids2, context=context)
301 return self._action(cr, uid, cases, False, context=context)
303 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
304 """ Override of the base.stage method
305 Parameter of the stage search taken from the lead:
306 - type: stage type must be the same or 'both'
307 - section_id: if set, stages must belong to this section or
308 be a default stage; if not set, stages must be default
311 if isinstance(cases, (int, long)):
312 cases = self.browse(cr, uid, cases, context=context)
313 domain = list(domain)
315 domain += ['|', ('section_ids', '=', section_id)]
316 domain.append(('case_default', '=', True))
318 domain += ['|', ('type', '=', lead.type), ('type', '=', 'both')]
319 lead_section_id = lead.section_id.id if lead.section_id else None
321 domain += ['|', ('section_ids', '=', lead_section_id), ('case_default', '=', True)]
322 stage_ids = self.pool.get('crm.case.stage').search(cr, uid, domain, order=order, context=context)
327 def case_cancel(self, cr, uid, ids, context=None):
328 """ Overrides case_cancel from base_stage to set probability """
329 res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
330 self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
333 def case_reset(self, cr, uid, ids, context=None):
334 """ Overrides case_reset from base_stage to set probability """
335 res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
336 self.write(cr, uid, ids, {'probability': 0.0}, context=context)
339 def case_mark_lost(self, cr, uid, ids, context=None):
340 """ Mark the case as lost: state=cancel and probability=0 """
341 for lead in self.browse(cr, uid, ids):
342 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
344 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
345 self.case_mark_lost_send_note(cr, uid, ids, context=context)
348 def case_mark_won(self, cr, uid, ids, context=None):
349 """ Mark the case as lost: state=done and probability=100 """
350 for lead in self.browse(cr, uid, ids):
351 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
353 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
354 self.case_mark_won_send_note(cr, uid, ids, context=context)
357 def set_priority(self, cr, uid, ids, priority):
360 return self.write(cr, uid, ids, {'priority' : priority})
362 def set_high_priority(self, cr, uid, ids, context=None):
363 """Set lead priority to high
365 return self.set_priority(cr, uid, ids, '1')
367 def set_normal_priority(self, cr, uid, ids, context=None):
368 """Set lead priority to normal
370 return self.set_priority(cr, uid, ids, '3')
373 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
374 # prepare opportunity data into dictionary for merging
375 opportunities = self.browse(cr, uid, ids, context=context)
376 def _get_first_not_null(attr):
377 if hasattr(oldest, attr):
378 return getattr(oldest, attr)
379 for opportunity in opportunities:
380 if hasattr(opportunity, attr):
381 return getattr(opportunity, attr)
384 def _get_first_not_null_id(attr):
385 res = _get_first_not_null(attr)
386 return res and res.id or False
388 def _concat_all(attr):
389 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
392 for field_name in fields:
393 field_info = self._all_columns.get(field_name)
394 if field_info is None:
396 field = field_info.column
397 if field._type in ('many2many', 'one2many'):
399 elif field._type == 'many2one':
400 data[field_name] = _get_first_not_null_id(field_name) # !!
401 elif field._type == 'text':
402 data[field_name] = _concat_all(field_name) #not lost
404 data[field_name] = _get_first_not_null(field_name) #not lost
407 def _merge_find_oldest(self, cr, uid, ids, context=None):
410 #TOCHECK: where pass 'convert' in context ?
411 if context.get('convert'):
412 ids = list(set(ids) - set(context.get('lead_ids', False)) )
414 #search opportunities order by create date
415 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
416 oldest_id = opportunity_ids[0]
417 return self.browse(cr, uid, oldest_id, context=context)
419 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
422 body.append("%s\n" % (title))
423 for field_name in fields:
424 field_info = self._all_columns.get(field_name)
425 if field_info is None:
427 field = field_info.column
430 if field._type == 'selection':
431 if hasattr(field.selection, '__call__'):
432 key = field.selection(self, cr, uid, context=context)
434 key = field.selection
435 value = dict(key).get(lead[field_name], lead[field_name])
436 elif field._type == 'many2one':
438 value = lead[field_name].name_get()[0][1]
440 value = lead[field_name]
442 body.append("%s: %s" % (field.string, value or ''))
443 return "\n".join(body + ['---'])
445 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
446 #TOFIX: mail template should be used instead of fix body, subject text
448 merge_message = _('Merged opportunities')
449 subject = [merge_message]
450 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
451 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
452 'country_id', 'city', 'street', 'street2', 'zip']
453 for opportunity in opportunities:
454 subject.append(opportunity.name)
455 title = "%s : %s" % (merge_message, opportunity.name)
456 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
458 subject = subject[0] + ", ".join(subject[1:])
459 details = "\n\n".join(details)
460 return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
462 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
463 message = self.pool.get('mail.message')
464 for opportunity in opportunities:
465 for history in opportunity.message_ids:
466 message.write(cr, uid, history.id, {
467 'res_id': opportunity_id,
468 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
473 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
474 attachment = self.pool.get('ir.attachment')
476 # return attachments of opportunity
477 def _get_attachments(opportunity_id):
478 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
479 return attachment.browse(cr, uid, attachment_ids, context=context)
482 first_attachments = _get_attachments(opportunity_id)
483 for opportunity in opportunities:
484 attachments = _get_attachments(opportunity.id)
485 for first in first_attachments:
486 for attachment in attachments:
487 if attachment.name == first.name:
489 name = "%s (%s)" % (attachment.name, count,),
490 res_id = opportunity_id,
492 attachment.write(values)
497 def merge_opportunity(self, cr, uid, ids, context=None):
499 To merge opportunities
500 :param ids: list of opportunities ids to merge
502 if context is None: context = {}
504 #TOCHECK: where pass lead_ids in context?
505 lead_ids = context and context.get('lead_ids', []) or []
508 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
510 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
511 opportunities = self.browse(cr, uid, ids, context=context)
512 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
513 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
514 if ctx_opportunities :
515 first_opportunity = ctx_opportunities[0]
516 tail_opportunities = opportunities_list
518 first_opportunity = opportunities_list[0]
519 tail_opportunities = opportunities_list[1:]
521 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',
522 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
523 'date_action_next', 'email_from', 'email_cc', 'partner_name']
525 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
527 # merge data into first opportunity
528 self.write(cr, uid, [first_opportunity.id], data, context=context)
530 #copy message and attachements into the first opportunity
531 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
532 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
534 #Notification about loss of information
535 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
536 #delete tail opportunities
537 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
539 #open first opportunity
540 self.case_open(cr, uid, [first_opportunity.id])
541 return first_opportunity.id
543 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
544 crm_stage = self.pool.get('crm.case.stage')
547 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
549 section_id = lead.section_id and lead.section_id.id or False
551 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
553 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
554 stage_id = stage_ids and stage_ids[0] or False
556 'planned_revenue': lead.planned_revenue,
557 'probability': lead.probability,
559 'partner_id': customer and customer.id or False,
560 'user_id': (lead.user_id and lead.user_id.id),
561 'type': 'opportunity',
562 'stage_id': stage_id or False,
563 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
564 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
567 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
568 partner = self.pool.get('res.partner')
569 mail_message = self.pool.get('mail.message')
572 customer = partner.browse(cr, uid, partner_id, context=context)
573 for lead in self.browse(cr, uid, ids, context=context):
574 if lead.state in ('done', 'cancel'):
576 if user_ids or section_id:
577 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
579 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
580 self.write(cr, uid, [lead.id], vals, context=context)
582 self.convert_opportunity_send_note(cr, uid, lead, context=context)
583 #TOCHECK: why need to change partner details in all messages of lead ?
585 msg_ids = [ x.id for x in lead.message_ids]
586 mail_message.write(cr, uid, msg_ids, {
587 'partner_id': lead.partner_id.id
591 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
592 partner = self.pool.get('res.partner')
593 vals = { 'name': name,
594 'user_id': lead.user_id.id,
595 'comment': lead.description,
596 'section_id': lead.section_id.id or False,
597 'parent_id': parent_id,
599 'mobile': lead.mobile,
600 'email': lead.email_from and to_email(lead.email_from)[0],
602 'title': lead.title and lead.title.id or False,
603 'function': lead.function,
604 'street': lead.street,
605 'street2': lead.street2,
608 'country_id': lead.country_id and lead.country_id.id or False,
609 'state_id': lead.state_id and lead.state_id.id or False,
610 'is_company': is_company,
613 partner = partner.create(cr, uid,vals, context)
616 def _create_lead_partner(self, cr, uid, lead, context=None):
618 if lead.partner_name and lead.contact_name:
619 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
620 self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
621 elif lead.partner_name and not lead.contact_name:
622 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
623 elif not lead.partner_name and lead.contact_name:
624 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
626 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
629 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
631 res_partner = self.pool.get('res.partner')
633 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
634 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
635 res = lead.write({'partner_id' : partner_id, }, context=context)
636 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
639 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
641 This function convert partner based on action.
642 if action is 'create', create new partner with contact and assign lead to new partner_id.
643 otherwise assign lead to specified partner_id
648 for lead in self.browse(cr, uid, ids, context=context):
649 if action == 'create':
651 partner_id = self._create_lead_partner(cr, uid, lead, context)
652 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
653 partner_ids[lead.id] = partner_id
656 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
658 Send mail to salesman with updated Lead details.
659 @ lead: browse record of 'crm.lead' object.
661 #TOFIX: mail template should be used here instead of fix subject, body text.
662 message = self.pool.get('mail.message')
663 email_to = lead.user_id and lead.user_id.user_email
667 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
668 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
669 subject = "lead %s converted into opportunity" % lead.name
670 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
671 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
674 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
679 value['section_id'] = team_id
680 if index < len(user_ids):
681 value['user_id'] = user_ids[index]
684 self.write(cr, uid, [lead_id], value, context=context)
687 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):
689 action :('schedule','Schedule a call'), ('log','Log a call')
691 phonecall = self.pool.get('crm.phonecall')
692 model_data = self.pool.get('ir.model.data')
695 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
697 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
698 for lead in self.browse(cr, uid, ids, context=context):
700 section_id = lead.section_id and lead.section_id.id or False
702 user_id = lead.user_id and lead.user_id.id or False
704 'name' : call_summary,
705 'opportunity_id' : lead.id,
706 'user_id' : user_id or False,
707 'categ_id' : categ_id or False,
708 'description' : desc or '',
709 'date' : schedule_time,
710 'section_id' : section_id or False,
711 'partner_id': lead.partner_id and lead.partner_id.id or False,
712 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
713 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
714 'priority': lead.priority,
716 new_id = phonecall.create(cr, uid, vals, context=context)
717 phonecall.case_open(cr, uid, [new_id], context=context)
719 phonecall.case_close(cr, uid, [new_id], context=context)
720 phonecall_dict[lead.id] = new_id
721 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
722 return phonecall_dict
725 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
726 models_data = self.pool.get('ir.model.data')
728 # Get Opportunity views
729 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
730 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
732 'name': _('Opportunity'),
734 'view_mode': 'tree, form',
735 'res_model': 'crm.lead',
736 'domain': [('type', '=', 'opportunity')],
737 'res_id': int(opportunity_id),
739 'views': [(form_view and form_view[1] or False, 'form'),
740 (tree_view and tree_view[1] or False, 'tree'),
741 (False, 'calendar'), (False, 'graph')],
742 'type': 'ir.actions.act_window',
746 def message_new(self, cr, uid, msg, custom_values=None, context=None):
747 """Automatically calls when new email message arrives"""
748 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
749 subject = msg.get('subject') or _("No Subject")
750 body = msg.get('body_text')
752 msg_from = msg.get('from')
753 priority = msg.get('priority')
756 'email_from': msg_from,
757 'email_cc': msg.get('cc'),
762 vals['priority'] = priority
763 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
764 self.write(cr, uid, [res_id], vals, context)
767 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
768 if isinstance(ids, (str, int, long)):
772 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
774 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
775 vals['priority'] = msg.get('priority')
777 'cost':'planned_cost',
778 'revenue': 'planned_revenue',
779 'probability':'probability'
782 for line in msg['body_text'].split('\n'):
784 res = tools.misc.command_re.match(line)
785 if res and maps.get(res.group(1).lower()):
786 key = maps.get(res.group(1).lower())
787 vls[key] = res.group(2).lower()
790 # Unfortunately the API is based on lists
791 # but we want to update the state based on the
792 # previous state, so we have to loop:
793 for case in self.browse(cr, uid, ids, context=context):
795 if case.state in CRM_LEAD_PENDING_STATES:
797 values.update(state=crm.AVAILABLE_STATES[1][0])
798 if not case.date_open:
799 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
800 res = self.write(cr, uid, [case.id], values, context=context)
803 def action_makeMeeting(self, cr, uid, ids, context=None):
805 This opens Meeting's calendar view to schedule meeting on current Opportunity
806 @return : Dictionary value for created Meeting view
811 data_obj = self.pool.get('ir.model.data')
812 for opp in self.browse(cr, uid, ids, context=context):
814 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
815 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
816 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
817 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
819 'default_opportunity_id': opp.id,
820 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
821 'default_user_id': uid,
822 'default_section_id': opp.section_id and opp.section_id.id or False,
823 'default_email_from': opp.email_from,
824 'default_state': 'open',
825 'default_name': opp.name
828 'name': _('Meetings'),
831 'view_mode': 'calendar,form,tree',
832 'res_model': 'crm.meeting',
834 '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')],
835 'type': 'ir.actions.act_window',
836 'search_view_id': search_view and search_view[1] or False,
842 def unlink(self, cr, uid, ids, context=None):
843 for lead in self.browse(cr, uid, ids, context):
844 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
845 raise osv.except_osv(_('Error'),
846 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
847 "You should better cancel it, instead of deleting it.") % lead.name)
848 return super(crm_lead, self).unlink(cr, uid, ids, context)
851 def write(self, cr, uid, ids, vals, context=None):
855 if 'date_closed' in vals:
856 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
858 if vals.get('stage_id'):
859 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
860 # change probability of lead(s) if required by stage
861 if not vals.get('probability') and stage.on_change:
862 vals['probability'] = stage.probability
863 for case in self.browse(cr, uid, ids, context=context):
864 message = _("Stage changed to <b>%s</b>.") % (stage.name)
865 case.message_append_note(body=message)
866 return super(crm_lead,self).write(cr, uid, ids, vals, context)
868 # ----------------------------------------
869 # OpenChatter methods and notifications
870 # ----------------------------------------
872 def message_get_subscribers(self, cr, uid, ids, context=None):
873 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
874 # add salesman to the subscribers
875 for obj in self.browse(cr, uid, ids, context=context):
877 sub_ids.append(obj.user_id.id)
878 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
880 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
881 if isinstance(lead, (int, long)):
882 lead = self.browse(cr, uid, [lead], context=context)[0]
883 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
885 def create_send_note(self, cr, uid, ids, context=None):
887 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
888 self.message_append_note(cr, uid, [id], body=message, context=context)
891 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
892 message = _("Opportunity has been <b>lost</b>.")
893 return self.message_append_note(cr, uid, ids, body=message, context=context)
895 def case_mark_won_send_note(self, cr, uid, ids, context=None):
896 message = _("Opportunity has been <b>won</b>.")
897 return self.message_append_note(cr, uid, ids, body=message, context=context)
899 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
900 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
901 if action == 'log': prefix = 'Logged'
902 else: prefix = 'Scheduled'
903 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
904 return self. message_append_note(cr, uid, ids, body=message, context=context)
906 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
907 for lead in self.browse(cr, uid, ids, context=context):
908 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))
909 lead.message_append_note(body=message)
912 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
913 message = _("Lead has been <b>converted to an opportunity</b>.")
914 lead.message_append_note(body=message)
919 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: