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', '=', False)]
92 search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', 1), ('fold', '=', False)]
94 search_domain += ['|', ('type', '=', type), ('type', '=', 'both')]
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):
358 """ Set lead 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')
372 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
373 # prepare opportunity data into dictionary for merging
374 opportunities = self.browse(cr, uid, ids, context=context)
375 def _get_first_not_null(attr):
376 if hasattr(oldest, attr):
377 return getattr(oldest, attr)
378 for opportunity in opportunities:
379 if hasattr(opportunity, attr):
380 return getattr(opportunity, attr)
383 def _get_first_not_null_id(attr):
384 res = _get_first_not_null(attr)
385 return res and res.id or False
387 def _concat_all(attr):
388 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
391 for field_name in fields:
392 field_info = self._all_columns.get(field_name)
393 if field_info is None:
395 field = field_info.column
396 if field._type in ('many2many', 'one2many'):
398 elif field._type == 'many2one':
399 data[field_name] = _get_first_not_null_id(field_name) # !!
400 elif field._type == 'text':
401 data[field_name] = _concat_all(field_name) #not lost
403 data[field_name] = _get_first_not_null(field_name) #not lost
406 def _merge_find_oldest(self, cr, uid, ids, context=None):
409 #TOCHECK: where pass 'convert' in context ?
410 if context.get('convert'):
411 ids = list(set(ids) - set(context.get('lead_ids', False)) )
413 #search opportunities order by create date
414 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
415 oldest_id = opportunity_ids[0]
416 return self.browse(cr, uid, oldest_id, context=context)
418 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
421 body.append("%s\n" % (title))
422 for field_name in fields:
423 field_info = self._all_columns.get(field_name)
424 if field_info is None:
426 field = field_info.column
429 if field._type == 'selection':
430 if hasattr(field.selection, '__call__'):
431 key = field.selection(self, cr, uid, context=context)
433 key = field.selection
434 value = dict(key).get(lead[field_name], lead[field_name])
435 elif field._type == 'many2one':
437 value = lead[field_name].name_get()[0][1]
439 value = lead[field_name]
441 body.append("%s: %s" % (field.string, value or ''))
442 return "\n".join(body + ['---'])
444 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
445 #TOFIX: mail template should be used instead of fix body, subject text
447 merge_message = _('Merged opportunities')
448 subject = [merge_message]
449 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
450 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
451 'country_id', 'city', 'street', 'street2', 'zip']
452 for opportunity in opportunities:
453 subject.append(opportunity.name)
454 title = "%s : %s" % (merge_message, opportunity.name)
455 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
457 subject = subject[0] + ", ".join(subject[1:])
458 details = "\n\n".join(details)
459 return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
461 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
462 message = self.pool.get('mail.message')
463 for opportunity in opportunities:
464 for history in opportunity.message_ids:
465 message.write(cr, uid, history.id, {
466 'res_id': opportunity_id,
467 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
472 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
473 attachment = self.pool.get('ir.attachment')
475 # return attachments of opportunity
476 def _get_attachments(opportunity_id):
477 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
478 return attachment.browse(cr, uid, attachment_ids, context=context)
481 first_attachments = _get_attachments(opportunity_id)
482 for opportunity in opportunities:
483 attachments = _get_attachments(opportunity.id)
484 for first in first_attachments:
485 for attachment in attachments:
486 if attachment.name == first.name:
488 name = "%s (%s)" % (attachment.name, count,),
489 res_id = opportunity_id,
491 attachment.write(values)
496 def merge_opportunity(self, cr, uid, ids, context=None):
498 To merge opportunities
499 :param ids: list of opportunities ids to merge
501 if context is None: context = {}
503 #TOCHECK: where pass lead_ids in context?
504 lead_ids = context and context.get('lead_ids', []) or []
507 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
509 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
510 opportunities = self.browse(cr, uid, ids, context=context)
511 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
512 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
513 if ctx_opportunities :
514 first_opportunity = ctx_opportunities[0]
515 tail_opportunities = opportunities_list
517 first_opportunity = opportunities_list[0]
518 tail_opportunities = opportunities_list[1:]
520 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',
521 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
522 'date_action_next', 'email_from', 'email_cc', 'partner_name']
524 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
526 # merge data into first opportunity
527 self.write(cr, uid, [first_opportunity.id], data, context=context)
529 #copy message and attachements into the first opportunity
530 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
531 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
533 #Notification about loss of information
534 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
535 #delete tail opportunities
536 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
538 #open first opportunity
539 self.case_open(cr, uid, [first_opportunity.id])
540 return first_opportunity.id
542 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
543 crm_stage = self.pool.get('crm.case.stage')
546 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
548 section_id = lead.section_id and lead.section_id.id or False
550 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
552 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
553 stage_id = stage_ids and stage_ids[0] or False
555 'planned_revenue': lead.planned_revenue,
556 'probability': lead.probability,
558 'partner_id': customer and customer.id or False,
559 'user_id': (lead.user_id and lead.user_id.id),
560 'type': 'opportunity',
561 'stage_id': stage_id or False,
562 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
563 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
566 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
567 partner = self.pool.get('res.partner')
568 mail_message = self.pool.get('mail.message')
571 customer = partner.browse(cr, uid, partner_id, context=context)
572 for lead in self.browse(cr, uid, ids, context=context):
573 if lead.state in ('done', 'cancel'):
575 if user_ids or section_id:
576 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
578 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
579 self.write(cr, uid, [lead.id], vals, context=context)
581 self.convert_opportunity_send_note(cr, uid, lead, context=context)
582 #TOCHECK: why need to change partner details in all messages of lead ?
584 msg_ids = [ x.id for x in lead.message_ids]
585 mail_message.write(cr, uid, msg_ids, {
586 'partner_id': lead.partner_id.id
590 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
591 partner = self.pool.get('res.partner')
592 vals = { 'name': name,
593 'user_id': lead.user_id.id,
594 'comment': lead.description,
595 'section_id': lead.section_id.id or False,
596 'parent_id': parent_id,
598 'mobile': lead.mobile,
599 'email': lead.email_from and to_email(lead.email_from)[0],
601 'title': lead.title and lead.title.id or False,
602 'function': lead.function,
603 'street': lead.street,
604 'street2': lead.street2,
607 'country_id': lead.country_id and lead.country_id.id or False,
608 'state_id': lead.state_id and lead.state_id.id or False,
609 'is_company': is_company,
612 partner = partner.create(cr, uid,vals, context)
615 def _create_lead_partner(self, cr, uid, lead, context=None):
617 if lead.partner_name and lead.contact_name:
618 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
619 self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
620 elif lead.partner_name and not lead.contact_name:
621 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
622 elif not lead.partner_name and lead.contact_name:
623 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
625 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
628 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
630 res_partner = self.pool.get('res.partner')
632 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
633 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
634 res = lead.write({'partner_id' : partner_id, }, context=context)
635 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
638 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
640 This function convert partner based on action.
641 if action is 'create', create new partner with contact and assign lead to new partner_id.
642 otherwise assign lead to specified partner_id
647 for lead in self.browse(cr, uid, ids, context=context):
648 if action == 'create':
650 partner_id = self._create_lead_partner(cr, uid, lead, context)
651 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
652 partner_ids[lead.id] = partner_id
655 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
657 Send mail to salesman with updated Lead details.
658 @ lead: browse record of 'crm.lead' object.
660 #TOFIX: mail template should be used here instead of fix subject, body text.
661 message = self.pool.get('mail.message')
662 email_to = lead.user_id and lead.user_id.user_email
666 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
667 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
668 subject = "lead %s converted into opportunity" % lead.name
669 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
670 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
673 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
678 value['section_id'] = team_id
679 if index < len(user_ids):
680 value['user_id'] = user_ids[index]
683 self.write(cr, uid, [lead_id], value, context=context)
686 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):
688 action :('schedule','Schedule a call'), ('log','Log a call')
690 phonecall = self.pool.get('crm.phonecall')
691 model_data = self.pool.get('ir.model.data')
694 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
696 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
697 for lead in self.browse(cr, uid, ids, context=context):
699 section_id = lead.section_id and lead.section_id.id or False
701 user_id = lead.user_id and lead.user_id.id or False
703 'name' : call_summary,
704 'opportunity_id' : lead.id,
705 'user_id' : user_id or False,
706 'categ_id' : categ_id or False,
707 'description' : desc or '',
708 'date' : schedule_time,
709 'section_id' : section_id or False,
710 'partner_id': lead.partner_id and lead.partner_id.id or False,
711 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
712 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
713 'priority': lead.priority,
715 new_id = phonecall.create(cr, uid, vals, context=context)
716 phonecall.case_open(cr, uid, [new_id], context=context)
718 phonecall.case_close(cr, uid, [new_id], context=context)
719 phonecall_dict[lead.id] = new_id
720 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
721 return phonecall_dict
724 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
725 models_data = self.pool.get('ir.model.data')
727 # Get Opportunity views
728 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
729 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
731 'name': _('Opportunity'),
733 'view_mode': 'tree, form',
734 'res_model': 'crm.lead',
735 'domain': [('type', '=', 'opportunity')],
736 'res_id': int(opportunity_id),
738 'views': [(form_view and form_view[1] or False, 'form'),
739 (tree_view and tree_view[1] or False, 'tree'),
740 (False, 'calendar'), (False, 'graph')],
741 'type': 'ir.actions.act_window',
745 def message_new(self, cr, uid, msg, custom_values=None, context=None):
746 """Automatically calls when new email message arrives"""
747 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
748 subject = msg.get('subject') or _("No Subject")
749 body = msg.get('body_text')
751 msg_from = msg.get('from')
752 priority = msg.get('priority')
755 'email_from': msg_from,
756 'email_cc': msg.get('cc'),
761 vals['priority'] = priority
762 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
763 self.write(cr, uid, [res_id], vals, context)
766 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
767 if isinstance(ids, (str, int, long)):
771 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
773 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
774 vals['priority'] = msg.get('priority')
776 'cost':'planned_cost',
777 'revenue': 'planned_revenue',
778 'probability':'probability'
781 for line in msg['body_text'].split('\n'):
783 res = tools.misc.command_re.match(line)
784 if res and maps.get(res.group(1).lower()):
785 key = maps.get(res.group(1).lower())
786 vls[key] = res.group(2).lower()
789 # Unfortunately the API is based on lists
790 # but we want to update the state based on the
791 # previous state, so we have to loop:
792 for case in self.browse(cr, uid, ids, context=context):
794 if case.state in CRM_LEAD_PENDING_STATES:
796 values.update(state=crm.AVAILABLE_STATES[1][0])
797 if not case.date_open:
798 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
799 res = self.write(cr, uid, [case.id], values, context=context)
802 def action_makeMeeting(self, cr, uid, ids, context=None):
804 This opens Meeting's calendar view to schedule meeting on current Opportunity
805 @return : Dictionary value for created Meeting view
810 data_obj = self.pool.get('ir.model.data')
811 for opp in self.browse(cr, uid, ids, context=context):
813 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
814 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
815 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
816 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
818 'default_opportunity_id': opp.id,
819 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
820 'default_user_id': uid,
821 'default_section_id': opp.section_id and opp.section_id.id or False,
822 'default_email_from': opp.email_from,
823 'default_state': 'open',
824 'default_name': opp.name
827 'name': _('Meetings'),
830 'view_mode': 'calendar,form,tree',
831 'res_model': 'crm.meeting',
833 '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')],
834 'type': 'ir.actions.act_window',
835 'search_view_id': search_view and search_view[1] or False,
841 def unlink(self, cr, uid, ids, context=None):
842 for lead in self.browse(cr, uid, ids, context):
843 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
844 raise osv.except_osv(_('Error'),
845 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
846 "You should better cancel it, instead of deleting it.") % lead.name)
847 return super(crm_lead, self).unlink(cr, uid, ids, context)
849 def write(self, cr, uid, ids, vals, context=None):
850 if vals.get('stage_id') and not vals.get('probability'):
851 # change probability of lead(s) if required by stage
852 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
854 vals['probability'] = stage.probability
855 return super(crm_lead,self).write(cr, uid, ids, vals, context)
857 # ----------------------------------------
858 # OpenChatter methods and notifications
859 # ----------------------------------------
861 def message_get_subscribers(self, cr, uid, ids, context=None):
862 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
863 # add salesman to the subscribers
864 for obj in self.browse(cr, uid, ids, context=context):
866 sub_ids.append(obj.user_id.id)
867 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
869 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
870 """ Override of the (void) default notification method. """
871 stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
872 return self.message_append_note(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
874 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
875 if isinstance(lead, (int, long)):
876 lead = self.browse(cr, uid, [lead], context=context)[0]
877 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
879 def create_send_note(self, cr, uid, ids, context=None):
881 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
882 self.message_append_note(cr, uid, [id], body=message, context=context)
885 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
886 message = _("Opportunity has been <b>lost</b>.")
887 return self.message_append_note(cr, uid, ids, body=message, context=context)
889 def case_mark_won_send_note(self, cr, uid, ids, context=None):
890 message = _("Opportunity has been <b>won</b>.")
891 return self.message_append_note(cr, uid, ids, body=message, context=context)
893 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
894 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
895 if action == 'log': prefix = 'Logged'
896 else: prefix = 'Scheduled'
897 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
898 return self. message_append_note(cr, uid, ids, body=message, context=context)
900 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
901 for lead in self.browse(cr, uid, ids, context=context):
902 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))
903 lead.message_append_note(body=message)
906 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
907 message = _("Lead has been <b>converted to an opportunity</b>.")
908 lead.message_append_note(body=message)
913 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: