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 = ['ir.needaction_mixin', 'mail.thread','res.partner']
45 def _get_default_section_id(self, cr, uid, context=None):
46 """ Gives default section by checking if present in the context """
47 return self._resolve_section_id_from_context(cr, uid, context=context)
49 def _resolve_section_id_from_context(self, cr, uid, context=None):
50 """ Returns ID of section based on the value of 'section_id'
51 context key, or None if it cannot be resolved to a single
56 if type(context.get('default_section_id')) in (int, long):
57 return context.get('default_section_id')
58 if isinstance(context.get('default_section_id'), basestring):
59 section_name = context['default_section_id']
60 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=section_name, context=context)
61 if len(section_ids) == 1:
62 return int(section_ids[0][0])
65 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
66 access_rights_uid = access_rights_uid or uid
67 stage_obj = self.pool.get('crm.case.stage')
68 order = stage_obj._order
69 # lame hack to allow reverting search, should just work in the trivial case
70 if read_group_order == 'stage_id desc':
71 order = "%s desc" % order
72 # retrieve section_id from the context and write the domain
74 section_id = self._resolve_section_id_from_context(cr, uid, context=context)
76 search_domain += ['|', '&', ('section_ids', '=', section_id), ('fold', '=', True)]
77 search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', 1), ('fold', '=', False)]
79 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
80 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
81 # restore order of the search
82 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
86 'stage_id': _read_group_stage_ids
89 def _compute_day(self, cr, uid, ids, fields, args, context=None):
91 @param cr: the current row, from the database cursor,
92 @param uid: the current user’s ID for security checks,
93 @param ids: List of Openday’s IDs
94 @return: difference between current date and log date
95 @param context: A standard dictionary for contextual values
97 cal_obj = self.pool.get('resource.calendar')
98 res_obj = self.pool.get('resource.resource')
101 for lead in self.browse(cr, uid, ids, context=context):
106 if field == 'day_open':
108 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
109 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
110 ans = date_open - date_create
111 date_until = lead.date_open
112 elif field == 'day_close':
114 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
115 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
116 date_until = lead.date_closed
117 ans = date_close - date_create
121 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
122 if len(resource_ids):
123 resource_id = resource_ids[0]
125 duration = float(ans.days)
126 if lead.section_id and lead.section_id.resource_calendar_id:
127 duration = float(ans.days) * 24
128 new_dates = cal_obj.interval_get(cr,
130 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
131 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
136 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
137 for in_time, out_time in new_dates:
138 if in_time.date not in no_days:
139 no_days.append(in_time.date)
140 if out_time > date_until:
142 duration = len(no_days)
143 res[lead.id][field] = abs(int(duration))
146 def _history_search(self, cr, uid, obj, name, args, context=None):
148 msg_obj = self.pool.get('mail.message')
149 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
150 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
153 return [('id', 'in', lead_ids)]
155 return [('id', '=', '0')]
157 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
159 for obj in self.browse(cr, uid, ids, context=context):
161 for msg in obj.message_ids:
163 res[obj.id] = msg.subject
168 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
169 select=True, help="Optional linked partner, usually after conversion of the lead"),
171 'id': fields.integer('ID', readonly=True),
172 'name': fields.char('Name', size=64, select=1),
173 'active': fields.boolean('Active', required=False),
174 'date_action_last': fields.datetime('Last Action', readonly=1),
175 'date_action_next': fields.datetime('Next Action', readonly=1),
176 'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
177 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
178 select=True, help='When sending mails, the default email address is taken from the sales team.'),
179 'create_date': fields.datetime('Creation Date' , readonly=True),
180 '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"),
181 'description': fields.text('Notes'),
182 'write_date': fields.datetime('Update Date' , readonly=True),
183 'categ_id': fields.many2one('crm.case.categ', 'Category', \
184 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
185 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
186 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
187 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
188 'contact_name': fields.char('Contact Name', size=64),
189 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner company that will be created while converting the lead into opportunity', select=1),
190 'opt_in': fields.boolean('Opt-In', oldname='optin', help="If opt-in is checked, this contact has accepted to receive emails."),
191 'opt_out': fields.boolean('Opt-Out', oldname='optout', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
192 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
193 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
194 'date_closed': fields.datetime('Closed', readonly=True),
195 'stage_id': fields.many2one('crm.case.stage', 'Stage',
196 domain="['&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
197 'user_id': fields.many2one('res.users', 'Salesperson'),
198 'referred': fields.char('Referred By', size=64),
199 'date_open': fields.datetime('Opened', readonly=True),
200 'day_open': fields.function(_compute_day, string='Days to Open', \
201 multi='day_open', type="float", store=True),
202 'day_close': fields.function(_compute_day, string='Days to Close', \
203 multi='day_close', type="float", store=True),
204 'state': fields.related('stage_id', 'state', type="selection", store=True,
205 selection=crm.AVAILABLE_STATES, string="State", readonly=True,
206 help='The state is set to \'Draft\', when a case is created.\
207 If the case is in progress the state is set to \'Open\'.\
208 When the case is over, the state is set to \'Done\'.\
209 If the case needs to be reviewed then the state is \
210 set to \'Pending\'.'),
211 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
212 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
214 # Only used for type opportunity
215 'probability': fields.float('Success Rate (%)',group_operator="avg"),
216 'planned_revenue': fields.float('Expected Revenue'),
217 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
218 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
219 'phone': fields.char("Phone", size=64),
220 'date_deadline': fields.date('Expected Closing'),
221 'date_action': fields.date('Next Action Date', select=True),
222 'title_action': fields.char('Next Action', size=64),
223 'color': fields.integer('Color Index'),
224 'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
225 'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
226 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
227 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
228 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
234 'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
235 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
237 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
238 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
239 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
240 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
244 def get_needaction_user_ids(self, cr, uid, ids, context=None):
245 result = dict.fromkeys(ids, [])
246 for obj in self.browse(cr, uid, ids, context=context):
247 # salesman must perform an action when in draft mode
248 if obj.state == 'draft' and obj.user_id:
249 result[obj.id] = [obj.user_id.id]
252 def create(self, cr, uid, vals, context=None):
253 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
254 self.create_send_note(cr, uid, [obj_id], context=context)
257 def on_change_opt_in(self, cr, uid, ids, opt_in):
258 return {'value':{'opt_in':opt_in,'opt_out':False}}
260 def on_change_opt_out(self, cr, uid, ids, opt_out):
261 return {'value':{'opt_out':opt_out,'opt_in':False}}
263 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
266 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
267 if not stage.on_change:
269 return {'value':{'probability': stage.probability}}
271 def _get_default_stage_id(self, cr, uid, context=None):
272 """ Gives default stage_id """
273 section_id = self._get_default_section_id(cr, uid, context=context)
274 return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft'), ('type', '=', 'both')], context=context)
276 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
277 """ Override of the base.stage method
278 Parameter of the stage search taken from the lead:
279 - type: stage type must be the same or 'both'
280 - section_id: if set, stages must belong to this section or
283 if isinstance(cases, (int, long)):
284 cases = self.browse(cr, uid, cases, context=context)
285 domain = list(domain)
287 domain += ['|', ('section_ids', '=', section_id), ('case_default', '=', True)]
289 lead_section_id = lead.section_id.id if lead.section_id else None
290 domain += ['|', ('type', '=', lead.type), ('type', '=', 'both')]
292 domain += ['|', ('section_ids', '=', lead_section_id), ('case_default', '=', True)]
293 stage_ids = self.pool.get('crm.case.stage').search(cr, uid, domain, order=order, context=context)
298 def case_cancel(self, cr, uid, ids, context=None):
299 """Overrides cancel for crm_case for setting probability
301 res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
302 self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
305 def case_reset(self, cr, uid, ids, context=None):
306 """Overrides reset as draft in order to set the stage field as empty
308 res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
309 self.write(cr, uid, ids, {'probability': 0.0}, context=context)
312 def case_mark_lost(self, cr, uid, ids, context=None):
313 """Mark the case as lost: state = done and probability = 0%
315 for lead in self.browse(cr, uid, ids):
316 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
318 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
319 self.case_mark_lost_send_note(cr, uid, ids, context=context)
322 def case_mark_won(self, cr, uid, ids, context=None):
323 """Mark the case as lost: state = done and probability = 0%
325 for lead in self.browse(cr, uid, ids):
326 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
328 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
329 self.case_mark_won_send_note(cr, uid, ids, context=context)
332 def set_priority(self, cr, uid, ids, priority):
335 return self.write(cr, uid, ids, {'priority' : priority})
337 def set_high_priority(self, cr, uid, ids, context=None):
338 """Set lead priority to high
340 return self.set_priority(cr, uid, ids, '1')
342 def set_normal_priority(self, cr, uid, ids, context=None):
343 """Set lead priority to normal
345 return self.set_priority(cr, uid, ids, '3')
348 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
349 # prepare opportunity data into dictionary for merging
350 opportunities = self.browse(cr, uid, ids, context=context)
351 def _get_first_not_null(attr):
352 if hasattr(oldest, attr):
353 return getattr(oldest, attr)
354 for opportunity in opportunities:
355 if hasattr(opportunity, attr):
356 return getattr(opportunity, attr)
359 def _get_first_not_null_id(attr):
360 res = _get_first_not_null(attr)
361 return res and res.id or False
363 def _concat_all(attr):
364 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
367 for field_name in fields:
368 field_info = self._all_columns.get(field_name)
369 if field_info is None:
371 field = field_info.column
372 if field._type in ('many2many', 'one2many'):
374 elif field._type == 'many2one':
375 data[field_name] = _get_first_not_null_id(field_name) # !!
376 elif field._type == 'text':
377 data[field_name] = _concat_all(field_name) #not lost
379 data[field_name] = _get_first_not_null(field_name) #not lost
382 def _merge_find_oldest(self, cr, uid, ids, context=None):
385 #TOCHECK: where pass 'convert' in context ?
386 if context.get('convert'):
387 ids = list(set(ids) - set(context.get('lead_ids', False)) )
389 #search opportunities order by create date
390 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
391 oldest_id = opportunity_ids[0]
392 return self.browse(cr, uid, oldest_id, context=context)
394 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
397 body.append("%s\n" % (title))
398 for field_name in fields:
399 field_info = self._all_columns.get(field_name)
400 if field_info is None:
402 field = field_info.column
405 if field._type == 'selection':
406 if hasattr(field.selection, '__call__'):
407 key = field.selection(self, cr, uid, context=context)
409 key = field.selection
410 value = dict(key).get(lead[field_name], lead[field_name])
411 elif field._type == 'many2one':
413 value = lead[field_name].name_get()[0][1]
415 value = lead[field_name]
417 body.append("%s: %s" % (field.string, value or ''))
418 return "\n".join(body + ['---'])
420 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
421 #TOFIX: mail template should be used instead of fix body, subject text
423 merge_message = _('Merged opportunities')
424 subject = [merge_message]
425 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
426 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
427 'country_id', 'city', 'street', 'street2', 'zip']
428 for opportunity in opportunities:
429 subject.append(opportunity.name)
430 title = "%s : %s" % (merge_message, opportunity.name)
431 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
433 subject = subject[0] + ", ".join(subject[1:])
434 details = "\n\n".join(details)
435 return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
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 opportunity from the list view.'))
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 fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
497 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
498 'date_action_next', 'email_from', 'email_cc', 'partner_name']
500 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
502 # merge data into first opportunity
503 self.write(cr, uid, [first_opportunity.id], data, context=context)
505 #copy message and attachements into the first opportunity
506 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
507 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
509 #Notification about loss of information
510 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
511 #delete tail opportunities
512 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
514 #open first opportunity
515 self.case_open(cr, uid, [first_opportunity.id])
516 return first_opportunity.id
518 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
519 crm_stage = self.pool.get('crm.case.stage')
522 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
524 section_id = lead.section_id and lead.section_id.id or False
526 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
528 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
529 stage_id = stage_ids and stage_ids[0] or False
531 'planned_revenue': lead.planned_revenue,
532 'probability': lead.probability,
534 'partner_id': customer and customer.id or False,
535 'user_id': (lead.user_id and lead.user_id.id),
536 'type': 'opportunity',
537 'stage_id': stage_id or False,
538 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
539 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
542 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
543 partner = self.pool.get('res.partner')
544 mail_message = self.pool.get('mail.message')
547 customer = partner.browse(cr, uid, partner_id, context=context)
548 for lead in self.browse(cr, uid, ids, context=context):
549 if lead.state in ('done', 'cancel'):
551 if user_ids or section_id:
552 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
554 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
555 self.write(cr, uid, [lead.id], vals, context=context)
557 self.convert_opportunity_send_note(cr, uid, lead, context=context)
558 #TOCHECK: why need to change partner details in all messages of lead ?
560 msg_ids = [ x.id for x in lead.message_ids]
561 mail_message.write(cr, uid, msg_ids, {
562 'partner_id': lead.partner_id.id
566 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
567 partner = self.pool.get('res.partner')
568 vals = { 'name': name,
569 'user_id': lead.user_id.id,
570 'comment': lead.description,
571 'section_id': lead.section_id.id or False,
572 'parent_id': parent_id,
574 'mobile': lead.mobile,
575 'email': lead.email_from and to_email(lead.email_from)[0],
577 'title': lead.title and lead.title.id or False,
578 'function': lead.function,
579 'street': lead.street,
580 'street2': lead.street2,
583 'country_id': lead.country_id and lead.country_id.id or False,
584 'state_id': lead.state_id and lead.state_id.id or False,
585 'is_company': is_company,
588 partner = partner.create(cr, uid,vals, context)
591 def _create_lead_partner(self, cr, uid, lead, context=None):
593 if lead.partner_name and lead.contact_name:
594 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
595 self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
596 elif lead.partner_name and not lead.contact_name:
597 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
598 elif not lead.partner_name and lead.contact_name:
599 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
601 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
604 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
606 res_partner = self.pool.get('res.partner')
608 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
609 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
610 res = lead.write({'partner_id' : partner_id, }, context=context)
611 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
614 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
616 This function convert partner based on action.
617 if action is 'create', create new partner with contact and assign lead to new partner_id.
618 otherwise assign lead to specified partner_id
623 for lead in self.browse(cr, uid, ids, context=context):
624 if action == 'create':
626 partner_id = self._create_lead_partner(cr, uid, lead, context)
627 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
628 partner_ids[lead.id] = partner_id
631 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
633 Send mail to salesman with updated Lead details.
634 @ lead: browse record of 'crm.lead' object.
636 #TOFIX: mail template should be used here instead of fix subject, body text.
637 message = self.pool.get('mail.message')
638 email_to = lead.user_id and lead.user_id.user_email
642 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
643 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
644 subject = "lead %s converted into opportunity" % lead.name
645 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
646 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
649 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
654 value['section_id'] = team_id
655 if index < len(user_ids):
656 value['user_id'] = user_ids[index]
659 self.write(cr, uid, [lead_id], value, context=context)
662 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):
664 action :('schedule','Schedule a call'), ('log','Log a call')
666 phonecall = self.pool.get('crm.phonecall')
667 model_data = self.pool.get('ir.model.data')
670 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
672 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
673 for lead in self.browse(cr, uid, ids, context=context):
675 section_id = lead.section_id and lead.section_id.id or False
677 user_id = lead.user_id and lead.user_id.id or False
679 'name' : call_summary,
680 'opportunity_id' : lead.id,
681 'user_id' : user_id or False,
682 'categ_id' : categ_id or False,
683 'description' : desc or '',
684 'date' : schedule_time,
685 'section_id' : section_id or False,
686 'partner_id': lead.partner_id and lead.partner_id.id or False,
687 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
688 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
689 'priority': lead.priority,
691 new_id = phonecall.create(cr, uid, vals, context=context)
692 phonecall.case_open(cr, uid, [new_id], context=context)
694 phonecall.case_close(cr, uid, [new_id], context=context)
695 phonecall_dict[lead.id] = new_id
696 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
697 return phonecall_dict
700 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
701 models_data = self.pool.get('ir.model.data')
703 # Get Opportunity views
704 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
705 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
707 'name': _('Opportunity'),
709 'view_mode': 'tree, form',
710 'res_model': 'crm.lead',
711 'domain': [('type', '=', 'opportunity')],
712 'res_id': int(opportunity_id),
714 'views': [(form_view and form_view[1] or False, 'form'),
715 (tree_view and tree_view[1] or False, 'tree'),
716 (False, 'calendar'), (False, 'graph')],
717 'type': 'ir.actions.act_window',
721 def message_new(self, cr, uid, msg, custom_values=None, context=None):
722 """Automatically calls when new email message arrives"""
723 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
724 subject = msg.get('subject') or _("No Subject")
725 body = msg.get('body_text')
727 msg_from = msg.get('from')
728 priority = msg.get('priority')
731 'email_from': msg_from,
732 'email_cc': msg.get('cc'),
737 vals['priority'] = priority
738 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
739 self.write(cr, uid, [res_id], vals, context)
742 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
743 if isinstance(ids, (str, int, long)):
747 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
749 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
750 vals['priority'] = msg.get('priority')
752 'cost':'planned_cost',
753 'revenue': 'planned_revenue',
754 'probability':'probability'
757 for line in msg['body_text'].split('\n'):
759 res = tools.misc.command_re.match(line)
760 if res and maps.get(res.group(1).lower()):
761 key = maps.get(res.group(1).lower())
762 vls[key] = res.group(2).lower()
765 # Unfortunately the API is based on lists
766 # but we want to update the state based on the
767 # previous state, so we have to loop:
768 for case in self.browse(cr, uid, ids, context=context):
770 if case.state in CRM_LEAD_PENDING_STATES:
772 values.update(state=crm.AVAILABLE_STATES[1][0])
773 if not case.date_open:
774 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
775 res = self.write(cr, uid, [case.id], values, context=context)
778 def action_makeMeeting(self, cr, uid, ids, context=None):
780 This opens Meeting's calendar view to schedule meeting on current Opportunity
781 @return : Dictionary value for created Meeting view
786 data_obj = self.pool.get('ir.model.data')
787 for opp in self.browse(cr, uid, ids, context=context):
789 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
790 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
791 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
792 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
794 'default_opportunity_id': opp.id,
795 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
796 'default_user_id': uid,
797 'default_section_id': opp.section_id and opp.section_id.id or False,
798 'default_email_from': opp.email_from,
799 'default_state': 'open',
800 'default_name': opp.name
803 'name': _('Meetings'),
806 'view_mode': 'calendar,form,tree',
807 'res_model': 'crm.meeting',
809 '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')],
810 'type': 'ir.actions.act_window',
811 'search_view_id': search_view and search_view[1] or False,
817 def unlink(self, cr, uid, ids, context=None):
818 for lead in self.browse(cr, uid, ids, context):
819 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
820 raise osv.except_osv(_('Error'),
821 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
822 "You should better cancel it, instead of deleting it.") % lead.name)
823 return super(crm_lead, self).unlink(cr, uid, ids, context)
826 def write(self, cr, uid, ids, vals, context=None):
830 if 'date_closed' in vals:
831 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
833 if vals.get('stage_id'):
834 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
835 # change probability of lead(s) if required by stage
836 if not vals.get('probability') and stage.on_change:
837 vals['probability'] = stage.probability
838 for case in self.browse(cr, uid, ids, context=context):
839 message = _("Stage changed to <b>%s</b>.") % (stage.name)
840 case.message_append_note(body=message)
841 return super(crm_lead,self).write(cr, uid, ids, vals, context)
843 # ----------------------------------------
844 # OpenChatter methods and notifications
845 # ----------------------------------------
847 def message_get_subscribers(self, cr, uid, ids, context=None):
848 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
849 # add salesman to the subscribers
850 for obj in self.browse(cr, uid, ids, context=context):
852 sub_ids.append(obj.user_id.id)
853 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
855 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
856 if isinstance(lead, (int, long)):
857 lead = self.browse(cr, uid, [lead], context=context)[0]
858 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
860 def create_send_note(self, cr, uid, ids, context=None):
862 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
863 self.message_append_note(cr, uid, [id], body=message, context=context)
866 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
867 message = _("Opportunity has been <b>lost</b>.")
868 return self.message_append_note(cr, uid, ids, body=message, context=context)
870 def case_mark_won_send_note(self, cr, uid, ids, context=None):
871 message = _("Opportunity has been <b>won</b>.")
872 return self.message_append_note(cr, uid, ids, body=message, context=context)
874 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
875 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
876 if action == 'log': prefix = 'Logged'
877 else: prefix = 'Scheduled'
878 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
879 return self. message_append_note(cr, uid, ids, body=message, context=context)
881 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
882 for lead in self.browse(cr, uid, ids, context=context):
883 message = _("%s <b>partner</b> is now set to <em>%s</em>." % (self.case_get_note_msg_prefix(cr, uid, lead, context=context), lead.partner_id.name))
884 lead.message_append_note(body=message)
887 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
888 message = _("Lead has been <b>converted to an opportunity</b>.")
889 lead.message_append_note(body=message)
894 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: