1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-today OpenERP SA (<http://www.openerp.com>)
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 ##############################################################################
23 from base_status.base_stage import base_stage
25 from datetime import datetime
26 from mail.mail_message import to_email
27 from osv import fields, osv
30 from tools.translate import _
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(base_stage, 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) or False)
49 def _get_default_stage_id(self, cr, uid, context=None):
50 """ Gives default stage_id """
51 section_id = self._get_default_section_id(cr, uid, context=context)
52 return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft'), ('type', '=', 'both')], context=context)
54 def _resolve_section_id_from_context(self, cr, uid, context=None):
55 """ Returns ID of section based on the value of 'section_id'
56 context key, or None if it cannot be resolved to a single
61 if type(context.get('default_section_id')) in (int, long):
62 return context.get('default_section_id')
63 if isinstance(context.get('default_section_id'), basestring):
64 section_name = context['default_section_id']
65 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=section_name, context=context)
66 if len(section_ids) == 1:
67 return int(section_ids[0][0])
70 def _resolve_type_from_context(self, cr, uid, context=None):
71 """ Returns the type (lead or opportunity) from the type context
72 key. Returns None if it cannot be resolved.
76 return context.get('default_type')
78 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
79 access_rights_uid = access_rights_uid or uid
80 stage_obj = self.pool.get('crm.case.stage')
81 order = stage_obj._order
82 # lame hack to allow reverting search, should just work in the trivial case
83 if read_group_order == 'stage_id desc':
84 order = "%s desc" % order
85 # retrieve section_id from the context and write the domain
86 # - ('id', 'in', 'ids'): add columns that should be present
87 # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
88 # - OR ('section_ids', '=', section_id), ('fold', '=', False) if section_id: add section columns that are not folded
90 section_id = self._resolve_section_id_from_context(cr, uid, context=context)
92 search_domain += ['|', '&', ('section_ids', '=', section_id), ('fold', '=', False)]
93 search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)]
94 # retrieve type from the context (if set: choose 'type' or 'both')
95 type = self._resolve_type_from_context(cr, uid, context=context)
97 search_domain += ['|', ('type', '=', type), ('type', '=', 'both')]
99 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
100 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
101 # restore order of the search
102 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
106 'stage_id': _read_group_stage_ids
109 def _compute_day(self, cr, uid, ids, fields, args, context=None):
111 @param cr: the current row, from the database cursor,
112 @param uid: the current user’s ID for security checks,
113 @param ids: List of Openday’s IDs
114 @return: difference between current date and log date
115 @param context: A standard dictionary for contextual values
117 cal_obj = self.pool.get('resource.calendar')
118 res_obj = self.pool.get('resource.resource')
121 for lead in self.browse(cr, uid, ids, context=context):
126 if field == 'day_open':
128 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
129 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
130 ans = date_open - date_create
131 date_until = lead.date_open
132 elif field == 'day_close':
134 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
135 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
136 date_until = lead.date_closed
137 ans = date_close - date_create
141 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
142 if len(resource_ids):
143 resource_id = resource_ids[0]
145 duration = float(ans.days)
146 if lead.section_id and lead.section_id.resource_calendar_id:
147 duration = float(ans.days) * 24
148 new_dates = cal_obj.interval_get(cr,
150 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
151 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
156 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
157 for in_time, out_time in new_dates:
158 if in_time.date not in no_days:
159 no_days.append(in_time.date)
160 if out_time > date_until:
162 duration = len(no_days)
163 res[lead.id][field] = abs(int(duration))
166 def _history_search(self, cr, uid, obj, name, args, context=None):
168 msg_obj = self.pool.get('mail.message')
169 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
170 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
173 return [('id', 'in', lead_ids)]
175 return [('id', '=', '0')]
177 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
179 for obj in self.browse(cr, uid, ids, context=context):
181 for msg in obj.message_ids:
183 res[obj.id] = msg.subject
188 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
189 select=True, help="Optional linked partner, usually after conversion of the lead"),
191 'id': fields.integer('ID', readonly=True),
192 'name': fields.char('Subject', size=64, required=True, select=1),
193 'active': fields.boolean('Active', required=False),
194 'date_action_last': fields.datetime('Last Action', readonly=1),
195 'date_action_next': fields.datetime('Next Action', readonly=1),
196 'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
197 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
198 select=True, help='When sending mails, the default email address is taken from the sales team.'),
199 'create_date': fields.datetime('Creation Date' , readonly=True),
200 '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"),
201 'description': fields.text('Notes'),
202 'write_date': fields.datetime('Update Date' , readonly=True),
203 'categ_id': fields.many2one('crm.case.categ', 'Category', \
204 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
205 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
206 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
207 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
208 'contact_name': fields.char('Contact Name', size=64),
209 '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),
210 'opt_in': fields.boolean('Opt-In', oldname='optin', help="If opt-in is checked, this contact has accepted to receive emails."),
211 '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."),
212 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
213 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
214 'date_closed': fields.datetime('Closed', readonly=True),
215 'stage_id': fields.many2one('crm.case.stage', 'Stage',
216 domain="['&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
217 'user_id': fields.many2one('res.users', 'Salesperson'),
218 'referred': fields.char('Referred By', size=64),
219 'date_open': fields.datetime('Opened', readonly=True),
220 'day_open': fields.function(_compute_day, string='Days to Open', \
221 multi='day_open', type="float", store=True),
222 'day_close': fields.function(_compute_day, string='Days to Close', \
223 multi='day_close', type="float", store=True),
224 'state': fields.related('stage_id', 'state', type="selection", store=True,
225 selection=crm.AVAILABLE_STATES, string="State", readonly=True,
226 help='The state is set to \'Draft\', when a case is created.\
227 If the case is in progress the state is set to \'Open\'.\
228 When the case is over, the state is set to \'Done\'.\
229 If the case needs to be reviewed then the state is \
230 set to \'Pending\'.'),
231 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
233 # Only used for type opportunity
234 'probability': fields.float('Success Rate (%)',group_operator="avg"),
235 'planned_revenue': fields.float('Expected Revenue'),
236 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
237 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
238 'phone': fields.char("Phone", size=64),
239 'date_deadline': fields.date('Expected Closing'),
240 'date_action': fields.date('Next Action Date', select=True),
241 'title_action': fields.char('Next Action', size=64),
242 'color': fields.integer('Color Index'),
243 'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
244 'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
245 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
246 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
247 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
253 'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
254 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
255 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
256 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
257 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
258 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
262 def create(self, cr, uid, vals, context=None):
263 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
264 self.create_send_note(cr, uid, [obj_id], context=context)
267 def on_change_opt_in(self, cr, uid, ids, opt_in):
268 return {'value':{'opt_in':opt_in,'opt_out':False}}
270 def on_change_opt_out(self, cr, uid, ids, opt_out):
271 return {'value':{'opt_out':opt_out,'opt_in':False}}
273 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
276 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
277 if not stage.on_change:
279 return {'value':{'probability': stage.probability}}
281 def _check(self, cr, uid, ids=False, context=None):
282 """ Override of the base.stage method.
283 Function called by the scheduler to process cases for date actions
284 Only works on not done and cancelled cases
286 cr.execute('select * from crm_case \
287 where (date_action_last<%s or date_action_last is null) \
288 and (date_action_next<=%s or date_action_next is null) \
289 and state not in (\'cancel\',\'done\')',
290 (time.strftime("%Y-%m-%d %H:%M:%S"),
291 time.strftime('%Y-%m-%d %H:%M:%S')))
293 ids2 = map(lambda x: x[0], cr.fetchall() or [])
294 cases = self.browse(cr, uid, ids2, context=context)
295 return self._action(cr, uid, cases, False, context=context)
297 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
298 """ Override of the base.stage method
299 Parameter of the stage search taken from the lead:
300 - type: stage type must be the same or 'both'
301 - section_id: if set, stages must belong to this section or
302 be a default stage; if not set, stages must be default
305 if isinstance(cases, (int, long)):
306 cases = self.browse(cr, uid, cases, context=context)
307 # collect all section_ids
311 section_ids.append(section_id)
314 section_ids.append(lead.section_id.id)
315 if lead.type not in types:
316 types.append(lead.type)
317 # OR all section_ids and OR with case_default
320 search_domain += [('|')] * len(section_ids)
321 for section_id in section_ids:
322 search_domain.append(('section_ids', '=', section_id))
323 search_domain.append(('case_default', '=', True))
324 # AND with cases types
325 search_domain.append(('type', 'in', types))
326 # AND with the domain in parameter
327 search_domain += list(domain)
328 # perform search, return the first found
329 stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
334 def case_cancel(self, cr, uid, ids, context=None):
335 """ Overrides case_cancel from base_stage to set probability """
336 res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
337 self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
340 def case_reset(self, cr, uid, ids, context=None):
341 """ Overrides case_reset from base_stage to set probability """
342 res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
343 self.write(cr, uid, ids, {'probability': 0.0}, context=context)
346 def case_mark_lost(self, cr, uid, ids, context=None):
347 """ Mark the case as lost: state=cancel and probability=0 """
348 for lead in self.browse(cr, uid, ids):
349 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
351 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
352 self.case_mark_lost_send_note(cr, uid, ids, context=context)
355 def case_mark_won(self, cr, uid, ids, context=None):
356 """ Mark the case as lost: state=done and probability=100 """
357 for lead in self.browse(cr, uid, ids):
358 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
360 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
361 self.case_mark_won_send_note(cr, uid, ids, context=context)
364 def set_priority(self, cr, uid, ids, priority):
365 """ Set lead priority
367 return self.write(cr, uid, ids, {'priority' : priority})
369 def set_high_priority(self, cr, uid, ids, context=None):
370 """ Set lead priority to high
372 return self.set_priority(cr, uid, ids, '1')
374 def set_normal_priority(self, cr, uid, ids, context=None):
375 """ Set lead priority to normal
377 return self.set_priority(cr, uid, ids, '3')
379 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
380 # prepare opportunity data into dictionary for merging
381 opportunities = self.browse(cr, uid, ids, context=context)
382 def _get_first_not_null(attr):
383 if hasattr(oldest, attr):
384 return getattr(oldest, attr)
385 for opportunity in opportunities:
386 if hasattr(opportunity, attr):
387 return getattr(opportunity, attr)
390 def _get_first_not_null_id(attr):
391 res = _get_first_not_null(attr)
392 return res and res.id or False
394 def _concat_all(attr):
395 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
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
403 if field._type in ('many2many', 'one2many'):
405 elif field._type == 'many2one':
406 data[field_name] = _get_first_not_null_id(field_name) # !!
407 elif field._type == 'text':
408 data[field_name] = _concat_all(field_name) #not lost
410 data[field_name] = _get_first_not_null(field_name) #not lost
413 def _merge_find_oldest(self, cr, uid, ids, context=None):
416 #TOCHECK: where pass 'convert' in context ?
417 if context.get('convert'):
418 ids = list(set(ids) - set(context.get('lead_ids', False)) )
420 #search opportunities order by create date
421 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
422 oldest_id = opportunity_ids[0]
423 return self.browse(cr, uid, oldest_id, context=context)
425 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
428 body.append("%s\n" % (title))
429 for field_name in fields:
430 field_info = self._all_columns.get(field_name)
431 if field_info is None:
433 field = field_info.column
436 if field._type == 'selection':
437 if hasattr(field.selection, '__call__'):
438 key = field.selection(self, cr, uid, context=context)
440 key = field.selection
441 value = dict(key).get(lead[field_name], lead[field_name])
442 elif field._type == 'many2one':
444 value = lead[field_name].name_get()[0][1]
446 value = lead[field_name]
448 body.append("%s: %s" % (field.string, value or ''))
449 return "\n".join(body + ['---'])
451 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
452 #TOFIX: mail template should be used instead of fix body, subject text
454 merge_message = _('Merged opportunities')
455 subject = [merge_message]
456 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
457 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
458 'country_id', 'city', 'street', 'street2', 'zip']
459 for opportunity in opportunities:
460 subject.append(opportunity.name)
461 title = "%s : %s" % (merge_message, opportunity.name)
462 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
464 subject = subject[0] + ", ".join(subject[1:])
465 details = "\n\n".join(details)
466 return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
468 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
469 message = self.pool.get('mail.message')
470 for opportunity in opportunities:
471 for history in opportunity.message_ids:
472 message.write(cr, uid, history.id, {
473 'res_id': opportunity_id,
474 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
479 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
480 attachment = self.pool.get('ir.attachment')
482 # return attachments of opportunity
483 def _get_attachments(opportunity_id):
484 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
485 return attachment.browse(cr, uid, attachment_ids, context=context)
488 first_attachments = _get_attachments(opportunity_id)
489 for opportunity in opportunities:
490 attachments = _get_attachments(opportunity.id)
491 for first in first_attachments:
492 for attachment in attachments:
493 if attachment.name == first.name:
495 name = "%s (%s)" % (attachment.name, count,),
496 res_id = opportunity_id,
498 attachment.write(values)
503 def merge_opportunity(self, cr, uid, ids, context=None):
505 To merge opportunities
506 :param ids: list of opportunities ids to merge
508 if context is None: context = {}
510 #TOCHECK: where pass lead_ids in context?
511 lead_ids = context and context.get('lead_ids', []) or []
514 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
516 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
517 opportunities = self.browse(cr, uid, ids, context=context)
518 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
519 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
520 if ctx_opportunities :
521 first_opportunity = ctx_opportunities[0]
522 tail_opportunities = opportunities_list
524 first_opportunity = opportunities_list[0]
525 tail_opportunities = opportunities_list[1:]
527 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',
528 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
529 'date_action_next', 'email_from', 'email_cc', 'partner_name']
531 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
533 # merge data into first opportunity
534 self.write(cr, uid, [first_opportunity.id], data, context=context)
536 #copy message and attachements into the first opportunity
537 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
538 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
540 #Notification about loss of information
541 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
542 #delete tail opportunities
543 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
545 #open first opportunity
546 self.case_open(cr, uid, [first_opportunity.id])
547 return first_opportunity.id
549 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
550 crm_stage = self.pool.get('crm.case.stage')
553 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
555 section_id = lead.section_id and lead.section_id.id or False
557 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
559 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
560 stage_id = stage_ids and stage_ids[0] or False
562 'planned_revenue': lead.planned_revenue,
563 'probability': lead.probability,
565 'partner_id': customer and customer.id or False,
566 'user_id': (lead.user_id and lead.user_id.id),
567 'type': 'opportunity',
568 'stage_id': stage_id or False,
569 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
570 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
573 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
574 partner = self.pool.get('res.partner')
575 mail_message = self.pool.get('mail.message')
578 customer = partner.browse(cr, uid, partner_id, context=context)
579 for lead in self.browse(cr, uid, ids, context=context):
580 if lead.state in ('done', 'cancel'):
582 if user_ids or section_id:
583 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
585 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
586 self.write(cr, uid, [lead.id], vals, context=context)
588 self.convert_opportunity_send_note(cr, uid, lead, context=context)
589 #TOCHECK: why need to change partner details in all messages of lead ?
591 msg_ids = [ x.id for x in lead.message_ids]
592 mail_message.write(cr, uid, msg_ids, {
593 'partner_id': lead.partner_id.id
597 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
598 partner = self.pool.get('res.partner')
599 vals = { 'name': name,
600 'user_id': lead.user_id.id,
601 'comment': lead.description,
602 'section_id': lead.section_id.id or False,
603 'parent_id': parent_id,
605 'mobile': lead.mobile,
606 'email': lead.email_from and to_email(lead.email_from)[0],
608 'title': lead.title and lead.title.id or False,
609 'function': lead.function,
610 'street': lead.street,
611 'street2': lead.street2,
614 'country_id': lead.country_id and lead.country_id.id or False,
615 'state_id': lead.state_id and lead.state_id.id or False,
616 'is_company': is_company,
619 partner = partner.create(cr, uid,vals, context)
622 def _create_lead_partner(self, cr, uid, lead, context=None):
624 if lead.partner_name and lead.contact_name:
625 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
626 self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
627 elif lead.partner_name and not lead.contact_name:
628 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
629 elif not lead.partner_name and lead.contact_name:
630 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
632 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
635 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
637 res_partner = self.pool.get('res.partner')
639 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
640 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
641 res = lead.write({'partner_id' : partner_id, }, context=context)
642 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
645 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
647 This function convert partner based on action.
648 if action is 'create', create new partner with contact and assign lead to new partner_id.
649 otherwise assign lead to specified partner_id
654 for lead in self.browse(cr, uid, ids, context=context):
655 if action == 'create':
657 partner_id = self._create_lead_partner(cr, uid, lead, context)
658 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
659 partner_ids[lead.id] = partner_id
662 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
664 Send mail to salesman with updated Lead details.
665 @ lead: browse record of 'crm.lead' object.
667 #TOFIX: mail template should be used here instead of fix subject, body text.
668 message = self.pool.get('mail.message')
669 email_to = lead.user_id and lead.user_id.user_email
673 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
674 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
675 subject = "lead %s converted into opportunity" % lead.name
676 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
677 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
680 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
685 value['section_id'] = team_id
686 if index < len(user_ids):
687 value['user_id'] = user_ids[index]
690 self.write(cr, uid, [lead_id], value, context=context)
693 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):
695 action :('schedule','Schedule a call'), ('log','Log a call')
697 phonecall = self.pool.get('crm.phonecall')
698 model_data = self.pool.get('ir.model.data')
701 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
703 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
704 for lead in self.browse(cr, uid, ids, context=context):
706 section_id = lead.section_id and lead.section_id.id or False
708 user_id = lead.user_id and lead.user_id.id or False
710 'name' : call_summary,
711 'opportunity_id' : lead.id,
712 'user_id' : user_id or False,
713 'categ_id' : categ_id or False,
714 'description' : desc or '',
715 'date' : schedule_time,
716 'section_id' : section_id or False,
717 'partner_id': lead.partner_id and lead.partner_id.id or False,
718 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
719 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
720 'priority': lead.priority,
722 new_id = phonecall.create(cr, uid, vals, context=context)
723 phonecall.case_open(cr, uid, [new_id], context=context)
725 phonecall.case_close(cr, uid, [new_id], context=context)
726 phonecall_dict[lead.id] = new_id
727 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
728 return phonecall_dict
731 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
732 models_data = self.pool.get('ir.model.data')
734 # Get Opportunity views
735 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
736 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
738 'name': _('Opportunity'),
740 'view_mode': 'tree, form',
741 'res_model': 'crm.lead',
742 'domain': [('type', '=', 'opportunity')],
743 'res_id': int(opportunity_id),
745 'views': [(form_view and form_view[1] or False, 'form'),
746 (tree_view and tree_view[1] or False, 'tree'),
747 (False, 'calendar'), (False, 'graph')],
748 'type': 'ir.actions.act_window',
751 def action_makeMeeting(self, cr, uid, ids, context=None):
753 This opens Meeting's calendar view to schedule meeting on current Opportunity
754 @return : Dictionary value for created Meeting view
759 data_obj = self.pool.get('ir.model.data')
760 for opp in self.browse(cr, uid, ids, context=context):
762 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
763 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
764 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
765 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
767 'default_opportunity_id': opp.id,
768 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
769 'default_user_id': uid,
770 'default_section_id': opp.section_id and opp.section_id.id or False,
771 'default_email_from': opp.email_from,
772 'default_state': 'open',
773 'default_name': opp.name
776 'name': _('Meetings'),
779 'view_mode': 'calendar,form,tree',
780 'res_model': 'crm.meeting',
782 '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')],
783 'type': 'ir.actions.act_window',
784 'search_view_id': search_view and search_view[1] or False,
790 def unlink(self, cr, uid, ids, context=None):
791 for lead in self.browse(cr, uid, ids, context):
792 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
793 raise osv.except_osv(_('Error'),
794 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
795 "You should better cancel it, instead of deleting it.") % lead.name)
796 return super(crm_lead, self).unlink(cr, uid, ids, context)
798 def write(self, cr, uid, ids, vals, context=None):
799 if vals.get('stage_id') and not vals.get('probability'):
800 # change probability of lead(s) if required by stage
801 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
803 vals['probability'] = stage.probability
804 return super(crm_lead,self).write(cr, uid, ids, vals, context)
806 # ----------------------------------------
808 # ----------------------------------------
810 def message_new(self, cr, uid, msg, custom_values=None, context=None):
811 """ Overrides mail_thread message_new that is called by the mailgateway
812 through message_process.
813 This override updates the document according to the email.
815 if custom_values is None: custom_values = {}
816 custom_values.update({
817 'name': msg.get('subject') or _("No Subject"),
818 'description': msg.get('body_text'),
819 'email_from': msg.get('from'),
820 'email_cc': msg.get('cc'),
823 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
824 custom_values['priority'] = msg.get('priority')
825 custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from', False), context=context))
826 return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
828 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
829 """ Overrides mail_thread message_update that is called by the mailgateway
830 through message_process.
831 This method updates the document according to the email.
833 if isinstance(ids, (str, int, long)):
835 if update_vals is None: update_vals = {}
837 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
838 vals['priority'] = msg.get('priority')
840 'cost':'planned_cost',
841 'revenue': 'planned_revenue',
842 'probability':'probability',
844 for line in msg.get('body_text', '').split('\n'):
846 res = tools.misc.command_re.match(line)
847 if res and maps.get(res.group(1).lower()):
848 key = maps.get(res.group(1).lower())
849 vals[key] = res.group(2).lower()
851 return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
853 # ----------------------------------------
854 # OpenChatter methods and notifications
855 # ----------------------------------------
857 def message_get_subscribers(self, cr, uid, ids, context=None):
858 """ Override to add the salesman. """
859 user_ids = super(crm_lead, self).message_get_subscribers(cr, uid, ids, context=context)
860 for obj in self.browse(cr, uid, ids, context=context):
861 if obj.user_id and not obj.user_id.id in user_ids:
862 user_ids.append(obj.user_id.id)
865 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
866 """ Override of the (void) default notification method. """
867 stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
868 return self.message_append_note(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
870 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
871 if isinstance(lead, (int, long)):
872 lead = self.browse(cr, uid, [lead], context=context)[0]
873 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
875 def create_send_note(self, cr, uid, ids, context=None):
877 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
878 self.message_append_note(cr, uid, [id], body=message, context=context)
881 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
882 message = _("Opportunity has been <b>lost</b>.")
883 return self.message_append_note(cr, uid, ids, body=message, context=context)
885 def case_mark_won_send_note(self, cr, uid, ids, context=None):
886 message = _("Opportunity has been <b>won</b>.")
887 return self.message_append_note(cr, uid, ids, body=message, context=context)
889 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
890 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
891 if action == 'log': prefix = 'Logged'
892 else: prefix = 'Scheduled'
893 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
894 return self.message_append_note(cr, uid, ids, body=message, context=context)
896 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
897 for lead in self.browse(cr, uid, ids, context=context):
898 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))
899 lead.message_append_note(body=message)
902 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
903 message = _("Lead has been <b>converted to an opportunity</b>.")
904 lead.message_append_note(body=message)
909 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: