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 _
30 CRM_LEAD_PENDING_STATES = (
31 crm.AVAILABLE_STATES[2][0], # Cancelled
32 crm.AVAILABLE_STATES[3][0], # Done
33 crm.AVAILABLE_STATES[4][0], # Pending
36 class crm_lead(base_stage, osv.osv):
39 _description = "Lead/Opportunity"
40 _order = "priority,date_action,id desc"
41 _inherit = ['mail.thread','ir.needaction_mixin']
43 def _get_default_section_id(self, cr, uid, context=None):
44 """ Gives default section by checking if present in the context """
45 return (self._resolve_section_id_from_context(cr, uid, context=context) or False)
47 def _get_default_stage_id(self, cr, uid, context=None):
48 """ Gives default stage_id """
49 section_id = self._get_default_section_id(cr, uid, context=context)
50 return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft'), ('type', '=', 'both')], context=context)
52 def _resolve_section_id_from_context(self, cr, uid, context=None):
53 """ Returns ID of section based on the value of 'section_id'
54 context key, or None if it cannot be resolved to a single
59 if type(context.get('default_section_id')) in (int, long):
60 return context.get('default_section_id')
61 if isinstance(context.get('default_section_id'), basestring):
62 section_name = context['default_section_id']
63 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=section_name, context=context)
64 if len(section_ids) == 1:
65 return int(section_ids[0][0])
68 def _resolve_type_from_context(self, cr, uid, context=None):
69 """ Returns the type (lead or opportunity) from the type context
70 key. Returns None if it cannot be resolved.
74 return context.get('default_type')
76 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
77 access_rights_uid = access_rights_uid or uid
78 stage_obj = self.pool.get('crm.case.stage')
79 order = stage_obj._order
80 # lame hack to allow reverting search, should just work in the trivial case
81 if read_group_order == 'stage_id desc':
82 order = "%s desc" % order
83 # retrieve section_id from the context and write the domain
84 # - ('id', 'in', 'ids'): add columns that should be present
85 # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
86 # - OR ('section_ids', '=', section_id), ('fold', '=', False) if section_id: add section columns that are not folded
88 section_id = self._resolve_section_id_from_context(cr, uid, context=context)
90 search_domain += ['|', '&', ('section_ids', '=', section_id), ('fold', '=', False)]
91 search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)]
92 # retrieve type from the context (if set: choose 'type' or 'both')
93 type = self._resolve_type_from_context(cr, uid, context=context)
95 search_domain += ['|', ('type', '=', type), ('type', '=', 'both')]
97 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
98 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
99 # restore order of the search
100 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
104 'stage_id': _read_group_stage_ids
107 def _compute_day(self, cr, uid, ids, fields, args, context=None):
109 @param cr: the current row, from the database cursor,
110 @param uid: the current user’s ID for security checks,
111 @param ids: List of Openday’s IDs
112 @return: difference between current date and log date
113 @param context: A standard dictionary for contextual values
115 cal_obj = self.pool.get('resource.calendar')
116 res_obj = self.pool.get('resource.resource')
119 for lead in self.browse(cr, uid, ids, context=context):
124 if field == 'day_open':
126 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
127 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
128 ans = date_open - date_create
129 date_until = lead.date_open
130 elif field == 'day_close':
132 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
133 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
134 date_until = lead.date_closed
135 ans = date_close - date_create
139 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
140 if len(resource_ids):
141 resource_id = resource_ids[0]
143 duration = float(ans.days)
144 if lead.section_id and lead.section_id.resource_calendar_id:
145 duration = float(ans.days) * 24
146 new_dates = cal_obj.interval_get(cr,
148 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
149 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
154 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
155 for in_time, out_time in new_dates:
156 if in_time.date not in no_days:
157 no_days.append(in_time.date)
158 if out_time > date_until:
160 duration = len(no_days)
161 res[lead.id][field] = abs(int(duration))
164 def _history_search(self, cr, uid, obj, name, args, context=None):
166 msg_obj = self.pool.get('mail.message')
167 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
168 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
171 return [('id', 'in', lead_ids)]
173 return [('id', '=', '0')]
176 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
177 select=True, help="Optional linked partner, usually after conversion of the lead"),
179 'id': fields.integer('ID', readonly=True),
180 'name': fields.char('Subject', size=64, required=True, select=1),
181 'active': fields.boolean('Active', required=False),
182 'date_action_last': fields.datetime('Last Action', readonly=1),
183 'date_action_next': fields.datetime('Next Action', readonly=1),
184 'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
185 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
186 select=True, help='When sending mails, the default email address is taken from the sales team.'),
187 'create_date': fields.datetime('Creation Date' , readonly=True),
188 '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"),
189 'description': fields.text('Notes'),
190 'write_date': fields.datetime('Update Date' , readonly=True),
191 'categ_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Categories', \
192 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
193 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
194 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
195 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
196 'contact_name': fields.char('Contact Name', size=64),
197 '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),
198 '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."),
199 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
200 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
201 'date_closed': fields.datetime('Closed', readonly=True),
202 'stage_id': fields.many2one('crm.case.stage', 'Stage',
203 domain="['&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
204 'user_id': fields.many2one('res.users', 'Salesperson'),
205 'referred': fields.char('Referred By', size=64),
206 'date_open': fields.datetime('Opened', readonly=True),
207 'day_open': fields.function(_compute_day, string='Days to Open', \
208 multi='day_open', type="float", store=True),
209 'day_close': fields.function(_compute_day, string='Days to Close', \
210 multi='day_close', type="float", store=True),
211 'state': fields.related('stage_id', 'state', type="selection", store=True,
212 selection=crm.AVAILABLE_STATES, string="State", readonly=True,
213 help='The state is set to \'Draft\', when a case is created.\
214 If the case is in progress the state is set to \'Open\'.\
215 When the case is over, the state is set to \'Done\'.\
216 If the case needs to be reviewed then the state is \
217 set to \'Pending\'.'),
219 # Only used for type opportunity
220 'probability': fields.float('Success Rate (%)',group_operator="avg"),
221 'planned_revenue': fields.float('Expected Revenue'),
222 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
223 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
224 'phone': fields.char("Phone", size=64),
225 'date_deadline': fields.date('Expected Closing'),
226 'date_action': fields.date('Next Action Date', select=True),
227 'title_action': fields.char('Next Action', size=64),
228 'color': fields.integer('Color Index'),
229 'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
230 'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
231 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
232 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
233 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
235 # Fields for address, due to separation from crm and res.partner
236 'street': fields.char('Street', size=128),
237 'street2': fields.char('Street2', size=128),
238 'zip': fields.char('Zip', change_default=True, size=24),
239 'city': fields.char('City', size=128),
240 'state_id': fields.many2one("res.country.state", 'State', domain="[('country_id','=',country_id)]"),
241 'country_id': fields.many2one('res.country', 'Country'),
242 'phone': fields.char('Phone', size=64),
243 'fax': fields.char('Fax', size=64),
244 'mobile': fields.char('Mobile', size=64),
245 'function': fields.char('Function', size=128),
246 'title': fields.many2one('res.partner.title', 'Title'),
247 'company_id': fields.many2one('res.company', 'Company', select=1),
248 'payment_mode': fields.many2one('crm.payment.mode', 'Payment Mode', \
249 domain="[('section_id','=',section_id)]"),
250 'planned_cost': fields.float('Planned Costs'),
256 'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
257 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
258 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
259 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
260 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
261 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
265 def create(self, cr, uid, vals, context=None):
266 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
267 self.create_send_note(cr, uid, [obj_id], context=context)
270 def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
273 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
274 if not stage.on_change:
276 return {'value':{'probability': stage.probability}}
278 def on_change_partner(self, cr, uid, ids, partner_id, context=None):
282 partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
284 'partner_name' : partner.name,
285 'street' : partner.street,
286 'street2' : partner.street2,
287 'city' : partner.city,
288 'state_id' : partner.state_id and partner.state_id.id or False,
289 'country_id' : partner.country_id and partner.country_id.id or False,
291 return {'value' : values}
294 def _check(self, cr, uid, ids=False, context=None):
295 """ Override of the base.stage method.
296 Function called by the scheduler to process cases for date actions
297 Only works on not done and cancelled cases
299 cr.execute('select * from crm_case \
300 where (date_action_last<%s or date_action_last is null) \
301 and (date_action_next<=%s or date_action_next is null) \
302 and state not in (\'cancel\',\'done\')',
303 (time.strftime("%Y-%m-%d %H:%M:%S"),
304 time.strftime('%Y-%m-%d %H:%M:%S')))
306 ids2 = map(lambda x: x[0], cr.fetchall() or [])
307 cases = self.browse(cr, uid, ids2, context=context)
308 return self._action(cr, uid, cases, False, context=context)
310 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
311 """ Override of the base.stage method
312 Parameter of the stage search taken from the lead:
313 - type: stage type must be the same or 'both'
314 - section_id: if set, stages must belong to this section or
315 be a default stage; if not set, stages must be default
318 if isinstance(cases, (int, long)):
319 cases = self.browse(cr, uid, cases, context=context)
320 # collect all section_ids
324 section_ids.append(section_id)
327 section_ids.append(lead.section_id.id)
328 if lead.type not in types:
329 types.append(lead.type)
330 # OR all section_ids and OR with case_default
333 search_domain += [('|')] * len(section_ids)
334 for section_id in section_ids:
335 search_domain.append(('section_ids', '=', section_id))
336 search_domain.append(('case_default', '=', True))
337 # AND with cases types
338 search_domain.append(('type', 'in', types))
339 # AND with the domain in parameter
340 search_domain += list(domain)
341 # perform search, return the first found
342 stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
347 def case_cancel(self, cr, uid, ids, context=None):
348 """ Overrides case_cancel from base_stage to set probability """
349 res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
350 self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
353 def case_reset(self, cr, uid, ids, context=None):
354 """ Overrides case_reset from base_stage to set probability """
355 res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
356 self.write(cr, uid, ids, {'probability': 0.0}, context=context)
359 def case_mark_lost(self, cr, uid, ids, context=None):
360 """ Mark the case as lost: state=cancel and probability=0 """
361 for lead in self.browse(cr, uid, ids):
362 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
364 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
365 self.case_mark_lost_send_note(cr, uid, ids, context=context)
368 def case_mark_won(self, cr, uid, ids, context=None):
369 """ Mark the case as lost: state=done and probability=100 """
370 for lead in self.browse(cr, uid, ids):
371 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
373 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
374 self.case_mark_won_send_note(cr, uid, ids, context=context)
377 def set_priority(self, cr, uid, ids, priority):
378 """ Set lead priority
380 return self.write(cr, uid, ids, {'priority' : priority})
382 def set_high_priority(self, cr, uid, ids, context=None):
383 """ Set lead priority to high
385 return self.set_priority(cr, uid, ids, '1')
387 def set_normal_priority(self, cr, uid, ids, context=None):
388 """ Set lead priority to normal
390 return self.set_priority(cr, uid, ids, '3')
392 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
393 # prepare opportunity data into dictionary for merging
394 opportunities = self.browse(cr, uid, ids, context=context)
395 def _get_first_not_null(attr):
396 if hasattr(oldest, attr):
397 return getattr(oldest, attr)
398 for opportunity in opportunities:
399 if hasattr(opportunity, attr):
400 return getattr(opportunity, attr)
403 def _get_first_not_null_id(attr):
404 res = _get_first_not_null(attr)
405 return res and res.id or False
407 def _concat_all(attr):
408 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
411 for field_name in fields:
412 field_info = self._all_columns.get(field_name)
413 if field_info is None:
415 field = field_info.column
416 if field._type in ('many2many', 'one2many'):
418 elif field._type == 'many2one':
419 data[field_name] = _get_first_not_null_id(field_name) # !!
420 elif field._type == 'text':
421 data[field_name] = _concat_all(field_name) #not lost
423 data[field_name] = _get_first_not_null(field_name) #not lost
426 def _merge_find_oldest(self, cr, uid, ids, context=None):
429 #TOCHECK: where pass 'convert' in context ?
430 if context.get('convert'):
431 ids = list(set(ids) - set(context.get('lead_ids', False)) )
433 #search opportunities order by create date
434 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
435 oldest_id = opportunity_ids[0]
436 return self.browse(cr, uid, oldest_id, context=context)
438 def _mail_body(self, cr, uid, lead, fields, title=False, context=None):
441 body.append("%s\n" % (title))
442 for field_name in fields:
443 field_info = self._all_columns.get(field_name)
444 if field_info is None:
446 field = field_info.column
449 if field._type == 'selection':
450 if hasattr(field.selection, '__call__'):
451 key = field.selection(self, cr, uid, context=context)
453 key = field.selection
454 value = dict(key).get(lead[field_name], lead[field_name])
455 elif field._type == 'many2one':
457 value = lead[field_name].name_get()[0][1]
459 value = lead[field_name]
461 body.append("%s: %s" % (field.string, value or ''))
462 return "\n".join(body + ['---'])
464 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
465 #TOFIX: mail template should be used instead of fix body, subject text
467 merge_message = _('Merged opportunities')
468 subject = [merge_message]
469 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_ids', 'channel_id', 'company_id', 'contact_name',
470 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
471 'country_id', 'city', 'street', 'street2', 'zip']
472 for opportunity in opportunities:
473 subject.append(opportunity.name)
474 title = "%s : %s" % (merge_message, opportunity.name)
475 details.append(self._mail_body(cr, uid, opportunity, fields, title=title, context=context))
477 subject = subject[0] + ", ".join(subject[1:])
478 details = "\n\n".join(details)
479 return self.message_post(cr, uid, [opportunity_id], body=details, subject=subject, context=context)
481 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
482 message = self.pool.get('mail.message')
483 for opportunity in opportunities:
484 for history in opportunity.message_ids:
485 message.write(cr, uid, history.id, {
486 'res_id': opportunity_id,
487 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
492 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
493 attachment = self.pool.get('ir.attachment')
495 # return attachments of opportunity
496 def _get_attachments(opportunity_id):
497 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
498 return attachment.browse(cr, uid, attachment_ids, context=context)
501 first_attachments = _get_attachments(opportunity_id)
502 for opportunity in opportunities:
503 attachments = _get_attachments(opportunity.id)
504 for first in first_attachments:
505 for attachment in attachments:
506 if attachment.name == first.name:
508 name = "%s (%s)" % (attachment.name, count,),
509 res_id = opportunity_id,
511 attachment.write(values)
516 def merge_opportunity(self, cr, uid, ids, context=None):
518 To merge opportunities
519 :param ids: list of opportunities ids to merge
521 if context is None: context = {}
523 #TOCHECK: where pass lead_ids in context?
524 lead_ids = context and context.get('lead_ids', []) or []
527 raise osv.except_osv(_('Warning!'),_('Please select more than one opportunity from the list view.'))
529 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
530 opportunities = self.browse(cr, uid, ids, context=context)
531 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
532 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
533 if ctx_opportunities :
534 first_opportunity = ctx_opportunities[0]
535 tail_opportunities = opportunities_list + ctx_opportunities[1:]
537 first_opportunity = opportunities_list[0]
538 tail_opportunities = opportunities_list[1:]
540 fields = ['partner_id', 'title', 'name', 'categ_ids', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
541 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
542 'date_action_next', 'email_from', 'email_cc', 'partner_name']
544 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
546 # merge data into first opportunity
547 self.write(cr, uid, [first_opportunity.id], data, context=context)
549 #copy message and attachements into the first opportunity
550 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
551 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
553 #Notification about loss of information
554 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
555 #delete tail opportunities
556 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
558 #open first opportunity
559 self.case_open(cr, uid, [first_opportunity.id])
560 return first_opportunity.id
562 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
563 crm_stage = self.pool.get('crm.case.stage')
566 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
568 section_id = lead.section_id and lead.section_id.id or False
570 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
572 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
573 stage_id = stage_ids and stage_ids[0] or False
575 'planned_revenue': lead.planned_revenue,
576 'probability': lead.probability,
578 'partner_id': customer and customer.id or False,
579 'user_id': (lead.user_id and lead.user_id.id),
580 'type': 'opportunity',
581 'stage_id': stage_id or False,
582 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
583 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
586 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
587 partner = self.pool.get('res.partner')
588 mail_message = self.pool.get('mail.message')
591 customer = partner.browse(cr, uid, partner_id, context=context)
592 for lead in self.browse(cr, uid, ids, context=context):
593 if lead.state in ('done', 'cancel'):
595 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
596 self.write(cr, uid, [lead.id], vals, context=context)
597 self.convert_opportunity_send_note(cr, uid, lead, context=context)
599 if user_ids or section_id:
600 self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
604 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
605 partner = self.pool.get('res.partner')
606 vals = { 'name': name,
607 'user_id': lead.user_id.id,
608 'comment': lead.description,
609 'section_id': lead.section_id.id or False,
610 'parent_id': parent_id,
612 'mobile': lead.mobile,
613 'email': lead.email_from and tools.email_split(lead.email_from)[0],
615 'title': lead.title and lead.title.id or False,
616 'function': lead.function,
617 'street': lead.street,
618 'street2': lead.street2,
621 'country_id': lead.country_id and lead.country_id.id or False,
622 'state_id': lead.state_id and lead.state_id.id or False,
623 'is_company': is_company,
626 partner = partner.create(cr, uid,vals, context)
629 def _create_lead_partner(self, cr, uid, lead, context=None):
631 if lead.partner_name and lead.contact_name:
632 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
633 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
634 elif lead.partner_name and not lead.contact_name:
635 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
636 elif not lead.partner_name and lead.contact_name:
637 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
639 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
642 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
644 res_partner = self.pool.get('res.partner')
646 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
647 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
648 res = lead.write({'partner_id' : partner_id, }, context=context)
649 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
652 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
654 This function convert partner based on action.
655 if action is 'create', create new partner with contact and assign lead to new partner_id.
656 otherwise assign lead to specified partner_id
661 force_partner_id = partner_id
662 for lead in self.browse(cr, uid, ids, context=context):
663 if action == 'create':
665 partner_id = self._create_lead_partner(cr, uid, lead, context)
666 partner_id = force_partner_id or self._create_lead_partner(cr, uid, lead, context=context)
667 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
668 partner_ids[lead.id] = partner_id
671 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
676 value['section_id'] = team_id
677 if index < len(user_ids):
678 value['user_id'] = user_ids[index]
681 self.write(cr, uid, [lead_id], value, context=context)
684 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):
686 action :('schedule','Schedule a call'), ('log','Log a call')
688 phonecall = self.pool.get('crm.phonecall')
689 model_data = self.pool.get('ir.model.data')
692 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
694 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
695 for lead in self.browse(cr, uid, ids, context=context):
697 section_id = lead.section_id and lead.section_id.id or False
699 user_id = lead.user_id and lead.user_id.id or False
701 'name' : call_summary,
702 'opportunity_id' : lead.id,
703 'user_id' : user_id or False,
704 'categ_id' : categ_id or False,
705 'description' : desc or '',
706 'date' : schedule_time,
707 'section_id' : section_id or False,
708 'partner_id': lead.partner_id and lead.partner_id.id or False,
709 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
710 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
711 'priority': lead.priority,
713 new_id = phonecall.create(cr, uid, vals, context=context)
714 phonecall.case_open(cr, uid, [new_id], context=context)
716 phonecall.case_close(cr, uid, [new_id], context=context)
717 phonecall_dict[lead.id] = new_id
718 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
719 return phonecall_dict
722 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
723 models_data = self.pool.get('ir.model.data')
725 # Get Opportunity views
726 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
727 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
729 'name': _('Opportunity'),
731 'view_mode': 'tree, form',
732 'res_model': 'crm.lead',
733 'domain': [('type', '=', 'opportunity')],
734 'res_id': int(opportunity_id),
736 'views': [(form_view and form_view[1] or False, 'form'),
737 (tree_view and tree_view[1] or False, 'tree'),
738 (False, 'calendar'), (False, 'graph')],
739 'type': 'ir.actions.act_window',
742 def action_makeMeeting(self, cr, uid, ids, context=None):
743 """ This opens Meeting's calendar view to schedule meeting on current Opportunity
744 @return : Dictionary value for created Meeting view
746 opportunity = self.browse(cr, uid, ids[0], context)
747 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
749 'default_opportunity_id': opportunity.id,
750 'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
751 'default_partner_ids' : opportunity.partner_id and [opportunity.partner_id.id] or False,
752 'default_user_id': uid,
753 'default_section_id': opportunity.section_id and opportunity.section_id.id or False,
754 'default_email_from': opportunity.email_from,
755 'default_state': 'open',
756 'default_name': opportunity.name,
760 def unlink(self, cr, uid, ids, context=None):
761 for lead in self.browse(cr, uid, ids, context):
762 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
763 raise osv.except_osv(_('Error!'),
764 _("You cannot delete lead '%s' because it is not in 'Draft' state. " \
765 "You can still cancel it, instead of deleting it.") % lead.name)
766 return super(crm_lead, self).unlink(cr, uid, ids, context)
768 def write(self, cr, uid, ids, vals, context=None):
769 if vals.get('stage_id') and not vals.get('probability'):
770 # change probability of lead(s) if required by stage
771 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
773 vals['probability'] = stage.probability
774 return super(crm_lead,self).write(cr, uid, ids, vals, context)
776 # ----------------------------------------
778 # ----------------------------------------
780 def message_new(self, cr, uid, msg, custom_values=None, context=None):
781 """ Overrides mail_thread message_new that is called by the mailgateway
782 through message_process.
783 This override updates the document according to the email.
785 if custom_values is None: custom_values = {}
786 custom_values.update({
787 'name': msg.get('subject') or _("No Subject"),
788 'description': msg.get('body'),
789 'email_from': msg.get('from'),
790 'email_cc': msg.get('cc'),
793 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
794 custom_values['priority'] = msg.get('priority')
795 return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
797 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
798 """ Overrides mail_thread message_update that is called by the mailgateway
799 through message_process.
800 This method updates the document according to the email.
802 if isinstance(ids, (str, int, long)):
804 if update_vals is None: update_vals = {}
806 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
807 update_vals['priority'] = msg.get('priority')
809 'cost':'planned_cost',
810 'revenue': 'planned_revenue',
811 'probability':'probability',
813 for line in msg.get('body', '').split('\n'):
815 res = tools.misc.command_re.match(line)
816 if res and maps.get(res.group(1).lower()):
817 key = maps.get(res.group(1).lower())
818 update_vals[key] = res.group(2).lower()
820 return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
822 # ----------------------------------------
823 # OpenChatter methods and notifications
824 # ----------------------------------------
826 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
827 """ Override of the (void) default notification method. """
828 stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
829 return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
831 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
832 if isinstance(lead, (int, long)):
833 lead = self.browse(cr, uid, [lead], context=context)[0]
834 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
836 def create_send_note(self, cr, uid, ids, context=None):
838 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
839 self.message_post(cr, uid, [id], body=message, context=context)
842 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
843 message = _("Opportunity has been <b>lost</b>.")
844 return self.message_post(cr, uid, ids, body=message, context=context)
846 def case_mark_won_send_note(self, cr, uid, ids, context=None):
847 message = _("Opportunity has been <b>won</b>.")
848 return self.message_post(cr, uid, ids, body=message, context=context)
850 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
851 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
852 if action == 'log': prefix = 'Logged'
853 else: prefix = 'Scheduled'
854 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
855 return self.message_post(cr, uid, ids, body=message, context=context)
857 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
858 for lead in self.browse(cr, uid, ids, context=context):
859 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))
860 lead.message_post(body=message)
863 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
864 message = _("Lead has been <b>converted to an opportunity</b>.")
865 lead.message_post(body=message)
870 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: