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 # overridden because if 'base_contact' is installed - their default_get() will remove
72 # 'default_type' from context making it impossible to record an 'opportunity'
73 def default_get(self, cr, uid, fields_list, context=None):
74 return super(osv.osv, self).default_get(cr, uid, fields_list, context=context)
76 def _compute_day(self, cr, uid, ids, fields, args, context=None):
78 @param cr: the current row, from the database cursor,
79 @param uid: the current user’s ID for security checks,
80 @param ids: List of Openday’s IDs
81 @return: difference between current date and log date
82 @param context: A standard dictionary for contextual values
84 cal_obj = self.pool.get('resource.calendar')
85 res_obj = self.pool.get('resource.resource')
88 for lead in self.browse(cr, uid, ids, context=context):
93 if field == 'day_open':
95 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
96 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
97 ans = date_open - date_create
98 date_until = lead.date_open
99 elif field == 'day_close':
101 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
102 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
103 date_until = lead.date_closed
104 ans = date_close - date_create
108 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
109 if len(resource_ids):
110 resource_id = resource_ids[0]
112 duration = float(ans.days)
113 if lead.section_id and lead.section_id.resource_calendar_id:
114 duration = float(ans.days) * 24
115 new_dates = cal_obj.interval_get(cr,
117 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
118 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
123 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
124 for in_time, out_time in new_dates:
125 if in_time.date not in no_days:
126 no_days.append(in_time.date)
127 if out_time > date_until:
129 duration = len(no_days)
130 res[lead.id][field] = abs(int(duration))
133 def _history_search(self, cr, uid, obj, name, args, context=None):
135 msg_obj = self.pool.get('mail.message')
136 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
137 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
140 return [('id', 'in', lead_ids)]
142 return [('id', '=', '0')]
144 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
146 for obj in self.browse(cr, uid, ids, context=context):
148 for msg in obj.message_ids:
150 res[obj.id] = msg.subject
155 # Overridden from res.partner.address:
156 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
157 select=True, help="Optional linked partner, usually after conversion of the lead"),
159 'id': fields.integer('ID', readonly=True),
160 'name': fields.char('Name', size=64, select=1),
161 'active': fields.boolean('Active', required=False),
162 'date_action_last': fields.datetime('Last Action', readonly=1),
163 'date_action_next': fields.datetime('Next Action', readonly=1),
164 'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
165 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
166 select=True, help='When sending mails, the default email address is taken from the sales team.'),
167 'create_date': fields.datetime('Creation Date' , readonly=True),
168 '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"),
169 'description': fields.text('Notes'),
170 'write_date': fields.datetime('Update Date' , readonly=True),
172 'categ_id': fields.many2one('crm.case.categ', 'Category', \
173 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
174 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
175 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
176 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
177 'contact_name': fields.char('Contact Name', size=64),
178 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner that will be created while converting the lead into opportunity', select=1),
179 'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
180 'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
181 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
182 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
183 'date_closed': fields.datetime('Closed', readonly=True),
184 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
185 'user_id': fields.many2one('res.users', 'Salesman'),
186 'referred': fields.char('Referred By', size=64),
187 'date_open': fields.datetime('Opened', readonly=True),
188 'day_open': fields.function(_compute_day, string='Days to Open', \
189 multi='day_open', type="float", store=True),
190 'day_close': fields.function(_compute_day, string='Days to Close', \
191 multi='day_close', type="float", store=True),
192 'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
193 help='The state is set to \'Draft\', when a case is created.\
194 \nIf the case is in progress the state is set to \'Open\'.\
195 \nWhen the case is over, the state is set to \'Done\'.\
196 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
197 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
198 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
201 # Only used for type opportunity
202 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"),
203 'probability': fields.float('Probability (%)',group_operator="avg"),
204 'planned_revenue': fields.float('Expected Revenue'),
205 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
206 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
207 'phone': fields.char("Phone", size=64),
208 'date_deadline': fields.date('Expected Closing'),
209 'date_action': fields.date('Next Action Date', select=True),
210 'title_action': fields.char('Next Action', size=64),
211 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
212 'color': fields.integer('Color Index'),
213 'partner_address_name': fields.related('partner_address_id', 'name', type='char', string='Partner Contact Name', readonly=True),
214 'partner_address_email': fields.related('partner_address_id', 'email', type='char', string='Partner Contact Email', readonly=True),
215 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
216 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
217 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
222 'active': lambda *a: 1,
223 'user_id': crm_case._get_default_user,
224 'email_from': crm_case._get_default_email,
225 'state': lambda *a: 'draft',
226 'type': lambda *a: 'lead',
227 'section_id': crm_case._get_section,
228 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
229 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
233 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
234 """This function returns value of partner email based on Partner Address
237 return {'value': {'email_from': False, 'country_id': False}}
238 address = self.pool.get('res.partner.address').browse(cr, uid, add)
239 return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
241 def on_change_optin(self, cr, uid, ids, optin):
242 return {'value':{'optin':optin,'optout':False}}
244 def on_change_optout(self, cr, uid, ids, optout):
245 return {'value':{'optout':optout,'optin':False}}
247 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
250 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
251 if not stage.on_change:
253 return {'value':{'probability': stage.probability}}
255 def stage_find_percent(self, cr, uid, percent, section_id):
256 """ Return the first stage with a probability == percent
258 stage_pool = self.pool.get('crm.case.stage')
260 ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
262 ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
268 def stage_find_lost(self, cr, uid, section_id):
269 return self.stage_find_percent(cr, uid, 0.0, section_id)
271 def stage_find_won(self, cr, uid, section_id):
272 return self.stage_find_percent(cr, uid, 100.0, section_id)
274 def case_open(self, cr, uid, ids, *args):
275 for l in self.browse(cr, uid, ids):
276 # When coming from draft override date and stage otherwise just set state
277 if l.state == 'draft':
279 message = _("The lead '%s' has been opened.") % l.name
280 elif l.type == 'opportunity':
281 message = _("The opportunity '%s' has been opened.") % l.name
283 message = _("The case '%s' has been opened.") % l.name
284 self.log(cr, uid, l.id, message)
285 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
286 self.write(cr, uid, [l.id], value)
287 if l.type == 'opportunity' and not l.stage_id:
288 stage_id = self.stage_find(cr, uid, l.section_id.id or False, [('sequence','>',0)])
290 self.stage_set(cr, uid, [l.id], stage_id)
291 res = super(crm_lead, self).case_open(cr, uid, ids, *args)
294 def case_close(self, cr, uid, ids, *args):
295 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
296 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
297 for case in self.browse(cr, uid, ids):
298 if case.type == 'lead':
299 message = _("The lead '%s' has been closed.") % case.name
301 message = _("The case '%s' has been closed.") % case.name
302 self.log(cr, uid, case.id, message)
305 def case_cancel(self, cr, uid, ids, *args):
306 """Overrides cancel for crm_case for setting probability
308 res = super(crm_lead, self).case_cancel(cr, uid, ids, args)
309 self.write(cr, uid, ids, {'probability' : 0.0})
312 def case_reset(self, cr, uid, ids, *args):
313 """Overrides reset as draft in order to set the stage field as empty
315 res = super(crm_lead, self).case_reset(cr, uid, ids, *args)
316 self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
319 def case_mark_lost(self, cr, uid, ids, *args):
320 """Mark the case as lost: state = done and probability = 0%
322 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
323 self.write(cr, uid, ids, {'probability' : 0.0})
324 for l in self.browse(cr, uid, ids):
325 stage_id = self.stage_find_lost(cr, uid, l.section_id.id or False)
327 self.stage_set(cr, uid, [l.id], stage_id)
328 message = _("The opportunity '%s' has been marked as lost.") % l.name
329 self.log(cr, uid, l.id, message)
332 def case_mark_won(self, cr, uid, ids, *args):
333 """Mark the case as lost: state = done and probability = 0%
335 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
336 self.write(cr, uid, ids, {'probability' : 100.0})
337 for l in self.browse(cr, uid, ids):
338 stage_id = self.stage_find_won(cr, uid, l.section_id.id or False)
340 self.stage_set(cr, uid, [l.id], stage_id)
341 message = _("The opportunity '%s' has been been won.") % l.name
342 self.log(cr, uid, l.id, message)
345 def set_priority(self, cr, uid, ids, priority):
348 return self.write(cr, uid, ids, {'priority' : priority})
350 def set_high_priority(self, cr, uid, ids, *args):
351 """Set lead priority to high
353 return self.set_priority(cr, uid, ids, '1')
355 def set_normal_priority(self, cr, uid, ids, *args):
356 """Set lead priority to normal
358 return self.set_priority(cr, uid, ids, '3')
361 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
362 # prepare opportunity data into dictionary for merging
363 opportunities = self.browse(cr, uid, ids, context=context)
364 def _get_first_not_null(attr):
365 if hasattr(oldest, attr):
366 return getattr(oldest, attr)
367 for opportunity in opportunities:
368 if hasattr(opportunity, attr):
369 return getattr(opportunity, attr)
372 def _get_first_not_null_id(attr):
373 res = _get_first_not_null(attr)
374 return res and res.id or False
376 def _concat_all(attr):
377 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
380 for field_name in fields:
381 field_info = self._all_columns.get(field_name)
382 if field_info is None:
384 field = field_info.column
385 if field._type in ('many2many', 'one2many'):
387 elif field._type == 'many2one':
388 data[field_name] = _get_first_not_null_id(field_name) # !!
389 elif field._type == 'text':
390 data[field_name] = _concat_all(field_name) #not lost
392 data[field_name] = _get_first_not_null(field_name) #not lost
395 def _merge_find_oldest(self, cr, uid, ids, context=None):
398 #TOCHECK: where pass 'convert' in context ?
399 if context.get('convert'):
400 ids = list(set(ids) - set(context.get('lead_ids', False)) )
402 #search opportunities order by create date
403 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
404 oldest_id = opportunity_ids[0]
405 return self.browse(cr, uid, oldest_id, context=context)
407 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
410 body.append("%s\n" % (title))
411 for field_name in fields:
412 field_info = self._all_columns.get(field_name)
413 if field_info is None:
415 field = field_info.column
418 if field._type == 'selection':
419 if hasattr(field.selection, '__call__'):
420 key = field.selection(self, cr, uid, context=context)
422 key = field.selection
423 value = dict(key).get(lead[field_name], lead[field_name])
424 elif field._type == 'many2one':
426 value = lead[field_name].name_get()[0][1]
428 value = lead[field_name]
430 body.append("%s: %s" % (field.string, value or ''))
431 return "\n".join(body + ['---'])
433 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
434 #TOFIX: mail template should be used instead of fix body, subject text
436 merge_message = _('Merged opportunities')
437 subject = [merge_message]
438 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
439 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
440 'country_id', 'city', 'street', 'street2', 'zip']
441 for opportunity in opportunities:
442 subject.append(opportunity.name)
443 title = "%s : %s" % (merge_message, opportunity.name)
444 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
446 subject = subject[0] + ", ".join(subject[1:])
447 details = "\n\n".join(details)
448 return self.message_append(cr, uid, [opportunity_id], subject, body_text=details, context=context)
450 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
451 message = self.pool.get('mail.message')
452 for opportunity in opportunities:
453 for history in opportunity.message_ids:
454 message.write(cr, uid, history.id, {
455 'res_id': opportunity_id,
456 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
461 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
462 attachment = self.pool.get('ir.attachment')
464 # return attachments of opportunity
465 def _get_attachments(opportunity_id):
466 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
467 return attachment.browse(cr, uid, attachment_ids, context=context)
470 first_attachments = _get_attachments(opportunity_id)
471 for opportunity in opportunities:
472 attachments = _get_attachments(opportunity.id)
473 for first in first_attachments:
474 for attachment in attachments:
475 if attachment.name == first.name:
477 name = "%s (%s)" % (attachment.name, count,),
478 res_id = opportunity_id,
480 attachment.write(values)
485 def merge_opportunity(self, cr, uid, ids, context=None):
487 To merge opportunities
488 :param ids: list of opportunities ids to merge
490 if context is None: context = {}
492 #TOCHECK: where pass lead_ids in context?
493 lead_ids = context and context.get('lead_ids', []) or []
496 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
498 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
499 opportunities = self.browse(cr, uid, ids, context=context)
500 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
501 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
502 if ctx_opportunities :
503 first_opportunity = ctx_opportunities[0]
504 tail_opportunities = opportunities_list + ctx_opportunities[1:]
506 first_opportunity = opportunities_list[0]
507 tail_opportunities = opportunities_list[1:]
509 fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id',
510 'partner_address_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
511 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
512 'date_action_next', 'email_from', 'email_cc', 'partner_name']
514 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
516 # merge data into first opportunity
517 self.write(cr, uid, [first_opportunity.id], data, context=context)
519 #copy message and attachements into the first opportunity
520 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
521 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
523 #Notification about loss of information
524 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
525 #delete tail opportunities
526 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
528 #open first opportunity
529 self.case_open(cr, uid, [first_opportunity.id])
530 return first_opportunity.id
532 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
533 crm_stage = self.pool.get('crm.case.stage')
536 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
538 section_id = lead.section_id and lead.section_id.id or False
540 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
542 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
543 stage_id = stage_ids and stage_ids[0] or False
545 'planned_revenue': lead.planned_revenue,
546 'probability': lead.probability,
548 'partner_id': customer and customer.id or False,
549 'user_id': (lead.user_id and lead.user_id.id),
550 'type': 'opportunity',
551 'stage_id': stage_id or False,
552 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
553 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
554 'partner_address_id': contact_id,
557 def _convert_opportunity_notification(self, cr, uid, lead, context=None):
558 success_message = _("Lead '%s' has been converted to an opportunity.") % lead.name
559 self.message_append(cr, uid, [lead.id], success_message, body_text=success_message, context=context)
560 self.log(cr, uid, lead.id, success_message)
561 self._send_mail_to_salesman(cr, uid, lead, context=context)
564 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
565 partner = self.pool.get('res.partner')
566 mail_message = self.pool.get('mail.message')
569 customer = partner.browse(cr, uid, partner_id, context=context)
570 for lead in self.browse(cr, uid, ids, context=context):
571 if lead.state in ('done', 'cancel'):
574 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
575 self.write(cr, uid, [lead.id], vals, context=context)
576 self._convert_opportunity_notification(cr, uid, lead, context=context)
577 self.case_open(cr, uid, [lead.id])
578 #TOCHECK: why need to change partner details in all messages of lead ?
580 msg_ids = [ x.id for x in lead.message_ids]
581 mail_message.write(cr, uid, msg_ids, {
582 'partner_id': lead.partner_id.id
585 if user_ids or section_id:
586 self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
590 def _lead_create_partner(self, cr, uid, lead, context=None):
591 partner = self.pool.get('res.partner')
592 partner_id = partner.create(cr, uid, {
593 'name': lead.partner_name or lead.contact_name or lead.name,
594 'user_id': lead.user_id.id,
595 'comment': lead.description,
596 'section_id': lead.section_id.id or False,
601 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
603 res_partner = self.pool.get('res.partner')
605 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
606 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
607 res = lead.write({'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
611 def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
612 address = self.pool.get('res.partner.address')
613 return address.create(cr, uid, {
614 'partner_id': partner_id,
615 'name': lead.contact_name,
617 'mobile': lead.mobile,
618 'email': lead.email_from and to_email(lead.email_from)[0],
620 'title': lead.title and lead.title.id or False,
621 'function': lead.function,
622 'street': lead.street,
623 'street2': lead.street2,
626 'country_id': lead.country_id and lead.country_id.id or False,
627 'state_id': lead.state_id and lead.state_id.id or False,
630 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
632 This function convert partner based on action.
633 if action is 'create', create new partner with contact and assign lead to new partner_id.
634 otherwise assign lead to specified partner_id
639 force_partner_id = partner_id
640 for lead in self.browse(cr, uid, ids, context=context):
641 if action == 'create':
642 partner_id = force_partner_id or self._lead_create_partner(cr, uid, lead, context=context)
643 self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
644 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
645 partner_ids[lead.id] = partner_id
648 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
650 Send mail to salesman with updated Lead details.
651 @ lead: browse record of 'crm.lead' object.
653 #TOFIX: mail template should be used here instead of fix subject, body text.
654 message = self.pool.get('mail.message')
655 email_to = lead.user_id and lead.user_id.user_email
659 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
660 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
661 subject = "lead %s converted into opportunity" % lead.name
662 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
663 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
666 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
671 value['section_id'] = team_id
673 value['user_id'] = user_ids[index]
674 index = (index + 1) % len(user_ids)
676 self.write(cr, uid, [lead_id], value, context=context)
679 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):
681 action :('schedule','Schedule a call'), ('log','Log a call')
683 phonecall = self.pool.get('crm.phonecall')
684 model_data = self.pool.get('ir.model.data')
687 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
689 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
690 for lead in self.browse(cr, uid, ids, context=context):
692 section_id = lead.section_id and lead.section_id.id or False
694 user_id = lead.user_id and lead.user_id.id or False
696 'name' : call_summary,
697 'opportunity_id' : lead.id,
698 'user_id' : user_id or False,
699 'categ_id' : categ_id or False,
700 'description' : desc or '',
701 'date' : schedule_time,
702 'section_id' : section_id or False,
703 'partner_id': lead.partner_id and lead.partner_id.id or False,
704 'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
705 'partner_phone' : phone or lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
706 'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
707 'priority': lead.priority,
710 new_id = phonecall.create(cr, uid, vals, context=context)
711 phonecall.case_open(cr, uid, [new_id])
713 phonecall.case_close(cr, uid, [new_id])
714 phonecall_dict[lead.id] = new_id
715 return phonecall_dict
718 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
719 models_data = self.pool.get('ir.model.data')
721 # Get Opportunity views
722 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
723 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
725 'name': _('Opportunity'),
727 'view_mode': 'tree, form',
728 'res_model': 'crm.lead',
729 'domain': [('type', '=', 'opportunity')],
730 'res_id': int(opportunity_id),
732 'views': [(form_view and form_view[1] or False, 'form'),
733 (tree_view and tree_view[1] or False, 'tree'),
734 (False, 'calendar'), (False, 'graph')],
735 'type': 'ir.actions.act_window',
739 def message_new(self, cr, uid, msg, custom_values=None, context=None):
740 """Automatically calls when new email message arrives"""
741 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
742 subject = msg.get('subject') or _("No Subject")
743 body = msg.get('body_text')
745 msg_from = msg.get('from')
746 priority = msg.get('priority')
749 'email_from': msg_from,
750 'email_cc': msg.get('cc'),
755 vals['priority'] = priority
756 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
757 self.write(cr, uid, [res_id], vals, context)
760 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
761 if isinstance(ids, (str, int, long)):
765 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
767 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
768 vals['priority'] = msg.get('priority')
770 'cost':'planned_cost',
771 'revenue': 'planned_revenue',
772 'probability':'probability'
775 for line in msg['body_text'].split('\n'):
777 res = tools.misc.command_re.match(line)
778 if res and maps.get(res.group(1).lower()):
779 key = maps.get(res.group(1).lower())
780 vls[key] = res.group(2).lower()
783 # Unfortunately the API is based on lists
784 # but we want to update the state based on the
785 # previous state, so we have to loop:
786 for case in self.browse(cr, uid, ids, context=context):
788 if case.state in CRM_LEAD_PENDING_STATES:
790 values.update(state=crm.AVAILABLE_STATES[1][0])
791 if not case.date_open:
792 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
793 res = self.write(cr, uid, [case.id], values, context=context)
796 def action_makeMeeting(self, cr, uid, ids, context=None):
798 This opens Meeting's calendar view to schedule meeting on current Opportunity
799 @return : Dictionary value for created Meeting view
804 data_obj = self.pool.get('ir.model.data')
805 for opp in self.browse(cr, uid, ids, context=context):
807 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
808 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
809 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
810 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
812 'default_opportunity_id': opp.id,
813 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
814 'default_user_id': uid,
815 'default_section_id': opp.section_id and opp.section_id.id or False,
816 'default_email_from': opp.email_from,
817 'default_state': 'open',
818 'default_name': opp.name
821 'name': _('Meetings'),
824 'view_mode': 'calendar,form,tree',
825 'res_model': 'crm.meeting',
827 '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')],
828 'type': 'ir.actions.act_window',
829 'search_view_id': search_view and search_view[1] or False,
835 def unlink(self, cr, uid, ids, context=None):
836 for lead in self.browse(cr, uid, ids, context):
837 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
838 raise osv.except_osv(_('Error'),
839 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
840 "You should better cancel it, instead of deleting it.") % lead.name)
841 return super(crm_lead, self).unlink(cr, uid, ids, context)
844 def write(self, cr, uid, ids, vals, context=None):
848 if 'date_closed' in vals:
849 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
851 if vals.get('stage_id'):
852 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
853 # change probability of lead(s) if required by stage
854 if not vals.get('probability') and stage.on_change:
855 vals['probability'] = stage.probability
856 text = _("Changed Stage to: %s") % stage.name
857 self.message_append(cr, uid, ids, text, body_text=text, context=context)
858 for case in self.browse(cr, uid, ids, context=context):
859 if case.type == 'lead' or context.get('stage_type') == 'lead':
860 message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage.name)
861 self.log(cr, uid, case.id, message)
862 elif case.type == 'opportunity':
863 message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage.name)
864 self.log(cr, uid, case.id, message)
866 return super(crm_lead,self).write(cr, uid, ids, vals, context)
870 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: