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 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
46 access_rights_uid = access_rights_uid or uid
47 stage_obj = self.pool.get('crm.case.stage')
48 order = stage_obj._order
49 if read_group_order == 'stage_id desc':
50 # lame hack to allow reverting search, should just work in the trivial case
51 order = "%s desc" % order
52 stage_ids = stage_obj._search(cr, uid, ['|', ('id','in',ids),('case_default','=',1)], order=order,
53 access_rights_uid=access_rights_uid, context=context)
54 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
55 # restore order of the search
56 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
60 'stage_id': _read_group_stage_ids
63 # overridden because res.partner.address has an inconvenient name_get,
64 # especially if base_contact is installed.
65 def name_get(self, cr, user, ids, context=None):
66 if isinstance(ids, (int, long)):
68 return [(r['id'], tools.ustr(r[self._rec_name]))
69 for r in self.read(cr, user, ids, [self._rec_name], context)]
71 def _compute_day(self, cr, uid, ids, fields, args, context=None):
73 @param cr: the current row, from the database cursor,
74 @param uid: the current user’s ID for security checks,
75 @param ids: List of Openday’s IDs
76 @return: difference between current date and log date
77 @param context: A standard dictionary for contextual values
79 cal_obj = self.pool.get('resource.calendar')
80 res_obj = self.pool.get('resource.resource')
83 for lead in self.browse(cr, uid, ids, context=context):
88 if field == 'day_open':
90 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
91 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
92 ans = date_open - date_create
93 date_until = lead.date_open
94 elif field == 'day_close':
96 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
97 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
98 date_until = lead.date_closed
99 ans = date_close - date_create
103 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
104 if len(resource_ids):
105 resource_id = resource_ids[0]
107 duration = float(ans.days)
108 if lead.section_id and lead.section_id.resource_calendar_id:
109 duration = float(ans.days) * 24
110 new_dates = cal_obj.interval_get(cr,
112 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
113 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
118 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
119 for in_time, out_time in new_dates:
120 if in_time.date not in no_days:
121 no_days.append(in_time.date)
122 if out_time > date_until:
124 duration = len(no_days)
125 res[lead.id][field] = abs(int(duration))
128 def _history_search(self, cr, uid, obj, name, args, context=None):
130 msg_obj = self.pool.get('mail.message')
131 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
132 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
135 return [('id', 'in', lead_ids)]
137 return [('id', '=', '0')]
139 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
141 for obj in self.browse(cr, uid, ids, context=context):
143 for msg in obj.message_ids:
145 res[obj.id] = msg.subject
150 # Overridden from res.partner.address:
151 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
152 select=True, help="Optional linked partner, usually after conversion of the lead"),
154 'id': fields.integer('ID', readonly=True),
155 'name': fields.char('Name', size=64, select=1),
156 'active': fields.boolean('Active', required=False),
157 'date_action_last': fields.datetime('Last Action', readonly=1),
158 'date_action_next': fields.datetime('Next Action', readonly=1),
159 'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
160 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
161 select=True, help='When sending mails, the default email address is taken from the sales team.'),
162 'create_date': fields.datetime('Creation Date' , readonly=True),
163 '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"),
164 'description': fields.text('Notes'),
165 'write_date': fields.datetime('Update Date' , readonly=True),
167 'categ_id': fields.many2one('crm.case.categ', 'Category', \
168 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
169 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
170 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
171 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
172 'contact_name': fields.char('Contact Name', size=64),
173 '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),
174 'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
175 'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
176 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
177 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
178 'date_closed': fields.datetime('Closed', readonly=True),
179 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
180 'user_id': fields.many2one('res.users', 'Salesman'),
181 'referred': fields.char('Referred By', size=64),
182 'date_open': fields.datetime('Opened', readonly=True),
183 'day_open': fields.function(_compute_day, string='Days to Open', \
184 multi='day_open', type="float", store=True),
185 'day_close': fields.function(_compute_day, string='Days to Close', \
186 multi='day_close', type="float", store=True),
187 'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
188 help='The state is set to \'Draft\', when a case is created.\
189 \nIf the case is in progress the state is set to \'Open\'.\
190 \nWhen the case is over, the state is set to \'Done\'.\
191 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
192 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
193 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
196 # Only used for type opportunity
197 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"),
198 'probability': fields.float('Probability (%)',group_operator="avg"),
199 'planned_revenue': fields.float('Expected Revenue'),
200 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
201 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
202 'phone': fields.char("Phone", size=64),
203 'date_deadline': fields.date('Expected Closing'),
204 'date_action': fields.date('Next Action Date'),
205 'title_action': fields.char('Next Action', size=64),
206 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
207 'color': fields.integer('Color Index'),
208 'partner_address_name': fields.related('partner_address_id', 'name', type='char', string='Partner Contact Name', readonly=True),
209 'partner_address_email': fields.related('partner_address_id', 'email', type='char', string='Partner Contact Email', readonly=True),
210 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
211 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
212 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
217 'active': lambda *a: 1,
218 'user_id': crm_case._get_default_user,
219 'email_from': crm_case._get_default_email,
220 'state': lambda *a: 'draft',
221 'type': lambda *a: 'lead',
222 'section_id': crm_case._get_section,
223 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
224 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
228 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
229 """This function returns value of partner email based on Partner Address
232 return {'value': {'email_from': False, 'country_id': False}}
233 address = self.pool.get('res.partner.address').browse(cr, uid, add)
234 return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
236 def on_change_optin(self, cr, uid, ids, optin):
237 return {'value':{'optin':optin,'optout':False}}
239 def on_change_optout(self, cr, uid, ids, optout):
240 return {'value':{'optout':optout,'optin':False}}
242 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
245 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
246 if not stage.on_change:
248 return {'value':{'probability': stage.probability}}
250 def stage_find_percent(self, cr, uid, percent, section_id):
251 """ Return the first stage with a probability == percent
253 stage_pool = self.pool.get('crm.case.stage')
255 ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
257 ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
263 def stage_find_lost(self, cr, uid, section_id):
264 return self.stage_find_percent(cr, uid, 0.0, section_id)
266 def stage_find_won(self, cr, uid, section_id):
267 return self.stage_find_percent(cr, uid, 100.0, section_id)
269 def case_open(self, cr, uid, ids, *args):
270 for l in self.browse(cr, uid, ids):
271 # When coming from draft override date and stage otherwise just set state
272 if l.state == 'draft':
274 message = _("The lead '%s' has been opened.") % l.name
275 elif l.type == 'opportunity':
276 message = _("The opportunity '%s' has been opened.") % l.name
278 message = _("The case '%s' has been opened.") % l.name
279 self.log(cr, uid, l.id, message)
280 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
281 self.write(cr, uid, [l.id], value)
282 if l.type == 'opportunity' and not l.stage_id:
283 stage_id = self.stage_find(cr, uid, l.section_id.id or False, [('sequence','>',0)])
285 self.stage_set(cr, uid, [l.id], stage_id)
286 res = super(crm_lead, self).case_open(cr, uid, ids, *args)
289 def case_close(self, cr, uid, ids, *args):
290 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
291 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
292 for case in self.browse(cr, uid, ids):
293 if case.type == 'lead':
294 message = _("The lead '%s' has been closed.") % case.name
296 message = _("The case '%s' has been closed.") % case.name
297 self.log(cr, uid, case.id, message)
300 def case_cancel(self, cr, uid, ids, *args):
301 """Overrides cancel for crm_case for setting probability
303 res = super(crm_lead, self).case_cancel(cr, uid, ids, args)
304 self.write(cr, uid, ids, {'probability' : 0.0})
307 def case_reset(self, cr, uid, ids, *args):
308 """Overrides reset as draft in order to set the stage field as empty
310 res = super(crm_lead, self).case_reset(cr, uid, ids, *args)
311 self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
314 def case_mark_lost(self, cr, uid, ids, *args):
315 """Mark the case as lost: state = done and probability = 0%
317 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
318 self.write(cr, uid, ids, {'probability' : 0.0})
319 for l in self.browse(cr, uid, ids):
320 stage_id = self.stage_find_lost(cr, uid, l.section_id.id or False)
322 self.stage_set(cr, uid, [l.id], stage_id)
323 message = _("The opportunity '%s' has been marked as lost.") % l.name
324 self.log(cr, uid, l.id, message)
327 def case_mark_won(self, cr, uid, ids, *args):
328 """Mark the case as lost: state = done and probability = 0%
330 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
331 self.write(cr, uid, ids, {'probability' : 100.0})
332 for l in self.browse(cr, uid, ids):
333 stage_id = self.stage_find_won(cr, uid, l.section_id.id or False)
335 self.stage_set(cr, uid, [l.id], stage_id)
336 message = _("The opportunity '%s' has been been won.") % l.name
337 self.log(cr, uid, l.id, message)
340 def set_priority(self, cr, uid, ids, priority):
343 return self.write(cr, uid, ids, {'priority' : priority})
345 def set_high_priority(self, cr, uid, ids, *args):
346 """Set lead priority to high
348 return self.set_priority(cr, uid, ids, '1')
350 def set_normal_priority(self, cr, uid, ids, *args):
351 """Set lead priority to normal
353 return self.set_priority(cr, uid, ids, '3')
356 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
357 # prepare opportunity data into dictionary for merging
358 opportunities = self.browse(cr, uid, ids, context=context)
359 def _get_first_not_null(attr):
360 if hasattr(oldest, attr):
361 return getattr(oldest, attr)
362 for opportunity in opportunities:
363 if hasattr(opportunity, attr):
364 return getattr(opportunity, attr)
367 def _get_first_not_null_id(attr):
368 res = _get_first_not_null(attr)
369 return res and res.id or False
371 def _concat_all(attr):
372 return ', '.join([getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)])
375 for field_name in fields:
376 field_info = self._all_columns.get(field_name)
377 if field_info is None:
379 field = field_info.column
380 if field._type in ('many2many', 'one2many'):
382 elif field._type == 'many2one':
383 data[field_name] = _get_first_not_null_id(field_name) # !!
384 elif field._type == 'text':
385 data[field_name] = _concat_all(field_name) #not lost
387 data[field_name] = _get_first_not_null(field_name) #not lost
390 def _merge_find_oldest(self, cr, uid, ids, context=None):
393 #TOCHECK: where pass 'convert' in context ?
394 if context.get('convert'):
395 ids = list(set(ids) - set(context.get('lead_ids', False)) )
397 #search opportunities order by create date
398 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
399 oldest_id = opportunity_ids[0]
400 return self.browse(cr, uid, oldest_id, context=context)
402 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
405 body.append("%s\n" % (title))
406 for field_name in fields:
407 field_info = self._all_columns.get(field_name)
408 if field_info is None:
410 field = field_info.column
413 if field._type == 'selection':
414 if hasattr(field.selection, '__call__'):
415 key = field.selection(self, cr, uid, context=context)
417 key = field.selection
418 value = dict(key).get(lead[field_name], lead[field_name])
419 elif field._type == 'many2one':
421 value = lead[field_name].name_get()[0][1]
423 value = lead[field_name]
425 body.append("%s: %s\n" % (field.string, value or ''))
426 return "\n".join(body + ['---'])
428 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
429 #TOFIX: mail template should be used instead of fix body, subject text
431 merge_message = _('Merged opportunities')
432 subject = [merge_message]
433 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
434 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
435 'country_id', 'city', 'street', 'street2', 'zip']
436 for opportunity in opportunities:
437 subject.append(opportunity.name)
438 title = "%s : %s" % (merge_message, opportunity.name)
439 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
441 subject = subject[0] + ", ".join(subject[1:])
442 details = "\n\n".join(details)
443 return self.message_append(cr, uid, [opportunity_id], subject, body_text=details, context=context)
445 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
446 message = self.pool.get('mail.message')
447 for opportunity in opportunities:
448 for history in opportunity.message_ids:
449 message.write(cr, uid, history.id, {
450 'res_id': opportunity_id,
451 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
456 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
457 attachment = self.pool.get('ir.attachment')
459 # return attachments of opportunity
460 def _get_attachments(opportunity_id):
461 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
462 return attachment.browse(cr, uid, attachment_ids, context=context)
465 first_attachments = _get_attachments(opportunity_id)
466 for opportunity in opportunities:
467 attachments = _get_attachments(opportunity.id)
468 for first in first_attachments:
469 for attachment in attachments:
470 if attachment.name == first.name:
472 name = "%s (%s)" % (attachment.name, count,),
473 res_id = opportunity_id,
475 attachment.write(values)
480 def merge_opportunity(self, cr, uid, ids, context=None):
482 To merge opportunities
483 :param ids: list of opportunities ids to merge
485 if context is None: context = {}
487 #TOCHECK: where pass lead_ids in context?
488 lead_ids = context and context.get('lead_ids', []) or []
491 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunities.'))
493 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
494 opportunities = self.browse(cr, uid, ids, context=context)
495 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
496 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
497 if ctx_opportunities :
498 first_opportunity = ctx_opportunities[0]
499 tail_opportunities = opportunities_list
501 first_opportunity = opportunities_list[0]
502 tail_opportunities = opportunities_list[1:]
504 fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id',
505 'partner_address_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
506 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
507 'date_action_next', 'email_from', 'email_cc', 'partner_name']
509 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
511 # merge data into first opportunity
512 self.write(cr, uid, [first_opportunity.id], data, context=context)
514 #copy message and attachements into the first opportunity
515 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
516 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
518 #Notification about loss of information
519 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
520 #delete tail opportunities
521 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
523 #open first opportunity
524 self.case_open(cr, uid, [first_opportunity.id])
525 return first_opportunity.id
527 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
528 crm_stage = self.pool.get('crm.case.stage')
531 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
533 section_id = lead.section_id and lead.section_id.id or False
535 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
537 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
538 stage_id = stage_ids and stage_ids[0] or False
540 'planned_revenue': lead.planned_revenue,
541 'probability': lead.probability,
543 'partner_id': customer and customer.id or False,
544 'user_id': (lead.user_id and lead.user_id.id),
545 'type': 'opportunity',
546 'stage_id': stage_id or False,
547 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
548 'partner_address_id': contact_id
550 def _convert_opportunity_notification(self, cr, uid, lead, context=None):
551 success_message = _("Lead '%s' has been converted to an opportunity.") % lead.name
552 self.message_append(cr, uid, [lead.id], success_message, body_text=success_message, context=context)
553 self.log(cr, uid, lead.id, success_message)
554 self._send_mail_to_salesman(cr, uid, lead, context=context)
557 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
558 partner = self.pool.get('res.partner')
559 mail_message = self.pool.get('mail.message')
562 customer = partner.browse(cr, uid, partner_id, context=context)
563 for lead in self.browse(cr, uid, ids, context=context):
564 if lead.state in ('done', 'cancel'):
566 if user_ids or section_id:
567 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
569 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
570 self.write(cr, uid, [lead.id], vals, context=context)
572 self._convert_opportunity_notification(cr, uid, lead, context=context)
573 #TOCHECK: why need to change partner details in all messages of lead ?
575 msg_ids = [ x.id for x in lead.message_ids]
576 mail_message.write(cr, uid, msg_ids, {
577 'partner_id': lead.partner_id.id
581 def _lead_create_partner(self, cr, uid, lead, context=None):
582 partner = self.pool.get('res.partner')
583 partner_id = partner.create(cr, uid, {
584 'name': lead.partner_name or lead.contact_name or lead.name,
585 'user_id': lead.user_id.id,
586 'comment': lead.description,
587 'section_id': lead.section_id.id or False,
592 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
594 res_partner = self.pool.get('res.partner')
596 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
597 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
598 res = lead.write({'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
602 def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
603 address = self.pool.get('res.partner.address')
604 return address.create(cr, uid, {
605 'partner_id': partner_id,
606 'name': lead.contact_name,
608 'mobile': lead.mobile,
609 'email': lead.email_from and to_email(lead.email_from)[0],
611 'title': lead.title and lead.title.id or False,
612 'function': lead.function,
613 'street': lead.street,
614 'street2': lead.street2,
617 'country_id': lead.country_id and lead.country_id.id or False,
618 'state_id': lead.state_id and lead.state_id.id or False,
621 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
623 This function convert partner based on action.
624 if action is 'create', create new partner with contact and assign lead to new partner_id.
625 otherwise assign lead to specified partner_id
630 for lead in self.browse(cr, uid, ids, context=context):
631 if action == 'create':
633 partner_id = self._lead_create_partner(cr, uid, lead, context=context)
634 self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
635 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
636 partner_ids[lead.id] = partner_id
639 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
641 Send mail to salesman with updated Lead details.
642 @ lead: browse record of 'crm.lead' object.
644 #TOFIX: mail template should be used here instead of fix subject, body text.
645 message = self.pool.get('mail.message')
646 email_to = lead.user_id and lead.user_id.user_email
650 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
651 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
652 subject = "lead %s converted into opportunity" % lead.name
653 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
654 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
657 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
662 value['section_id'] = team_id
663 if index < len(user_ids):
664 value['user_id'] = user_ids[index]
667 self.write(cr, uid, [lead_id], value, context=context)
670 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):
672 action :('schedule','Schedule a call'), ('log','Log a call')
674 phonecall = self.pool.get('crm.phonecall')
675 model_data = self.pool.get('ir.model.data')
678 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
680 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
681 for lead in self.browse(cr, uid, ids, context=context):
683 section_id = lead.section_id and lead.section_id.id or False
685 user_id = lead.user_id and lead.user_id.id or False
687 'name' : call_summary,
688 'opportunity_id' : lead.id,
689 'user_id' : user_id or False,
690 'categ_id' : categ_id or False,
691 'description' : desc or '',
692 'date' : schedule_time,
693 'section_id' : section_id or False,
694 'partner_id': lead.partner_id and lead.partner_id.id or False,
695 'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
696 'partner_phone' : phone or lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
697 'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
698 'priority': lead.priority,
701 new_id = phonecall.create(cr, uid, vals, context=context)
702 phonecall.case_open(cr, uid, [new_id])
704 phonecall.case_close(cr, uid, [new_id])
705 phonecall_dict[lead.id] = new_id
706 return phonecall_dict
709 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
710 models_data = self.pool.get('ir.model.data')
712 # Get Opportunity views
713 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
714 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
716 'name': _('Opportunity'),
718 'view_mode': 'tree, form',
719 'res_model': 'crm.lead',
720 'domain': [('type', '=', 'opportunity')],
721 'res_id': int(opportunity_id),
723 'views': [(form_view and form_view[1] or False, 'form'),
724 (tree_view and tree_view[1] or False, 'tree'),
725 (False, 'calendar'), (False, 'graph')],
726 'type': 'ir.actions.act_window',
730 def message_new(self, cr, uid, msg, custom_values=None, context=None):
731 """Automatically calls when new email message arrives"""
732 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
733 subject = msg.get('subject') or _("No Subject")
734 body = msg.get('body_text')
736 msg_from = msg.get('from')
737 priority = msg.get('priority')
740 'email_from': msg_from,
741 'email_cc': msg.get('cc'),
746 vals['priority'] = priority
747 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
748 self.write(cr, uid, [res_id], vals, context)
751 def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
752 if isinstance(ids, (str, int, long)):
755 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
757 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
758 vals['priority'] = msg.get('priority')
760 'cost':'planned_cost',
761 'revenue': 'planned_revenue',
762 'probability':'probability'
765 for line in msg['body_text'].split('\n'):
767 res = tools.misc.command_re.match(line)
768 if res and maps.get(res.group(1).lower()):
769 key = maps.get(res.group(1).lower())
770 vls[key] = res.group(2).lower()
773 # Unfortunately the API is based on lists
774 # but we want to update the state based on the
775 # previous state, so we have to loop:
776 for case in self.browse(cr, uid, ids, context=context):
778 if case.state in CRM_LEAD_PENDING_STATES:
779 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
780 res = self.write(cr, uid, [case.id], values, context=context)
783 def action_makeMeeting(self, cr, uid, ids, context=None):
785 This opens Meeting's calendar view to schedule meeting on current Opportunity
786 @return : Dictionary value for created Meeting view
791 data_obj = self.pool.get('ir.model.data')
792 for opp in self.browse(cr, uid, ids, context=context):
794 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
795 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
796 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
797 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
799 'default_opportunity_id': opp.id,
800 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
801 'default_user_id': uid,
802 'default_section_id': opp.section_id and opp.section_id.id or False,
803 'default_email_from': opp.email_from,
804 'default_state': 'open',
805 'default_name': opp.name
808 'name': _('Meetings'),
811 'view_mode': 'calendar,form,tree',
812 'res_model': 'crm.meeting',
814 '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')],
815 'type': 'ir.actions.act_window',
816 'search_view_id': search_view and search_view[1] or False,
822 def unlink(self, cr, uid, ids, context=None):
823 for lead in self.browse(cr, uid, ids, context):
824 if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
825 raise osv.except_osv(_('Warning !'),
826 _('You can not delete this lead. You should better cancel it.'))
827 return super(crm_lead, self).unlink(cr, uid, ids, context)
830 def write(self, cr, uid, ids, vals, context=None):
834 if 'date_closed' in vals:
835 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
837 if 'stage_id' in vals and vals['stage_id']:
838 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
839 text = _("Changed Stage to: %s") % stage_obj.name
840 self.message_append(cr, uid, ids, text, body_text=text, context=context)
842 for case in self.browse(cr, uid, ids, context=context):
843 if case.type == 'lead' or context.get('stage_type',False)=='lead':
844 message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
845 elif case.type == 'opportunity':
846 message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
847 self.log(cr, uid, case.id, message)
848 return super(crm_lead,self).write(cr, uid, ids, vals, context)
852 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: