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
37 class crm_lead(crm_case, osv.osv):
40 _description = "Lead/Opportunity"
41 _order = "priority,date_action,id desc"
42 _inherit = ['ir.needaction_mixin', 'mail.thread','res.partner']
44 def _resolve_section_id_from_context(self, cr, uid, context=None):
45 """ Returns ID of section based on the value of 'section_id'
46 context key, or None if it cannot be resolved to a single project
50 if type(context.get('default_project_id')) in (int, long):
51 return context.get('default_project_id')
54 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
55 access_rights_uid = access_rights_uid or uid
56 stage_obj = self.pool.get('crm.case.stage')
57 order = stage_obj._order
58 if read_group_order == 'stage_id desc':
59 # lame hack to allow reverting search, should just work in the trivial case
60 order = "%s desc" % order
61 # retrieve section_id from the context
62 section_id = self._resolve_section_id_from_context(cr, uid, context=context)
66 search_domain += ['|', '&', ('section_ids', '=', section_id), ('fold', '=', True)]
67 search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', 1), ('fold', '=', False)]
69 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
70 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
71 # restore order of the search
72 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
77 'stage_id': _read_group_stage_ids
80 def _compute_day(self, cr, uid, ids, fields, args, context=None):
82 @param cr: the current row, from the database cursor,
83 @param uid: the current user’s ID for security checks,
84 @param ids: List of Openday’s IDs
85 @return: difference between current date and log date
86 @param context: A standard dictionary for contextual values
88 cal_obj = self.pool.get('resource.calendar')
89 res_obj = self.pool.get('resource.resource')
92 for lead in self.browse(cr, uid, ids, context=context):
97 if field == 'day_open':
99 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
100 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
101 ans = date_open - date_create
102 date_until = lead.date_open
103 elif field == 'day_close':
105 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
106 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
107 date_until = lead.date_closed
108 ans = date_close - date_create
112 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
113 if len(resource_ids):
114 resource_id = resource_ids[0]
116 duration = float(ans.days)
117 if lead.section_id and lead.section_id.resource_calendar_id:
118 duration = float(ans.days) * 24
119 new_dates = cal_obj.interval_get(cr,
121 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
122 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
127 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
128 for in_time, out_time in new_dates:
129 if in_time.date not in no_days:
130 no_days.append(in_time.date)
131 if out_time > date_until:
133 duration = len(no_days)
134 res[lead.id][field] = abs(int(duration))
137 def _history_search(self, cr, uid, obj, name, args, context=None):
139 msg_obj = self.pool.get('mail.message')
140 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
141 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
144 return [('id', 'in', lead_ids)]
146 return [('id', '=', '0')]
148 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
150 for obj in self.browse(cr, uid, ids, context=context):
152 for msg in obj.message_ids:
154 res[obj.id] = msg.subject
159 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
160 select=True, help="Optional linked partner, usually after conversion of the lead"),
162 'id': fields.integer('ID', readonly=True),
163 'name': fields.char('Name', size=64, select=1),
164 'active': fields.boolean('Active', required=False),
165 'date_action_last': fields.datetime('Last Action', readonly=1),
166 'date_action_next': fields.datetime('Next Action', readonly=1),
167 'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
168 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
169 select=True, help='When sending mails, the default email address is taken from the sales team.'),
170 'create_date': fields.datetime('Creation Date' , readonly=True),
171 '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"),
172 'description': fields.text('Notes'),
173 'write_date': fields.datetime('Update Date' , readonly=True),
175 'categ_id': fields.many2one('crm.case.categ', 'Category', \
176 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
177 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
178 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
179 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
180 'contact_name': fields.char('Contact Name', size=64),
181 '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),
182 'opt_in': fields.boolean('Opt-In', oldname='optin', help="If opt-in is checked, this contact has accepted to receive emails."),
183 '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."),
184 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
185 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
186 'date_closed': fields.datetime('Closed', readonly=True),
187 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
188 'user_id': fields.many2one('res.users', 'Salesman'),
189 'referred': fields.char('Referred by', size=64),
190 'date_open': fields.datetime('Opened', readonly=True),
191 'day_open': fields.function(_compute_day, string='Days to Open', \
192 multi='day_open', type="float", store=True),
193 'day_close': fields.function(_compute_day, string='Days to Close', \
194 multi='day_close', type="float", store=True),
195 'state': fields.related('stage_id', 'state', type="selection", store=True,
196 selection=crm.AVAILABLE_STATES, string="State", readonly=True,
197 help='The state is set to \'Draft\', when a case is created.\
198 If the case is in progress the state is set to \'Open\'.\
199 When the case is over, the state is set to \'Done\'.\
200 If the case needs to be reviewed then the state is \
201 set to \'Pending\'.'),
202 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
203 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
205 # Only used for type opportunity
206 'probability': fields.float('Probability (%)',group_operator="avg"),
207 'planned_revenue': fields.float('Expected Revenue'),
208 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
209 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
210 'phone': fields.char("Phone", size=64),
211 'date_deadline': fields.date('Expected Closing'),
212 'date_action': fields.date('Next Action Date', select=True),
213 'title_action': fields.char('Next Action', size=64),
214 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
215 'color': fields.integer('Color Index'),
216 'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
217 'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
218 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
219 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
220 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
226 'user_id': crm_case._get_default_user,
227 'email_from': crm_case._get_default_email,
229 'stage_id': crm_case._get_default_stage_id,
230 'section_id': crm_case._get_section,
231 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
232 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
236 def get_needaction_user_ids(self, cr, uid, ids, context=None):
237 result = dict.fromkeys(ids, [])
238 for obj in self.browse(cr, uid, ids, context=context):
239 # salesman must perform an action when in draft mode
240 if obj.state == 'draft' and obj.user_id:
241 result[obj.id] = [obj.user_id.id]
244 def create(self, cr, uid, vals, context=None):
245 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
246 self.create_send_note(cr, uid, [obj_id], context=context)
249 def on_change_opt_in(self, cr, uid, ids, opt_in):
250 return {'value':{'opt_in':opt_in,'opt_out':False}}
252 def on_change_opt_out(self, cr, uid, ids, opt_out):
253 return {'value':{'opt_out':opt_out,'opt_in':False}}
255 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
258 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
259 if not stage.on_change:
261 return {'value':{'probability': stage.probability}}
263 def stage_find_percent(self, cr, uid, percent, section_id):
264 """ Return the first stage with a probability == percent
266 stage_pool = self.pool.get('crm.case.stage')
268 ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
270 ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
276 def stage_find_lost(self, cr, uid, section_id):
277 return self.stage_find_percent(cr, uid, 0.0, section_id)
279 def stage_find_won(self, cr, uid, section_id):
280 return self.stage_find_percent(cr, uid, 100.0, section_id)
282 def case_cancel(self, cr, uid, ids, context=None):
283 """Overrides cancel for crm_case for setting probability
285 res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
286 self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
289 def case_reset(self, cr, uid, ids, context=None):
290 """Overrides reset as draft in order to set the stage field as empty
292 res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
293 self.write(cr, uid, ids, {'probability': 0.0}, context=context)
296 def case_mark_lost(self, cr, uid, ids, context=None):
297 """Mark the case as lost: state = done and probability = 0%
299 for lead in self.browse(cr, uid, ids):
300 stage_id = self.stage_find_lost(cr, uid, lead.section_id.id or False)
302 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
303 self.case_close_send_note(cr, uid, ids, context=context)
306 def case_mark_won(self, cr, uid, ids, context=None):
307 """Mark the case as lost: state = done and probability = 0%
309 for lead in self.browse(cr, uid, ids):
310 stage_id = self.stage_find_won(cr, uid, lead.section_id.id or False)
312 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
313 self.case_close_send_note(cr, uid, ids, context=context)
316 def set_priority(self, cr, uid, ids, priority):
319 return self.write(cr, uid, ids, {'priority' : priority})
321 def set_high_priority(self, cr, uid, ids, context=None):
322 """Set lead priority to high
324 return self.set_priority(cr, uid, ids, '1')
326 def set_normal_priority(self, cr, uid, ids, context=None):
327 """Set lead priority to normal
329 return self.set_priority(cr, uid, ids, '3')
332 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
333 # prepare opportunity data into dictionary for merging
334 opportunities = self.browse(cr, uid, ids, context=context)
335 def _get_first_not_null(attr):
336 if hasattr(oldest, attr):
337 return getattr(oldest, attr)
338 for opportunity in opportunities:
339 if hasattr(opportunity, attr):
340 return getattr(opportunity, attr)
343 def _get_first_not_null_id(attr):
344 res = _get_first_not_null(attr)
345 return res and res.id or False
347 def _concat_all(attr):
348 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
351 for field_name in fields:
352 field_info = self._all_columns.get(field_name)
353 if field_info is None:
355 field = field_info.column
356 if field._type in ('many2many', 'one2many'):
358 elif field._type == 'many2one':
359 data[field_name] = _get_first_not_null_id(field_name) # !!
360 elif field._type == 'text':
361 data[field_name] = _concat_all(field_name) #not lost
363 data[field_name] = _get_first_not_null(field_name) #not lost
366 def _merge_find_oldest(self, cr, uid, ids, context=None):
369 #TOCHECK: where pass 'convert' in context ?
370 if context.get('convert'):
371 ids = list(set(ids) - set(context.get('lead_ids', False)) )
373 #search opportunities order by create date
374 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
375 oldest_id = opportunity_ids[0]
376 return self.browse(cr, uid, oldest_id, context=context)
378 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
381 body.append("%s\n" % (title))
382 for field_name in fields:
383 field_info = self._all_columns.get(field_name)
384 if field_info is None:
386 field = field_info.column
389 if field._type == 'selection':
390 if hasattr(field.selection, '__call__'):
391 key = field.selection(self, cr, uid, context=context)
393 key = field.selection
394 value = dict(key).get(lead[field_name], lead[field_name])
395 elif field._type == 'many2one':
397 value = lead[field_name].name_get()[0][1]
399 value = lead[field_name]
401 body.append("%s: %s" % (field.string, value or ''))
402 return "\n".join(body + ['---'])
404 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
405 #TOFIX: mail template should be used instead of fix body, subject text
407 merge_message = _('Merged opportunities')
408 subject = [merge_message]
409 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
410 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
411 'country_id', 'city', 'street', 'street2', 'zip']
412 for opportunity in opportunities:
413 subject.append(opportunity.name)
414 title = "%s : %s" % (merge_message, opportunity.name)
415 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
417 subject = subject[0] + ", ".join(subject[1:])
418 details = "\n\n".join(details)
419 return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
421 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
422 message = self.pool.get('mail.message')
423 for opportunity in opportunities:
424 for history in opportunity.message_ids:
425 message.write(cr, uid, history.id, {
426 'res_id': opportunity_id,
427 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
432 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
433 attachment = self.pool.get('ir.attachment')
435 # return attachments of opportunity
436 def _get_attachments(opportunity_id):
437 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
438 return attachment.browse(cr, uid, attachment_ids, context=context)
441 first_attachments = _get_attachments(opportunity_id)
442 for opportunity in opportunities:
443 attachments = _get_attachments(opportunity.id)
444 for first in first_attachments:
445 for attachment in attachments:
446 if attachment.name == first.name:
448 name = "%s (%s)" % (attachment.name, count,),
449 res_id = opportunity_id,
451 attachment.write(values)
456 def merge_opportunity(self, cr, uid, ids, context=None):
458 To merge opportunities
459 :param ids: list of opportunities ids to merge
461 if context is None: context = {}
463 #TOCHECK: where pass lead_ids in context?
464 lead_ids = context and context.get('lead_ids', []) or []
467 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
469 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
470 opportunities = self.browse(cr, uid, ids, context=context)
471 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
472 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
473 if ctx_opportunities :
474 first_opportunity = ctx_opportunities[0]
475 tail_opportunities = opportunities_list
477 first_opportunity = opportunities_list[0]
478 tail_opportunities = opportunities_list[1:]
480 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',
481 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
482 'date_action_next', 'email_from', 'email_cc', 'partner_name']
484 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
486 # merge data into first opportunity
487 self.write(cr, uid, [first_opportunity.id], data, context=context)
489 #copy message and attachements into the first opportunity
490 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
491 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
493 #Notification about loss of information
494 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
495 #delete tail opportunities
496 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
498 #open first opportunity
499 self.case_open(cr, uid, [first_opportunity.id])
500 return first_opportunity.id
502 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
503 crm_stage = self.pool.get('crm.case.stage')
506 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
508 section_id = lead.section_id and lead.section_id.id or False
510 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
512 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
513 stage_id = stage_ids and stage_ids[0] or False
515 'planned_revenue': lead.planned_revenue,
516 'probability': lead.probability,
518 'partner_id': customer and customer.id or False,
519 'user_id': (lead.user_id and lead.user_id.id),
520 'type': 'opportunity',
521 'stage_id': stage_id or False,
522 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
523 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
526 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
527 partner = self.pool.get('res.partner')
528 mail_message = self.pool.get('mail.message')
531 customer = partner.browse(cr, uid, partner_id, context=context)
532 for lead in self.browse(cr, uid, ids, context=context):
533 if lead.state in ('done', 'cancel'):
535 if user_ids or section_id:
536 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
538 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
539 self.write(cr, uid, [lead.id], vals, context=context)
541 self.convert_opportunity_send_note(cr, uid, lead, context=context)
542 #TOCHECK: why need to change partner details in all messages of lead ?
544 msg_ids = [ x.id for x in lead.message_ids]
545 mail_message.write(cr, uid, msg_ids, {
546 'partner_id': lead.partner_id.id
550 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
551 partner = self.pool.get('res.partner')
552 vals = { 'name': name,
553 'user_id': lead.user_id.id,
554 'comment': lead.description,
555 'section_id': lead.section_id.id or False,
556 'parent_id': parent_id,
558 'mobile': lead.mobile,
559 'email': lead.email_from and to_email(lead.email_from)[0],
561 'title': lead.title and lead.title.id or False,
562 'function': lead.function,
563 'street': lead.street,
564 'street2': lead.street2,
567 'country_id': lead.country_id and lead.country_id.id or False,
568 'state_id': lead.state_id and lead.state_id.id or False,
569 'is_company': is_company,
572 partner = partner.create(cr, uid,vals, context)
575 def _create_lead_partner(self, cr, uid, lead, context=None):
577 if lead.partner_name and lead.contact_name:
578 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
579 self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
580 elif lead.partner_name and not lead.contact_name:
581 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
582 elif not lead.partner_name and lead.contact_name:
583 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
585 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
588 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
590 res_partner = self.pool.get('res.partner')
592 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
593 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
594 res = lead.write({'partner_id' : partner_id, }, context=context)
595 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
598 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
600 This function convert partner based on action.
601 if action is 'create', create new partner with contact and assign lead to new partner_id.
602 otherwise assign lead to specified partner_id
607 for lead in self.browse(cr, uid, ids, context=context):
608 if action == 'create':
610 partner_id = self._create_lead_partner(cr, uid, lead, context)
611 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
612 partner_ids[lead.id] = partner_id
615 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
617 Send mail to salesman with updated Lead details.
618 @ lead: browse record of 'crm.lead' object.
620 #TOFIX: mail template should be used here instead of fix subject, body text.
621 message = self.pool.get('mail.message')
622 email_to = lead.user_id and lead.user_id.user_email
626 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
627 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
628 subject = "lead %s converted into opportunity" % lead.name
629 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
630 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
633 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
638 value['section_id'] = team_id
639 if index < len(user_ids):
640 value['user_id'] = user_ids[index]
643 self.write(cr, uid, [lead_id], value, context=context)
646 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):
648 action :('schedule','Schedule a call'), ('log','Log a call')
650 phonecall = self.pool.get('crm.phonecall')
651 model_data = self.pool.get('ir.model.data')
654 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
656 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
657 for lead in self.browse(cr, uid, ids, context=context):
659 section_id = lead.section_id and lead.section_id.id or False
661 user_id = lead.user_id and lead.user_id.id or False
663 'name' : call_summary,
664 'opportunity_id' : lead.id,
665 'user_id' : user_id or False,
666 'categ_id' : categ_id or False,
667 'description' : desc or '',
668 'date' : schedule_time,
669 'section_id' : section_id or False,
670 'partner_id': lead.partner_id and lead.partner_id.id or False,
671 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
672 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
673 'priority': lead.priority,
675 new_id = phonecall.create(cr, uid, vals, context=context)
676 phonecall.case_open(cr, uid, [new_id], context=context)
678 phonecall.case_close(cr, uid, [new_id], context=context)
679 phonecall_dict[lead.id] = new_id
680 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
681 return phonecall_dict
684 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
685 models_data = self.pool.get('ir.model.data')
687 # Get Opportunity views
688 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
689 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
691 'name': _('Opportunity'),
693 'view_mode': 'tree, form',
694 'res_model': 'crm.lead',
695 'domain': [('type', '=', 'opportunity')],
696 'res_id': int(opportunity_id),
698 'views': [(form_view and form_view[1] or False, 'form'),
699 (tree_view and tree_view[1] or False, 'tree'),
700 (False, 'calendar'), (False, 'graph')],
701 'type': 'ir.actions.act_window',
705 def message_new(self, cr, uid, msg, custom_values=None, context=None):
706 """Automatically calls when new email message arrives"""
707 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
708 subject = msg.get('subject') or _("No Subject")
709 body = msg.get('body_text')
711 msg_from = msg.get('from')
712 priority = msg.get('priority')
715 'email_from': msg_from,
716 'email_cc': msg.get('cc'),
721 vals['priority'] = priority
722 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
723 self.write(cr, uid, [res_id], vals, context)
726 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
727 if isinstance(ids, (str, int, long)):
731 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
733 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
734 vals['priority'] = msg.get('priority')
736 'cost':'planned_cost',
737 'revenue': 'planned_revenue',
738 'probability':'probability'
741 for line in msg['body_text'].split('\n'):
743 res = tools.misc.command_re.match(line)
744 if res and maps.get(res.group(1).lower()):
745 key = maps.get(res.group(1).lower())
746 vls[key] = res.group(2).lower()
749 # Unfortunately the API is based on lists
750 # but we want to update the state based on the
751 # previous state, so we have to loop:
752 for case in self.browse(cr, uid, ids, context=context):
754 if case.state in CRM_LEAD_PENDING_STATES:
756 values.update(state=crm.AVAILABLE_STATES[1][0])
757 if not case.date_open:
758 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
759 res = self.write(cr, uid, [case.id], values, context=context)
762 def action_makeMeeting(self, cr, uid, ids, context=None):
764 This opens Meeting's calendar view to schedule meeting on current Opportunity
765 @return : Dictionary value for created Meeting view
770 data_obj = self.pool.get('ir.model.data')
771 for opp in self.browse(cr, uid, ids, context=context):
773 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
774 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
775 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
776 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
778 'default_opportunity_id': opp.id,
779 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
780 'default_user_id': uid,
781 'default_section_id': opp.section_id and opp.section_id.id or False,
782 'default_email_from': opp.email_from,
783 'default_state': 'open',
784 'default_name': opp.name
787 'name': _('Meetings'),
790 'view_mode': 'calendar,form,tree',
791 'res_model': 'crm.meeting',
793 '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')],
794 'type': 'ir.actions.act_window',
795 'search_view_id': search_view and search_view[1] or False,
801 def unlink(self, cr, uid, ids, context=None):
802 for lead in self.browse(cr, uid, ids, context):
803 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
804 raise osv.except_osv(_('Error'),
805 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
806 "You should better cancel it, instead of deleting it.") % lead.name)
807 return super(crm_lead, self).unlink(cr, uid, ids, context)
810 def write(self, cr, uid, ids, vals, context=None):
814 if 'date_closed' in vals:
815 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
817 if vals.get('stage_id'):
818 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
819 # change probability of lead(s) if required by stage
820 if not vals.get('probability') and stage.on_change:
821 vals['probability'] = stage.probability
822 for case in self.browse(cr, uid, ids, context=context):
823 message = _("Stage changed to <b>%s</b>.") % (stage.name)
824 case.message_append_note(body=message)
825 return super(crm_lead,self).write(cr, uid, ids, vals, context)
827 # ----------------------------------------
828 # OpenChatter methods and notifications
829 # ----------------------------------------
831 def message_get_subscribers(self, cr, uid, ids, context=None):
832 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
833 # add salesman to the subscribers
834 for obj in self.browse(cr, uid, ids, context=context):
836 sub_ids.append(obj.user_id.id)
837 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
839 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
840 if isinstance(lead, (int, long)):
841 lead = self.browse(cr, uid, [lead], context=context)[0]
842 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
844 def create_send_note(self, cr, uid, ids, context=None):
846 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
847 self.message_append_note(cr, uid, [id], body=message, context=context)
850 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
851 message = _("Opportunity has been <b>lost</b>.")
852 return self.message_append_note(cr, uid, ids, body=message, context=context)
854 def case_mark_won_send_note(self, cr, uid, ids, context=None):
855 message = _("Opportunity has been <b>won</b>.")
856 return self.message_append_note(cr, uid, ids, body=message, context=context)
858 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
859 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
860 if action == 'log': prefix = 'Logged'
861 else: prefix = 'Scheduled'
862 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
863 return self. message_append_note(cr, uid, ids, body=message, context=context)
865 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
866 for lead in self.browse(cr, uid, ids, context=context):
867 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))
868 lead.message_append_note(body=message)
871 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
872 message = _("Lead has been <b>converted to an opportunity</b>.")
873 lead.message_append_note(body=message)
878 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: