1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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 ##############################################################################
22 from osv import fields, osv
23 from datetime import datetime
26 from tools.translate import _
27 from crm import crm_case
30 from mail.mail_message import to_email
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(crm_case, osv.osv):
41 _description = "Lead/Opportunity"
42 _order = "priority,date_action,id desc"
43 _inherit = ['mail.thread','res.partner.address']
45 # overridden because res.partner.address has an inconvenient name_get,
46 # especially if base_contact is installed.
47 def name_get(self, cr, user, ids, context=None):
48 if isinstance(ids, (int, long)):
50 return [(r['id'], tools.ustr(r[self._rec_name]))
51 for r in self.read(cr, user, ids, [self._rec_name], context)]
53 def _compute_day(self, cr, uid, ids, fields, args, context=None):
55 @param cr: the current row, from the database cursor,
56 @param uid: the current user’s ID for security checks,
57 @param ids: List of Openday’s IDs
58 @return: difference between current date and log date
59 @param context: A standard dictionary for contextual values
61 cal_obj = self.pool.get('resource.calendar')
62 res_obj = self.pool.get('resource.resource')
65 for lead in self.browse(cr, uid, ids, context=context):
70 if field == 'day_open':
72 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
73 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
74 ans = date_open - date_create
75 date_until = lead.date_open
76 elif field == 'day_close':
78 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
79 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
80 date_until = lead.date_closed
81 ans = date_close - date_create
85 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
87 resource_id = resource_ids[0]
89 duration = float(ans.days)
90 if lead.section_id and lead.section_id.resource_calendar_id:
91 duration = float(ans.days) * 24
92 new_dates = cal_obj.interval_get(cr,
94 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
95 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
100 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
101 for in_time, out_time in new_dates:
102 if in_time.date not in no_days:
103 no_days.append(in_time.date)
104 if out_time > date_until:
106 duration = len(no_days)
107 res[lead.id][field] = abs(int(duration))
110 def _history_search(self, cr, uid, obj, name, args, context=None):
112 msg_obj = self.pool.get('mail.message')
113 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
114 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
117 return [('id', 'in', lead_ids)]
119 return [('id', '=', '0')]
121 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
123 for obj in self.browse(cr, uid, ids, context=context):
125 for msg in obj.message_ids:
127 res[obj.id] = msg.subject
132 # Overridden from res.partner.address:
133 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
134 select=True, help="Optional linked partner, usually after conversion of the lead"),
136 'id': fields.integer('ID', readonly=True),
137 'name': fields.char('Name', size=64, select=1),
138 'active': fields.boolean('Active', required=False),
139 'date_action_last': fields.datetime('Last Action', readonly=1),
140 'date_action_next': fields.datetime('Next Action', readonly=1),
141 'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
142 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
143 select=True, help='When sending mails, the default email address is taken from the sales team.'),
144 'create_date': fields.datetime('Creation Date' , readonly=True),
145 '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"),
146 'description': fields.text('Notes'),
147 'write_date': fields.datetime('Update Date' , readonly=True),
149 'categ_id': fields.many2one('crm.case.categ', 'Category', \
150 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
151 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
152 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
153 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
154 'contact_name': fields.char('Contact Name', size=64),
155 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner that will be created while converting the into opportunity', select=1),
156 'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
157 'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
158 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
159 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
160 'date_closed': fields.datetime('Closed', readonly=True),
161 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
162 'user_id': fields.many2one('res.users', 'Salesman'),
163 'referred': fields.char('Referred By', size=64),
164 'date_open': fields.datetime('Opened', readonly=True),
165 'day_open': fields.function(_compute_day, string='Days to Open', \
166 multi='day_open', type="float", store=True),
167 'day_close': fields.function(_compute_day, string='Days to Close', \
168 multi='day_close', type="float", store=True),
169 'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
170 help='The state is set to \'Draft\', when a case is created.\
171 \nIf the case is in progress the state is set to \'Open\'.\
172 \nWhen the case is over, the state is set to \'Done\'.\
173 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
174 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
175 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
178 # Only used for type opportunity
179 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"),
180 'probability': fields.float('Probability (%)',group_operator="avg"),
181 'planned_revenue': fields.float('Expected Revenue'),
182 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
183 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
184 'phone': fields.char("Phone", size=64),
185 'date_deadline': fields.date('Expected Closing'),
186 'date_action': fields.date('Next Action Date'),
187 'title_action': fields.char('Next Action', size=64),
188 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
189 'color': fields.integer('Color Index'),
190 'partner_address_name': fields.related('partner_address_id', 'name', type='char', string='Partner Contact Name', readonly=True),
191 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
192 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
193 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
198 'active': lambda *a: 1,
199 'user_id': crm_case._get_default_user,
200 'email_from': crm_case._get_default_email,
201 'state': lambda *a: 'draft',
202 'type': lambda *a: 'lead',
203 'section_id': crm_case._get_section,
204 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
205 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
207 #'stage_id': _get_stage_id,
210 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
211 """This function returns value of partner email based on Partner Address
214 return {'value': {'email_from': False, 'country_id': False}}
215 address = self.pool.get('res.partner.address').browse(cr, uid, add)
216 return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
218 def on_change_optin(self, cr, uid, ids, optin):
219 return {'value':{'optin':optin,'optout':False}}
221 def on_change_optout(self, cr, uid, ids, optout):
222 return {'value':{'optout':optout,'optin':False}}
224 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
227 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
228 if not stage.on_change:
230 return {'value':{'probability': stage.probability}}
232 def stage_find_percent(self, cr, uid, percent, section_id):
233 """ Return the first stage with a probability == percent
235 stage_pool = self.pool.get('crm.case.stage')
237 ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
239 ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
245 def stage_find_lost(self, cr, uid, section_id):
246 return self.stage_find_percent(cr, uid, 0.0, section_id)
248 def stage_find_won(self, cr, uid, section_id):
249 return self.stage_find_percent(cr, uid, 100.0, section_id)
251 def case_open(self, cr, uid, ids, *args):
252 for l in self.browse(cr, uid, ids):
253 # When coming from draft override date and stage otherwise just set state
254 if l.state == 'draft':
256 message = _("The lead '%s' has been opened.") % l.name
257 elif l.type == 'opportunity':
258 message = _("The opportunity '%s' has been opened.") % l.name
260 message = _("The case '%s' has been opened.") % l.name
261 self.log(cr, uid, l.id, message)
262 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
263 self.write(cr, uid, [l.id], value)
264 if l.type == 'opportunity' and not l.stage_id:
265 stage_id = self.stage_find(cr, uid, l.section_id.id or False, [('sequence','>',0)])
267 self.stage_set(cr, uid, [l.id], stage_id)
268 res = super(crm_lead, self).case_open(cr, uid, ids, *args)
271 def case_close(self, cr, uid, ids, *args):
272 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
273 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
274 for case in self.browse(cr, uid, ids):
275 if case.type == 'lead':
276 message = _("The lead '%s' has been closed.") % case.name
278 message = _("The case '%s' has been closed.") % case.name
279 self.log(cr, uid, case.id, message)
282 def case_cancel(self, cr, uid, ids, *args):
283 """Overrides cancel for crm_case for setting probability
285 res = super(crm_lead, self).case_cancel(cr, uid, ids, args)
286 self.write(cr, uid, ids, {'probability' : 0.0})
289 def case_reset(self, cr, uid, ids, *args):
290 """Overrides reset as draft in order to set the stage field as empty
292 res = super(crm_lead, self).case_reset(cr, uid, ids, *args)
293 self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
296 def case_mark_lost(self, cr, uid, ids, *args):
297 """Mark the case as lost: state = done and probability = 0%
299 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
300 self.write(cr, uid, ids, {'probability' : 0.0})
301 for l in self.browse(cr, uid, ids):
302 stage_id = self.stage_find_lost(cr, uid, l.section_id.id or False)
304 self.stage_set(cr, uid, [l.id], stage_id)
305 message = _("The opportunity '%s' has been marked as lost.") % l.name
306 self.log(cr, uid, l.id, message)
309 def case_mark_won(self, cr, uid, ids, *args):
310 """Mark the case as lost: state = done and probability = 0%
312 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
313 self.write(cr, uid, ids, {'probability' : 100.0})
314 for l in self.browse(cr, uid, ids):
315 stage_id = self.stage_find_won(cr, uid, l.section_id.id or False)
317 self.stage_set(cr, uid, [l.id], stage_id)
318 message = _("The opportunity '%s' has been been won.") % l.name
319 self.log(cr, uid, l.id, message)
322 def set_priority(self, cr, uid, ids, priority):
325 return self.write(cr, uid, ids, {'priority' : priority})
327 def set_high_priority(self, cr, uid, ids, *args):
328 """Set lead priority to high
330 return self.set_priority(cr, uid, ids, '1')
332 def set_normal_priority(self, cr, uid, ids, *args):
333 """Set lead priority to normal
335 return self.set_priority(cr, uid, ids, '3')
338 def _merge_data(self, cr, uid, ids, oldest, context=None):
339 # prepare opportunity data into dictionary for merging
340 opportunities = self.browse(cr, uid, ids, context=context)
341 def _get_first_not_null(attr):
342 if hasattr(oldest, attr):
343 return getattr(oldest, attr)
344 for opportunity in opportunities:
345 if hasattr(opportunity, attr):
346 return getattr(opportunity, attr)
349 def _get_first_not_null_id(attr):
350 res = _get_first_not_null(attr)
351 return res and res.id or False
353 def _concat_all(attr):
354 return ', '.join([getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)])
357 'partner_id': _get_first_not_null_id('partner_id'), # !!
358 'title': _get_first_not_null_id('title'),
359 'name' : _get_first_not_null('name'), #not lost
360 'categ_id' : _get_first_not_null_id('categ_id'), # !!
361 'channel_id' : _get_first_not_null_id('channel_id'), # !!
362 'city' : _get_first_not_null('city'), # !!
363 'company_id' : _get_first_not_null_id('company_id'), #!!
364 'contact_name' : _get_first_not_null('contact_name'), #not lost
365 'country_id' : _get_first_not_null_id('country_id'), #!!
366 'partner_address_id' : _get_first_not_null_id('partner_address_id'), #!!
367 'type_id' : _get_first_not_null_id('type_id'), #!!
368 'user_id' : _get_first_not_null_id('user_id'), #!!
369 'section_id' : _get_first_not_null_id('section_id'), #!!
370 'state_id' : _get_first_not_null_id('state_id'),
371 'description' : _concat_all('description'), #not lost
372 'email' : _get_first_not_null('email'), # !!
373 'fax' : _get_first_not_null('fax'),
374 'mobile' : _get_first_not_null('mobile'),
375 'partner_name' : _get_first_not_null('partner_name'),
376 'phone' : _get_first_not_null('phone'),
377 'probability' : _get_first_not_null('probability'),
378 'planned_revenue' : _get_first_not_null('planned_revenue'),
379 'street' : _get_first_not_null('street'),
380 'street2' : _get_first_not_null('street2'),
381 'zip' : _get_first_not_null('zip'),
383 'create_date' : _get_first_not_null('create_date'),
384 'date_action_last': _get_first_not_null('date_action_last'),
385 'date_action_next': _get_first_not_null('date_action_next'),
386 'email_from' : _get_first_not_null('email_from'),
387 'email_cc' : _get_first_not_null('email_cc'),
388 'partner_name' : _get_first_not_null('partner_name'),
392 def _merge_find_oldest(self, cr, uid, ids, context=None):
395 #TOCHECK: where pass 'convert' in context ?
396 if context.get('convert'):
397 ids = list(set(ids) - set(context.get('lead_ids', False)) )
399 #search opportunities order by create date
400 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
401 oldest_id = opportunity_ids[0]
402 return self.browse(cr, uid, oldest_id, context=context)
404 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
405 #TOFIX: mail template should be used instead of fix body, subject text
407 merge_message = _('Merged opportunities')
408 subject = [merge_message]
409 for opportunity in opportunities:
410 subject.append(opportunity.name)
411 details.append(_('%s : %s\n Partner: %s\n Stage: %s\n Section: %s\n Salesman: %s\n Category: %s\n Channel: %s\n Company: %s\n Contact name: %s\n Email: %s\n Phone number: %s\n Fax: %s\n Mobile: %s\n State: %s\n Description: %s\n Probability: %s\n Planned revennue: %s\n Country: %s\n City: %s\n Street: %s\n Street 2: %s\n Zip 2: %s') % (merge_message, opportunity.name, opportunity.partner_id.name or '',
412 opportunity.stage_id.name or '',
413 opportunity.section_id.name or '',
414 opportunity.user_id.name or '',
415 opportunity.categ_id.name or '',
416 opportunity.channel_id.name or '',
417 opportunity.company_id.name or '',
418 opportunity.contact_name or '',
419 opportunity.email_from or '',
420 opportunity.phone or '',
421 opportunity.fax or '',
422 opportunity.mobile or '',
423 opportunity.state_id.name or '',
424 opportunity.description or '',
425 opportunity.probability or '',
426 opportunity.planned_revenue or '',
427 opportunity.country_id.name or '',
428 opportunity.city or '',
429 opportunity.street or '',
430 opportunity.street2 or '',
431 opportunity.zip or '',
433 subject = subject[0] + ", ".join(subject[1:])
434 details = "\n\n".join(details)
435 return self.message_append(cr, uid, [opportunity_id], subject, body_text=details, context=context)
437 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
438 message = self.pool.get('mail.message')
439 for opportunity in opportunities:
440 for history in opportunity.message_ids:
441 message.write(cr, uid, history.id, {
442 'res_id': opportunity_id,
443 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
448 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
449 attachment = self.pool.get('ir.attachment')
451 # return attachments of opportunity
452 def _get_attachments(opportunity_id):
453 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
454 return attachment.browse(cr, uid, attachment_ids, context=context)
457 first_attachments = _get_attachments(opportunity_id)
458 for opportunity in opportunities:
459 attachments = _get_attachments(opportunity.id)
460 for first in first_attachments:
461 for attachment in attachments:
462 if attachment.name == first.name:
464 name = "%s (%s)" % (attachment.name, count,),
465 res_id = opportunity_id,
467 attachment.write(values)
472 def merge_opportunity(self, cr, uid, ids, context=None):
474 To merge opportunities
475 :param ids: list of opportunities ids to merge
477 if context is None: context = {}
479 #TOCHECK: where pass lead_ids in context?
480 lead_ids = context and context.get('lead_ids', []) or []
483 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunities.'))
485 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
486 opportunities = self.browse(cr, uid, ids, context=context)
487 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
488 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
489 if ctx_opportunities :
490 first_opportunity = ctx_opportunities[0]
491 tail_opportunities = opportunities_list
493 first_opportunity = opportunities_list[0]
494 tail_opportunities = opportunities_list[1:]
496 data = self._merge_data(cr, uid, ids, oldest, context=context)
498 # merge data into first opportunity
499 self.write(cr, uid, [first_opportunity.id], data, context=context)
501 #copy message and attachements into the first opportunity
502 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
503 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
505 #Notification about loss of information
506 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
507 #delete tail opportunities
508 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
510 return first_opportunity.id
512 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
513 crm_stage = self.pool.get('crm.case.stage')
514 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
516 section_id = lead.section_id and lead.section_id.id or False
518 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
520 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
521 stage_id = stage_ids and stage_ids[0] or False
523 'planned_revenue': lead.planned_revenue,
524 'probability': lead.probability,
526 'partner_id': customer.id,
527 'user_id': (lead.user_id and lead.user_id.id),
528 'type': 'opportunity',
529 'stage_id': stage_id or False,
530 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
531 'partner_address_id': contact_id
533 def _convert_opportunity_notification(self, cr, uid, lead, context=None):
534 success_message = _("Lead '%s' has been converted to an opportunity.") % lead.name
535 self.message_append(cr, uid, [lead.id], success_message, body_text=success_message, context=context)
536 self.log(cr, uid, lead.id, success_message)
537 self._send_mail_to_salesman(cr, uid, lead, context=context)
540 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
541 partner = self.pool.get('res.partner')
542 mail_message = self.pool.get('mail.message')
544 customer = partner.browse(cr, uid, partner_id, context=context)
546 for lead in self.browse(cr, uid, ids, context=context):
547 if lead.state in ('done', 'cancel'):
549 if user_ids or section_id:
550 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
552 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
553 self.write(cr, uid, [lead.id], vals, context=context)
555 self._convert_opportunity_notification(cr, uid, lead, context=context)
556 #TOCHECK: why need to change partner details in all messages of lead ?
558 msg_ids = [ x.id for x in lead.message_ids]
559 mail_message.write(cr, uid, msg_ids, {
560 'partner_id': lead.partner_id.id
564 def _lead_create_partner(self, cr, uid, lead, context=None):
565 partner = self.pool.get('res.partner')
566 partner_id = partner.create(cr, uid, {
567 'name': lead.partner_name or lead.contact_name or lead.name,
568 'user_id': lead.user_id.id,
569 'comment': lead.description,
574 def _lead_assign_partner(self, cr, uid, ids, partner_id, context=None):
575 contact_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id])['default']
576 return self.write(cr, uid, ids, {'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
578 def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
579 address = self.pool.get('res.partner.address')
580 return address.create(cr, uid, {
581 'partner_id': partner_id,
582 'name': lead.contact_name,
584 'mobile': lead.mobile,
585 'email': to_email(lead.email_from)[0],
587 'title': lead.title and lead.title.id or False,
588 'function': lead.function,
589 'street': lead.street,
590 'street2': lead.street2,
593 'country_id': lead.country_id and lead.country_id.id or False,
594 'state_id': lead.state_id and lead.state_id.id or False,
597 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
599 This function convert partner based on action.
600 if action is 'new', create new partner with contact and assign lead to new partner_id.
601 otherwise assign lead to specified partner_id
606 for lead in self.browse(cr, uid, ids, context=context):
607 partner_id = lead.partner_id and lead.partner_id.id or False
608 if action == 'create':
610 partner_id = self._lead_create_partner(cr, uid, lead, context=context)
611 self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
612 self._lead_assign_partner(cr, uid, [lead.id], partner_id, context=context)
613 partner_ids[lead.id] = partner_id
616 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
618 Send mail to salesman with updated Lead details.
619 @ lead: browse record of 'crm.lead' object.
621 #TOFIX: mail template should be used here instead of fix subject, body text.
622 message = self.pool.get('mail.message')
623 email_to = lead.user_id and lead.user_id.user_email
627 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
628 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
629 subject = "lead %s converted into opportunity" % lead.name
630 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
631 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
634 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
639 value['section_id'] = team_id
640 if index < len(user_ids):
641 value['user_id'] = user_ids[index]
644 self.write(cr, uid, [lead_id], value, context=context)
647 def schedule_phonecall(self, cr, uid, ids, schedule_time, call_summary, user_id=False, section_id=False, categ_id=False, action='schedule', context=None):
649 action :('schedule','Schedule a call'), ('log','Log a call')
651 phonecall = self.pool.get('crm.phonecall')
652 model_data = self.pool.get('ir.model.data')
655 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
657 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
658 for lead in self.browse(cr, uid, ids, context=context):
660 section_id = lead.section_id and lead.section_id.id or False
662 user_id = lead.user_id and lead.user_id.id or False
664 'name' : call_summary,
665 'opportunity_id' : lead.id,
666 'user_id' : user_id or False,
667 'categ_id' : categ_id or False,
668 'description' : lead.description or False,
669 'date' : schedule_time,
670 'section_id' : section_id or False,
671 'partner_id': lead.partner_id and lead.partner_id.id or False,
672 'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
673 'partner_phone' : lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
674 'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
675 'priority': lead.priority,
678 new_id = phonecall.create(cr, uid, vals, context=context)
679 phonecall.case_open(cr, uid, [new_id])
681 phonecall.case_close(cr, uid, [new_id])
682 phonecall_dict[lead.id] = new_id
683 return phonecall_dict
686 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
687 models_data = self.pool.get('ir.model.data')
689 # Get Opportunity views
690 opportunity_view_form = models_data._get_id(
691 cr, uid, 'crm', 'crm_case_form_view_oppor')
692 opportunity_view_tree = models_data._get_id(
693 cr, uid, 'crm', 'crm_case_tree_view_oppor')
694 if opportunity_view_form:
695 opportunity_view_form = models_data.browse(
696 cr, uid, opportunity_view_form, context=context).res_id
697 if opportunity_view_tree:
698 opportunity_view_tree = models_data.browse(
699 cr, uid, opportunity_view_tree, context=context).res_id
702 'name': _('Opportunity'),
704 'view_mode': 'tree, form',
705 'res_model': 'crm.lead',
706 'domain': [('type', '=', 'opportunity')],
707 'res_id': int(opportunity_id),
709 'views': [(opportunity_view_form, 'form'),
710 (opportunity_view_tree, 'tree'),
711 (False, 'calendar'), (False, 'graph')],
712 'type': 'ir.actions.act_window',
716 def button_convert_opportunity(self, cr, uid, ids, context=None):
717 """ Precomputation for converting lead to opportunity
721 context.update({'active_ids': ids})
723 data_obj = self.pool.get('ir.model.data')
727 for case in self.browse(cr, uid, ids, context=context):
728 context.update({'active_id': case.id})
729 data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
732 view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
734 'name': _('Create Partner'),
736 'view_mode': 'form,tree',
737 'res_model': 'crm.lead2opportunity.partner',
740 'views': [(view_id1, 'form')],
741 'type': 'ir.actions.act_window',
747 def message_new(self, cr, uid, msg, custom_values=None, context=None):
748 """Automatically calls when new email message arrives"""
749 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
750 subject = msg.get('subject') or _("No Subject")
751 body = msg.get('body_text')
753 msg_from = msg.get('from')
754 priority = msg.get('priority')
757 'email_from': msg_from,
758 'email_cc': msg.get('cc'),
763 vals['priority'] = priority
764 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
765 self.write(cr, uid, [res_id], vals, context)
768 def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
769 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:
796 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
797 res = self.write(cr, uid, [case.id], values, context=context)
800 def action_makeMeeting(self, cr, uid, ids, context=None):
802 This opens Meeting's calendar view to schedule meeting on current Opportunity
803 @return : Dictionary value for created Meeting view
806 for opp in self.browse(cr, uid, ids, context=context):
807 data_obj = self.pool.get('ir.model.data')
810 result = data_obj._get_id(cr, uid, 'crm', 'view_crm_case_meetings_filter')
811 res = data_obj.read(cr, uid, result, ['res_id'])
812 id1 = data_obj._get_id(cr, uid, 'crm', 'crm_case_calendar_view_meet')
813 id2 = data_obj._get_id(cr, uid, 'crm', 'crm_case_form_view_meet')
814 id3 = data_obj._get_id(cr, uid, 'crm', 'crm_case_tree_view_meet')
816 id1 = data_obj.browse(cr, uid, id1, context=context).res_id
818 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
820 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
823 'default_opportunity_id': opp.id,
824 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
825 'default_user_id': uid,
826 'default_section_id': opp.section_id and opp.section_id.id or False,
827 'default_email_from': opp.email_from,
828 'default_state': 'open',
829 'default_name': opp.name
832 'name': _('Meetings'),
835 'view_mode': 'calendar,form,tree',
836 'res_model': 'crm.meeting',
838 'views': [(id1, 'calendar'), (id2, 'form'), (id3, 'tree')],
839 'type': 'ir.actions.act_window',
840 'search_view_id': res['res_id'],
846 def unlink(self, cr, uid, ids, context=None):
847 for lead in self.browse(cr, uid, ids, context):
848 if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
849 raise osv.except_osv(_('Warning !'),
850 _('You can not delete this lead. You should better cancel it.'))
851 return super(crm_lead, self).unlink(cr, uid, ids, context)
854 def write(self, cr, uid, ids, vals, context=None):
858 if 'date_closed' in vals:
859 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
861 if 'stage_id' in vals and vals['stage_id']:
862 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
863 text = _("Changed Stage to: %s") % stage_obj.name
864 self.message_append(cr, uid, ids, text, body_text=text, context=context)
866 for case in self.browse(cr, uid, ids, context=context):
867 if case.type == 'lead' or context.get('stage_type',False)=='lead':
868 message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
869 elif case.type == 'opportunity':
870 message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
871 self.log(cr, uid, case.id, message)
872 return super(crm_lead,self).write(cr, uid, ids, vals, context)
876 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: