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 ##############################################################################
22 from base_status.base_stage import base_stage
24 from datetime import datetime
25 from osv import fields, osv
28 from tools.translate import _
29 from tools import html2plaintext
31 from base.res.res_partner import format_address
33 CRM_LEAD_FIELDS_TO_MERGE = ['name',
63 CRM_LEAD_PENDING_STATES = (
64 crm.AVAILABLE_STATES[2][0], # Cancelled
65 crm.AVAILABLE_STATES[3][0], # Done
66 crm.AVAILABLE_STATES[4][0], # Pending
69 class crm_lead(base_stage, format_address, osv.osv):
72 _description = "Lead/Opportunity"
73 _order = "priority,date_action,id desc"
74 _inherit = ['mail.thread','ir.needaction_mixin']
76 def _get_default_section_id(self, cr, uid, context=None):
77 """ Gives default section by checking if present in the context """
78 return (self._resolve_section_id_from_context(cr, uid, context=context) or False)
80 def _get_default_stage_id(self, cr, uid, context=None):
81 """ Gives default stage_id """
82 section_id = self._get_default_section_id(cr, uid, context=context)
83 return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft')], context=context)
85 def _resolve_section_id_from_context(self, cr, uid, context=None):
86 """ Returns ID of section based on the value of 'section_id'
87 context key, or None if it cannot be resolved to a single
92 if type(context.get('default_section_id')) in (int, long):
93 return context.get('default_section_id')
94 if isinstance(context.get('default_section_id'), basestring):
95 section_name = context['default_section_id']
96 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=section_name, context=context)
97 if len(section_ids) == 1:
98 return int(section_ids[0][0])
101 def _resolve_type_from_context(self, cr, uid, context=None):
102 """ Returns the type (lead or opportunity) from the type context
103 key. Returns None if it cannot be resolved.
107 return context.get('default_type')
109 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
110 access_rights_uid = access_rights_uid or uid
111 stage_obj = self.pool.get('crm.case.stage')
112 order = stage_obj._order
113 # lame hack to allow reverting search, should just work in the trivial case
114 if read_group_order == 'stage_id desc':
115 order = "%s desc" % order
116 # retrieve section_id from the context and write the domain
117 # - ('id', 'in', 'ids'): add columns that should be present
118 # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
119 # - OR ('section_ids', '=', section_id), ('fold', '=', False) if section_id: add section columns that are not folded
121 section_id = self._resolve_section_id_from_context(cr, uid, context=context)
123 search_domain += ['|', ('section_ids', '=', section_id)]
124 search_domain += ['|', ('id', 'in', ids), ('case_default', '=', True)]
125 # retrieve type from the context (if set: choose 'type' or 'both')
126 type = self._resolve_type_from_context(cr, uid, context=context)
128 search_domain += ['|', ('type', '=', type), ('type', '=', 'both')]
130 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
131 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
132 # restore order of the search
133 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
136 for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
137 fold[stage.id] = stage.fold or False
141 def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
142 res = super(crm_lead,self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
143 if view_type == 'form':
144 res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
148 'stage_id': _read_group_stage_ids
151 def _compute_day(self, cr, uid, ids, fields, args, context=None):
153 :return dict: difference between current date and log date
155 cal_obj = self.pool.get('resource.calendar')
156 res_obj = self.pool.get('resource.resource')
159 for lead in self.browse(cr, uid, ids, context=context):
164 if field == 'day_open':
166 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
167 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
168 ans = date_open - date_create
169 date_until = lead.date_open
170 elif field == 'day_close':
172 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
173 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
174 date_until = lead.date_closed
175 ans = date_close - date_create
179 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
180 if len(resource_ids):
181 resource_id = resource_ids[0]
183 duration = float(ans.days)
184 if lead.section_id and lead.section_id.resource_calendar_id:
185 duration = float(ans.days) * 24
186 new_dates = cal_obj.interval_get(cr,
188 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
189 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
194 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
195 for in_time, out_time in new_dates:
196 if in_time.date not in no_days:
197 no_days.append(in_time.date)
198 if out_time > date_until:
200 duration = len(no_days)
201 res[lead.id][field] = abs(int(duration))
204 def _history_search(self, cr, uid, obj, name, args, context=None):
206 msg_obj = self.pool.get('mail.message')
207 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
208 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
211 return [('id', 'in', lead_ids)]
213 return [('id', '=', '0')]
216 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
217 select=True, help="Linked partner (optional). Usually created when converting the lead."),
219 'id': fields.integer('ID', readonly=True),
220 'name': fields.char('Subject', size=64, required=True, select=1),
221 'active': fields.boolean('Active', required=False),
222 'date_action_last': fields.datetime('Last Action', readonly=1),
223 'date_action_next': fields.datetime('Next Action', readonly=1),
224 'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
225 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
226 select=True, help='When sending mails, the default email address is taken from the sales team.'),
227 'create_date': fields.datetime('Creation Date' , readonly=True),
228 '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"),
229 'description': fields.text('Notes'),
230 'write_date': fields.datetime('Update Date' , readonly=True),
231 'categ_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Categories', \
232 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
233 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
234 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
235 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
236 'contact_name': fields.char('Contact Name', size=64),
237 '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),
238 '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."),
239 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
240 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
241 'date_closed': fields.datetime('Closed', readonly=True),
242 'stage_id': fields.many2one('crm.case.stage', 'Stage',
243 domain="['&', ('fold', '=', False), '&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
244 'user_id': fields.many2one('res.users', 'Salesperson'),
245 'referred': fields.char('Referred By', size=64),
246 'date_open': fields.datetime('Opened', readonly=True),
247 'day_open': fields.function(_compute_day, string='Days to Open', \
248 multi='day_open', type="float", store=True),
249 'day_close': fields.function(_compute_day, string='Days to Close', \
250 multi='day_close', type="float", store=True),
251 'state': fields.related('stage_id', 'state', type="selection", store=True,
252 selection=crm.AVAILABLE_STATES, string="Status", readonly=True,
253 help='The Status is set to \'Draft\', when a case is created. If the case is in progress the Status is set to \'Open\'. When the case is over, the Status is set to \'Done\'. If the case needs to be reviewed then the Status is set to \'Pending\'.'),
255 # Only used for type opportunity
256 'probability': fields.float('Success Rate (%)',group_operator="avg"),
257 'planned_revenue': fields.float('Expected Revenue'),
258 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
259 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
260 'phone': fields.char("Phone", size=64),
261 'date_deadline': fields.date('Expected Closing', help="Estimate of the date on which the opportunity will be won."),
262 'date_action': fields.date('Next Action Date', select=True),
263 'title_action': fields.char('Next Action', size=64),
264 'color': fields.integer('Color Index'),
265 'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
266 'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
267 'company_currency': fields.related('company_id', 'currency_id', type='many2one', string='Currency', readonly=True, relation="res.currency"),
268 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
269 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
271 # Fields for address, due to separation from crm and res.partner
272 'street': fields.char('Street', size=128),
273 'street2': fields.char('Street2', size=128),
274 'zip': fields.char('Zip', change_default=True, size=24),
275 'city': fields.char('City', size=128),
276 'state_id': fields.many2one("res.country.state", 'State'),
277 'country_id': fields.many2one('res.country', 'Country'),
278 'phone': fields.char('Phone', size=64),
279 'fax': fields.char('Fax', size=64),
280 'mobile': fields.char('Mobile', size=64),
281 'function': fields.char('Function', size=128),
282 'title': fields.many2one('res.partner.title', 'Title'),
283 'company_id': fields.many2one('res.company', 'Company', select=1),
284 'payment_mode': fields.many2one('crm.payment.mode', 'Payment Mode', \
285 domain="[('section_id','=',section_id)]"),
286 'planned_cost': fields.float('Planned Costs'),
292 'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
293 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
294 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
295 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
296 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
297 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
302 ('check_probability', 'check(probability >= 0 and probability <= 100)', 'The probability of closing the deal should be between 0% and 100%!')
305 def create(self, cr, uid, vals, context=None):
306 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
307 self._subscribe_salesteam_followers_to_lead(cr, uid, obj_id, context=context)
308 self.create_send_note(cr, uid, [obj_id], context=context)
311 def _subscribe_salesteam_followers_to_lead(self, cr, uid, obj_id, context=None):
312 follower_obj = self.pool.get('mail.followers')
313 subtype_obj = self.pool.get('mail.message.subtype')
314 section_id = self.browse(cr, uid, obj_id, context=context).section_id
316 followers = [follow.id for follow in section_id.message_follower_ids]
317 lead_subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('res_model', '=', self._name)], context=context)
318 lead_subtypes = subtype_obj.browse(cr, uid, lead_subtype_ids, context=context)
319 follower_ids = follower_obj.search(cr, uid, [('res_model', '=', 'crm.case.section'), ('res_id', '=', section_id)], context=context)
320 self.write(cr, uid, obj_id, {'message_follower_ids': [(6, 0, followers)]}, context=context)
321 for follower in follower_obj.browse(cr, uid, follower_ids, context=context):
322 if not follower.subtype_ids:
324 salesteam_subtype_names = [salesteam_subtype.name for salesteam_subtype in follower.subtype_ids]
325 lead_subtype_ids = [lead_subtype.id for lead_subtype in lead_subtypes if lead_subtype.name in salesteam_subtype_names]
326 self.message_subscribe(cr, uid, [obj_id], [follower.partner_id.id], subtype_ids=lead_subtype_ids, context=context)
328 def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
331 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
332 if not stage.on_change:
334 return {'value':{'probability': stage.probability}}
336 def on_change_partner(self, cr, uid, ids, partner_id, context=None):
340 partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
342 'partner_name' : partner.name,
343 'street' : partner.street,
344 'street2' : partner.street2,
345 'city' : partner.city,
346 'state_id' : partner.state_id and partner.state_id.id or False,
347 'country_id' : partner.country_id and partner.country_id.id or False,
348 'email_from' : partner.email,
349 'phone' : partner.phone,
350 'mobile' : partner.mobile,
353 return {'value' : values}
355 def _check(self, cr, uid, ids=False, context=None):
356 """ Override of the base.stage method.
357 Function called by the scheduler to process cases for date actions
358 Only works on not done and cancelled cases
360 cr.execute('select * from crm_case \
361 where (date_action_last<%s or date_action_last is null) \
362 and (date_action_next<=%s or date_action_next is null) \
363 and state not in (\'cancel\',\'done\')',
364 (time.strftime("%Y-%m-%d %H:%M:%S"),
365 time.strftime('%Y-%m-%d %H:%M:%S')))
367 ids2 = map(lambda x: x[0], cr.fetchall() or [])
368 cases = self.browse(cr, uid, ids2, context=context)
369 return self._action(cr, uid, cases, False, context=context)
371 def stage_find(self, cr, uid, cases, section_id, domain=None, order='sequence', context=None):
372 """ Override of the base.stage method
373 Parameter of the stage search taken from the lead:
374 - type: stage type must be the same or 'both'
375 - section_id: if set, stages must belong to this section or
376 be a default stage; if not set, stages must be default
379 if isinstance(cases, (int, long)):
380 cases = self.browse(cr, uid, cases, context=context)
381 # collect all section_ids
385 type = context.get('default_type')
388 section_ids.append(section_id)
391 section_ids.append(lead.section_id.id)
392 if lead.type not in types:
393 types.append(lead.type)
394 # OR all section_ids and OR with case_default
397 search_domain += [('|')] * len(section_ids)
398 for section_id in section_ids:
399 search_domain.append(('section_ids', '=', section_id))
400 search_domain.append(('case_default', '=', True))
401 # AND with cases types
402 search_domain.append(('type', 'in', types))
403 # AND with the domain in parameter
404 search_domain += list(domain)
405 # perform search, return the first found
406 stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
411 def case_cancel(self, cr, uid, ids, context=None):
412 """ Overrides case_cancel from base_stage to set probability """
413 res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
414 self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
417 def case_reset(self, cr, uid, ids, context=None):
418 """ Overrides case_reset from base_stage to set probability """
419 res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
420 self.write(cr, uid, ids, {'probability': 0.0}, context=context)
423 def case_mark_lost(self, cr, uid, ids, context=None):
424 """ Mark the case as lost: state=cancel and probability=0 """
425 for lead in self.browse(cr, uid, ids):
426 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
428 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
429 self.case_mark_lost_send_note(cr, uid, ids, context=context)
432 def case_mark_won(self, cr, uid, ids, context=None):
433 """ Mark the case as lost: state=done and probability=100 """
434 for lead in self.browse(cr, uid, ids):
435 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
437 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
438 self.case_mark_won_send_note(cr, uid, ids, context=context)
441 def set_priority(self, cr, uid, ids, priority):
442 """ Set lead priority
444 return self.write(cr, uid, ids, {'priority' : priority})
446 def set_high_priority(self, cr, uid, ids, context=None):
447 """ Set lead priority to high
449 return self.set_priority(cr, uid, ids, '1')
451 def set_normal_priority(self, cr, uid, ids, context=None):
452 """ Set lead priority to normal
454 return self.set_priority(cr, uid, ids, '3')
456 def _merge_get_result_type(self, cr, uid, opps, context=None):
458 Define the type of the result of the merge. If at least one of the
459 element to merge is an opp, the resulting new element will be an opp.
460 Otherwise it will be a lead.
462 We'll directly use a list of browse records instead of a list of ids
463 for performances' sake: it will spare a second browse of the
466 :param list opps: list of browse records containing the leads/opps to process
467 :return string type: the type of the final element
470 if (opp.type == 'opportunity'):
475 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
477 Prepare lead/opp data into a dictionary for merging. Different types
478 of fields are processed in different ways:
479 - text: all the values are concatenated
480 - m2m and o2m: those fields aren't processed
481 - m2o: the first not null value prevails (the other are dropped)
482 - any other type of field: same as m2o
484 :param list ids: list of ids of the leads to process
485 :param list fields: list of leads' fields to process
486 :return dict data: contains the merged values
488 opportunities = self.browse(cr, uid, ids, context=context)
490 def _get_first_not_null(attr):
491 if hasattr(oldest, attr):
492 return getattr(oldest, attr)
493 for opp in opportunities:
494 if hasattr(opp, attr):
495 return getattr(opp, attr)
498 def _get_first_not_null_id(attr):
499 res = _get_first_not_null(attr)
500 return res and res.id or False
502 def _concat_all(attr):
503 return ', '.join(filter(lambda x: x, [getattr(opp, attr) or '' for opp in opportunities if hasattr(opp, attr)]))
505 # Process the fields' values
507 for field_name in fields:
508 field_info = self._all_columns.get(field_name)
509 if field_info is None:
511 field = field_info.column
512 if field._type in ('many2many', 'one2many'):
514 elif field._type == 'many2one':
515 data[field_name] = _get_first_not_null_id(field_name) # !!
516 elif field._type == 'text':
517 data[field_name] = _concat_all(field_name) #not lost
519 data[field_name] = _get_first_not_null(field_name) #not lost
521 # Define the resulting type ('lead' or 'opportunity')
522 data['type'] = self._merge_get_result_type(cr, uid, opportunities, context)
526 def _merge_find_oldest(self, cr, uid, ids, context=None):
528 Return the oldest lead found among ids.
530 :param list ids: list of ids of the leads to inspect
531 :return object: browse record of the oldest of the leads
536 if context.get('convert'):
537 ids = list(set(ids) - set(context.get('lead_ids', [])))
539 # Search opportunities order by create date
540 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date', context=context)
541 oldest_opp_id = opportunity_ids[0]
542 return self.browse(cr, uid, oldest_opp_id, context=context)
544 def _mail_body(self, cr, uid, lead, fields, title=False, context=None):
547 body.append("%s\n" % (title))
549 for field_name in fields:
550 field_info = self._all_columns.get(field_name)
551 if field_info is None:
553 field = field_info.column
556 if field._type == 'selection':
557 if hasattr(field.selection, '__call__'):
558 key = field.selection(self, cr, uid, context=context)
560 key = field.selection
561 value = dict(key).get(lead[field_name], lead[field_name])
562 elif field._type == 'many2one':
564 value = lead[field_name].name_get()[0][1]
565 elif field._type == 'many2many':
567 for val in lead[field_name]:
568 field_value = val.name_get()[0][1]
569 value += field_value + ","
571 value = lead[field_name]
573 body.append("%s: %s" % (field.string, value or ''))
574 return "<br/>".join(body + ['<br/>'])
576 def _merge_notify(self, cr, uid, opportunity_id, opportunities, context=None):
578 Create a message gathering merged leads/opps information.
580 #TOFIX: mail template should be used instead of fix body, subject text
582 result_type = self._merge_get_result_type(cr, uid, opportunities, context)
583 if result_type == 'lead':
584 merge_message = _('Merged leads')
586 merge_message = _('Merged opportunities')
587 subject = [merge_message]
588 for opportunity in opportunities:
589 subject.append(opportunity.name)
590 title = "%s : %s" % (merge_message, opportunity.name)
591 details.append(self._mail_body(cr, uid, opportunity, CRM_LEAD_FIELDS_TO_MERGE, title=title, context=context))
593 # Chatter message's subject
594 subject = subject[0] + ": " + ", ".join(subject[1:])
595 details = "\n\n".join(details)
596 return self.message_post(cr, uid, [opportunity_id], body=details, subject=subject, context=context)
598 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
599 message = self.pool.get('mail.message')
600 for opportunity in opportunities:
601 for history in opportunity.message_ids:
602 message.write(cr, uid, history.id, {
603 'res_id': opportunity_id,
604 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
609 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
610 attachment = self.pool.get('ir.attachment')
612 # return attachments of opportunity
613 def _get_attachments(opportunity_id):
614 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
615 return attachment.browse(cr, uid, attachment_ids, context=context)
618 first_attachments = _get_attachments(opportunity_id)
619 for opportunity in opportunities:
620 attachments = _get_attachments(opportunity.id)
621 for first in first_attachments:
622 for attachment in attachments:
623 if attachment.name == first.name:
625 name = "%s (%s)" % (attachment.name, count,),
626 res_id = opportunity_id,
628 attachment.write(values)
633 def merge_opportunity(self, cr, uid, ids, context=None):
635 Different cases of merge:
636 - merge leads together = 1 new lead
637 - merge at least 1 opp with anything else (lead or opp) = 1 new opp
639 :param list ids: leads/opportunities ids to merge
640 :return int id: id of the resulting lead/opp
642 if context is None: context = {}
645 raise osv.except_osv(_('Warning!'),_('Please select more than one element (lead or opportunity) from the list view.'))
647 lead_ids = context.get('lead_ids', [])
649 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
650 opportunities = self.browse(cr, uid, ids, context=context)
651 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
652 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
653 if ctx_opportunities:
654 first_opportunity = ctx_opportunities[0]
655 tail_opportunities = opportunities_list + ctx_opportunities[1:]
657 first_opportunity = opportunities_list[0]
658 tail_opportunities = opportunities_list[1:]
660 merged_data = self._merge_data(cr, uid, ids, oldest, CRM_LEAD_FIELDS_TO_MERGE, context=context)
662 # Merge messages and attachements into the first opportunity
663 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
664 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
666 # Merge notifications about loss of information
667 self._merge_notify(cr, uid, first_opportunity, opportunities, context=context)
668 # Write merged data into first opportunity
669 self.write(cr, uid, [first_opportunity.id], merged_data, context=context)
670 # Delete tail opportunities
671 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
673 # Open first opportunity
674 self.case_open(cr, uid, [first_opportunity.id])
675 return first_opportunity.id
677 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
678 crm_stage = self.pool.get('crm.case.stage')
681 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
684 section_id = lead.section_id and lead.section_id.id or False
687 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
689 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
690 stage_id = stage_ids and stage_ids[0] or False
693 'planned_revenue': lead.planned_revenue,
694 'probability': lead.probability,
696 'partner_id': customer and customer.id or False,
697 'user_id': (lead.user_id and lead.user_id.id),
698 'type': 'opportunity',
699 'stage_id': stage_id or False,
700 'date_action': fields.datetime.now(),
701 'date_open': fields.datetime.now(),
702 'email_from': customer and customer.email or lead.email_from,
703 'phone': customer and customer.phone or lead.phone,
706 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
709 partner = self.pool.get('res.partner')
710 customer = partner.browse(cr, uid, partner_id, context=context)
711 for lead in self.browse(cr, uid, ids, context=context):
712 if lead.state in ('done', 'cancel'):
714 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
715 self.write(cr, uid, [lead.id], vals, context=context)
716 self.convert_opportunity_send_note(cr, uid, lead, context=context)
718 if user_ids or section_id:
719 self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
723 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
724 partner = self.pool.get('res.partner')
725 vals = { 'name': name,
726 'user_id': lead.user_id.id,
727 'comment': lead.description,
728 'section_id': lead.section_id.id or False,
729 'parent_id': parent_id,
731 'mobile': lead.mobile,
732 'email': lead.email_from and tools.email_split(lead.email_from)[0],
734 'title': lead.title and lead.title.id or False,
735 'function': lead.function,
736 'street': lead.street,
737 'street2': lead.street2,
740 'country_id': lead.country_id and lead.country_id.id or False,
741 'state_id': lead.state_id and lead.state_id.id or False,
742 'is_company': is_company,
745 partner = partner.create(cr, uid,vals, context)
748 def _create_lead_partner(self, cr, uid, lead, context=None):
750 if lead.partner_name and lead.contact_name:
751 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
752 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
753 elif lead.partner_name and not lead.contact_name:
754 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
755 elif not lead.partner_name and lead.contact_name:
756 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
758 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
761 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
763 Assign a partner to a lead.
765 :param object lead: browse record of the lead to process
766 :param int partner_id: identifier of the partner to assign
767 :return bool: True if the partner has properly been assigned
770 res_partner = self.pool.get('res.partner')
772 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
773 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
774 res = lead.write({'partner_id': partner_id}, context=context)
775 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
778 def handle_partner_assignation(self, cr, uid, ids, action='create', partner_id=False, context=None):
780 Handle partner assignation during a lead conversion.
781 if action is 'create', create new partner with contact and assign lead to new partner_id.
782 otherwise assign lead to the specified partner_id
784 :param list ids: leads/opportunities ids to process
785 :param string action: what has to be done regarding partners (create it, assign an existing one, or nothing)
786 :param int partner_id: partner to assign if any
787 :return dict: dictionary organized as followed: {lead_id: partner_assigned_id}
789 #TODO this is a duplication of the handle_partner_assignation method of crm_phonecall
791 # If a partner_id is given, force this partner for all elements
792 force_partner_id = partner_id
793 for lead in self.browse(cr, uid, ids, context=context):
794 # If the action is set to 'create' and no partner_id is set, create a new one
795 if action == 'create':
796 partner_id = force_partner_id or self._create_lead_partner(cr, uid, lead, context)
797 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
798 partner_ids[lead.id] = partner_id
801 def allocate_salesman(self, cr, uid, ids, user_ids=None, team_id=False, context=None):
803 Assign salesmen and salesteam to a batch of leads. If there are more
804 leads than salesmen, these salesmen will be assigned in round-robin.
805 E.g.: 4 salesmen (S1, S2, S3, S4) for 6 leads (L1, L2, ... L6). They
806 will be assigned as followed: L1 - S1, L2 - S2, L3 - S3, L4 - S4,
809 :param list ids: leads/opportunities ids to process
810 :param list user_ids: salesmen to assign
811 :param int team_id: salesteam to assign
819 value['section_id'] = team_id
821 value['user_id'] = user_ids[index]
822 # Cycle through user_ids
823 index = (index + 1) % len(user_ids)
825 self.write(cr, uid, [lead_id], value, context=context)
828 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):
830 :param string action: ('schedule','Schedule a call'), ('log','Log a call')
832 phonecall = self.pool.get('crm.phonecall')
833 model_data = self.pool.get('ir.model.data')
836 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
838 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
839 for lead in self.browse(cr, uid, ids, context=context):
841 section_id = lead.section_id and lead.section_id.id or False
843 user_id = lead.user_id and lead.user_id.id or False
845 'name': call_summary,
846 'opportunity_id': lead.id,
847 'user_id': user_id or False,
848 'categ_id': categ_id or False,
849 'description': desc or '',
850 'date': schedule_time,
851 'section_id': section_id or False,
852 'partner_id': lead.partner_id and lead.partner_id.id or False,
853 'partner_phone': phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
854 'partner_mobile': lead.partner_id and lead.partner_id.mobile or False,
855 'priority': lead.priority,
857 new_id = phonecall.create(cr, uid, vals, context=context)
858 phonecall.case_open(cr, uid, [new_id], context=context)
860 phonecall.case_close(cr, uid, [new_id], context=context)
861 phonecall_dict[lead.id] = new_id
862 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
863 return phonecall_dict
865 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
866 models_data = self.pool.get('ir.model.data')
868 # Get opportunity views
869 dummy, form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
870 dummy, tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
872 'name': _('Opportunity'),
874 'view_mode': 'tree, form',
875 'res_model': 'crm.lead',
876 'domain': [('type', '=', 'opportunity')],
877 'res_id': int(opportunity_id),
879 'views': [(form_view or False, 'form'),
880 (tree_view or False, 'tree'),
881 (False, 'calendar'), (False, 'graph')],
882 'type': 'ir.actions.act_window',
885 def redirect_lead_view(self, cr, uid, lead_id, context=None):
886 models_data = self.pool.get('ir.model.data')
889 dummy, form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_leads')
890 dummy, tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_leads')
894 'view_mode': 'tree, form',
895 'res_model': 'crm.lead',
896 'domain': [('type', '=', 'lead')],
897 'res_id': int(lead_id),
899 'views': [(form_view or False, 'form'),
900 (tree_view or False, 'tree'),
901 (False, 'calendar'), (False, 'graph')],
902 'type': 'ir.actions.act_window',
905 def action_makeMeeting(self, cr, uid, ids, context=None):
907 Open meeting's calendar view to schedule meeting on current opportunity.
908 :return dict: dictionary value for created Meeting view
910 opportunity = self.browse(cr, uid, ids[0], context)
911 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
913 'default_opportunity_id': opportunity.id,
914 'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
915 'default_partner_ids' : opportunity.partner_id and [opportunity.partner_id.id] or False,
916 'default_user_id': uid,
917 'default_section_id': opportunity.section_id and opportunity.section_id.id or False,
918 'default_email_from': opportunity.email_from,
919 'default_name': opportunity.name,
923 def write(self, cr, uid, ids, vals, context=None):
924 if vals.get('stage_id') and not vals.get('probability'):
925 # change probability of lead(s) if required by stage
926 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
928 vals['probability'] = stage.probability
929 res = super(crm_lead,self).write(cr, uid, ids, vals, context)
930 if vals.get('section_id'):
932 self._subscribe_salesteam_followers_to_lead(cr, uid, id, context=context)
935 # ----------------------------------------
937 # ----------------------------------------
939 def message_new(self, cr, uid, msg, custom_values=None, context=None):
940 """ Overrides mail_thread message_new that is called by the mailgateway
941 through message_process.
942 This override updates the document according to the email.
944 if custom_values is None: custom_values = {}
946 desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
947 custom_values.update({
948 'name': msg.get('subject') or _("No Subject"),
950 'email_from': msg.get('from'),
951 'email_cc': msg.get('cc'),
954 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
955 custom_values['priority'] = msg.get('priority')
956 return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
958 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
959 """ Overrides mail_thread message_update that is called by the mailgateway
960 through message_process.
961 This method updates the document according to the email.
963 if isinstance(ids, (str, int, long)):
965 if update_vals is None: update_vals = {}
967 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
968 update_vals['priority'] = msg.get('priority')
970 'cost':'planned_cost',
971 'revenue': 'planned_revenue',
972 'probability':'probability',
974 for line in msg.get('body', '').split('\n'):
976 res = tools.command_re.match(line)
977 if res and maps.get(res.group(1).lower()):
978 key = maps.get(res.group(1).lower())
979 update_vals[key] = res.group(2).lower()
981 return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
983 # ----------------------------------------
984 # OpenChatter methods and notifications
985 # ----------------------------------------
987 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
988 """ Override of the (void) default notification method. """
989 stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
990 return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), subtype="crm.mt_lead_stage", context=context)
992 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
993 if isinstance(lead, (int, long)):
994 lead = self.browse(cr, uid, [lead], context=context)[0]
995 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
997 def create_send_note(self, cr, uid, ids, context=None):
999 message = _("%s has been <b>created</b>.") % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
1000 self.message_post(cr, uid, [id], body=message, subtype="crm.mt_lead_create", context=context)
1003 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
1004 message = _("Opportunity has been <b>lost</b>.")
1005 return self.message_post(cr, uid, ids, body=message, subtype="crm.mt_lead_lost", context=context)
1007 def case_mark_won_send_note(self, cr, uid, ids, context=None):
1008 message = _("Opportunity has been <b>won</b>.")
1009 return self.message_post(cr, uid, ids, body=message, subtype="crm.mt_lead_won", context=context)
1011 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
1012 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
1013 if action == 'log': prefix = 'Logged'
1014 else: prefix = 'Scheduled'
1015 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
1016 return self.message_post(cr, uid, ids, body=message, context=context)
1018 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
1019 for lead in self.browse(cr, uid, ids, context=context):
1020 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))
1021 lead.message_post(body=message)
1024 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
1025 message = _("Lead has been <b>converted to an opportunity</b>.")
1026 lead.message_post(body=message, subtype="crm.mt_lead_convert_to_opportunity")
1029 def onchange_state(self, cr, uid, ids, state_id, context=None):
1031 country_id=self.pool.get('res.country.state').browse(cr, uid, state_id, context).country_id.id
1032 return {'value':{'country_id':country_id}}
1035 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: