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 from base.res.res_partner import format_address
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, format_address, osv.osv):
41 _description = "Lead/Opportunity"
42 _order = "priority,date_action,id desc"
43 _inherit = ['mail.thread','ir.needaction_mixin']
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)]
93 search_domain += ['|', ('id', 'in', ids), ('case_default', '=', True)]
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])))
105 for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
106 fold[stage.id] = stage.fold or False
110 def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
111 res = super(crm_lead,self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
112 if view_type == 'form':
113 res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
117 'stage_id': _read_group_stage_ids
120 def _compute_day(self, cr, uid, ids, fields, args, context=None):
122 @param cr: the current row, from the database cursor,
123 @param uid: the current user’s ID for security checks,
124 @param ids: List of Openday’s IDs
125 @return: difference between current date and log date
126 @param context: A standard dictionary for contextual values
128 cal_obj = self.pool.get('resource.calendar')
129 res_obj = self.pool.get('resource.resource')
132 for lead in self.browse(cr, uid, ids, context=context):
137 if field == 'day_open':
139 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
140 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
141 ans = date_open - date_create
142 date_until = lead.date_open
143 elif field == 'day_close':
145 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
146 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
147 date_until = lead.date_closed
148 ans = date_close - date_create
152 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
153 if len(resource_ids):
154 resource_id = resource_ids[0]
156 duration = float(ans.days)
157 if lead.section_id and lead.section_id.resource_calendar_id:
158 duration = float(ans.days) * 24
159 new_dates = cal_obj.interval_get(cr,
161 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
162 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
167 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
168 for in_time, out_time in new_dates:
169 if in_time.date not in no_days:
170 no_days.append(in_time.date)
171 if out_time > date_until:
173 duration = len(no_days)
174 res[lead.id][field] = abs(int(duration))
177 def _history_search(self, cr, uid, obj, name, args, context=None):
179 msg_obj = self.pool.get('mail.message')
180 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
181 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
184 return [('id', 'in', lead_ids)]
186 return [('id', '=', '0')]
189 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
190 select=True, help="Optional linked partner, usually after conversion of the lead"),
192 'id': fields.integer('ID', readonly=True),
193 'name': fields.char('Subject', size=64, required=True, select=1),
194 'active': fields.boolean('Active', required=False),
195 'date_action_last': fields.datetime('Last Action', readonly=1),
196 'date_action_next': fields.datetime('Next Action', readonly=1),
197 'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
198 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
199 select=True, help='When sending mails, the default email address is taken from the sales team.'),
200 'create_date': fields.datetime('Creation Date' , readonly=True),
201 '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"),
202 'description': fields.text('Notes'),
203 'write_date': fields.datetime('Update Date' , readonly=True),
204 'categ_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Categories', \
205 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
206 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
207 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
208 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
209 'contact_name': fields.char('Contact Name', size=64),
210 '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),
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="['&', ('fold', '=', False), '&', '|', ('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\'.'),
232 # Only used for type opportunity
233 'probability': fields.float('Success Rate (%)',group_operator="avg"),
234 'planned_revenue': fields.float('Expected Revenue'),
235 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
236 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
237 'phone': fields.char("Phone", size=64),
238 'date_deadline': fields.date('Expected Closing'),
239 'date_action': fields.date('Next Action Date', select=True),
240 'title_action': fields.char('Next Action', size=64),
241 'color': fields.integer('Color Index'),
242 'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
243 'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
244 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
245 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
246 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
248 # Fields for address, due to separation from crm and res.partner
249 'street': fields.char('Street', size=128),
250 'street2': fields.char('Street2', size=128),
251 'zip': fields.char('Zip', change_default=True, size=24),
252 'city': fields.char('City', size=128),
253 'state_id': fields.many2one("res.country.state", 'State', domain="[('country_id','=',country_id)]"),
254 'country_id': fields.many2one('res.country', 'Country'),
255 'phone': fields.char('Phone', size=64),
256 'fax': fields.char('Fax', size=64),
257 'mobile': fields.char('Mobile', size=64),
258 'function': fields.char('Function', size=128),
259 'title': fields.many2one('res.partner.title', 'Title'),
260 'company_id': fields.many2one('res.company', 'Company', select=1),
261 'payment_mode': fields.many2one('crm.payment.mode', 'Payment Mode', \
262 domain="[('section_id','=',section_id)]"),
263 'planned_cost': fields.float('Planned Costs'),
269 'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
270 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
271 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
272 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
273 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
274 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
278 def create(self, cr, uid, vals, context=None):
279 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
280 section_id = self.browse(cr, uid, obj_id, context=context).section_id
282 followers = [follow.id for follow in section_id.message_follower_ids]
283 self.message_subscribe(cr, uid, [obj_id], followers, context=context)
284 self.create_send_note(cr, uid, [obj_id], context=context)
287 def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
290 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
291 if not stage.on_change:
293 return {'value':{'probability': stage.probability}}
295 def on_change_partner(self, cr, uid, ids, partner_id, context=None):
299 partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
301 'partner_name' : partner.name,
302 'street' : partner.street,
303 'street2' : partner.street2,
304 'city' : partner.city,
305 'state_id' : partner.state_id and partner.state_id.id or False,
306 'country_id' : partner.country_id and partner.country_id.id or False,
307 'email_from' : partner.email,
308 'phone' : partner.phone,
309 'mobile' : partner.mobile,
312 return {'value' : values}
315 def _check(self, cr, uid, ids=False, context=None):
316 """ Override of the base.stage method.
317 Function called by the scheduler to process cases for date actions
318 Only works on not done and cancelled cases
320 cr.execute('select * from crm_case \
321 where (date_action_last<%s or date_action_last is null) \
322 and (date_action_next<=%s or date_action_next is null) \
323 and state not in (\'cancel\',\'done\')',
324 (time.strftime("%Y-%m-%d %H:%M:%S"),
325 time.strftime('%Y-%m-%d %H:%M:%S')))
327 ids2 = map(lambda x: x[0], cr.fetchall() or [])
328 cases = self.browse(cr, uid, ids2, context=context)
329 return self._action(cr, uid, cases, False, context=context)
331 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
332 """ Override of the base.stage method
333 Parameter of the stage search taken from the lead:
334 - type: stage type must be the same or 'both'
335 - section_id: if set, stages must belong to this section or
336 be a default stage; if not set, stages must be default
339 if isinstance(cases, (int, long)):
340 cases = self.browse(cr, uid, cases, context=context)
341 # collect all section_ids
345 section_ids.append(section_id)
348 section_ids.append(lead.section_id.id)
349 if lead.type not in types:
350 types.append(lead.type)
351 # OR all section_ids and OR with case_default
354 search_domain += [('|')] * len(section_ids)
355 for section_id in section_ids:
356 search_domain.append(('section_ids', '=', section_id))
357 search_domain.append(('case_default', '=', True))
358 # AND with cases types
359 search_domain.append(('type', 'in', types))
360 # AND with the domain in parameter
361 search_domain += list(domain)
362 # perform search, return the first found
363 stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
368 def case_cancel(self, cr, uid, ids, context=None):
369 """ Overrides case_cancel from base_stage to set probability """
370 res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
371 self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
374 def case_reset(self, cr, uid, ids, context=None):
375 """ Overrides case_reset from base_stage to set probability """
376 res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
377 self.write(cr, uid, ids, {'probability': 0.0}, context=context)
380 def case_mark_lost(self, cr, uid, ids, context=None):
381 """ Mark the case as lost: state=cancel and probability=0 """
382 for lead in self.browse(cr, uid, ids):
383 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
385 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
386 self.case_mark_lost_send_note(cr, uid, ids, context=context)
389 def case_mark_won(self, cr, uid, ids, context=None):
390 """ Mark the case as lost: state=done and probability=100 """
391 for lead in self.browse(cr, uid, ids):
392 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
394 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
395 self.case_mark_won_send_note(cr, uid, ids, context=context)
398 def set_priority(self, cr, uid, ids, priority):
399 """ Set lead priority
401 return self.write(cr, uid, ids, {'priority' : priority})
403 def set_high_priority(self, cr, uid, ids, context=None):
404 """ Set lead priority to high
406 return self.set_priority(cr, uid, ids, '1')
408 def set_normal_priority(self, cr, uid, ids, context=None):
409 """ Set lead priority to normal
411 return self.set_priority(cr, uid, ids, '3')
413 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
414 # prepare opportunity data into dictionary for merging
415 opportunities = self.browse(cr, uid, ids, context=context)
416 def _get_first_not_null(attr):
417 if hasattr(oldest, attr):
418 return getattr(oldest, attr)
419 for opportunity in opportunities:
420 if hasattr(opportunity, attr):
421 return getattr(opportunity, attr)
424 def _get_first_not_null_id(attr):
425 res = _get_first_not_null(attr)
426 return res and res.id or False
428 def _concat_all(attr):
429 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
432 for field_name in fields:
433 field_info = self._all_columns.get(field_name)
434 if field_info is None:
436 field = field_info.column
437 if field._type in ('many2many', 'one2many'):
439 elif field._type == 'many2one':
440 data[field_name] = _get_first_not_null_id(field_name) # !!
441 elif field._type == 'text':
442 data[field_name] = _concat_all(field_name) #not lost
444 data[field_name] = _get_first_not_null(field_name) #not lost
447 def _merge_find_oldest(self, cr, uid, ids, context=None):
450 #TOCHECK: where pass 'convert' in context ?
451 if context.get('convert'):
452 ids = list(set(ids) - set(context.get('lead_ids', False)) )
454 #search opportunities order by create date
455 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
456 oldest_id = opportunity_ids[0]
457 return self.browse(cr, uid, oldest_id, context=context)
459 def _mail_body(self, cr, uid, lead, fields, title=False, context=None):
462 body.append("%s\n" % (title))
463 for field_name in fields:
464 field_info = self._all_columns.get(field_name)
465 if field_info is None:
467 field = field_info.column
470 if field._type == 'selection':
471 if hasattr(field.selection, '__call__'):
472 key = field.selection(self, cr, uid, context=context)
474 key = field.selection
475 value = dict(key).get(lead[field_name], lead[field_name])
476 elif field._type == 'many2one':
478 value = lead[field_name].name_get()[0][1]
480 value = lead[field_name]
482 body.append("%s: %s" % (field.string, value or ''))
483 return "\n".join(body + ['---'])
485 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
486 #TOFIX: mail template should be used instead of fix body, subject text
488 merge_message = _('Merged opportunities')
489 subject = [merge_message]
490 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_ids', 'channel_id', 'company_id', 'contact_name',
491 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
492 'country_id', 'city', 'street', 'street2', 'zip']
493 for opportunity in opportunities:
494 subject.append(opportunity.name)
495 title = "%s : %s" % (merge_message, opportunity.name)
496 details.append(self._mail_body(cr, uid, opportunity, fields, title=title, context=context))
498 subject = subject[0] + ", ".join(subject[1:])
499 details = "\n\n".join(details)
500 return self.message_post(cr, uid, [opportunity_id], body=details, subject=subject, context=context)
502 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
503 message = self.pool.get('mail.message')
504 for opportunity in opportunities:
505 for history in opportunity.message_ids:
506 message.write(cr, uid, history.id, {
507 'res_id': opportunity_id,
508 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
513 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
514 attachment = self.pool.get('ir.attachment')
516 # return attachments of opportunity
517 def _get_attachments(opportunity_id):
518 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
519 return attachment.browse(cr, uid, attachment_ids, context=context)
522 first_attachments = _get_attachments(opportunity_id)
523 for opportunity in opportunities:
524 attachments = _get_attachments(opportunity.id)
525 for first in first_attachments:
526 for attachment in attachments:
527 if attachment.name == first.name:
529 name = "%s (%s)" % (attachment.name, count,),
530 res_id = opportunity_id,
532 attachment.write(values)
537 def merge_opportunity(self, cr, uid, ids, context=None):
539 To merge opportunities
540 :param ids: list of opportunities ids to merge
542 if context is None: context = {}
544 #TOCHECK: where pass lead_ids in context?
545 lead_ids = context and context.get('lead_ids', []) or []
548 raise osv.except_osv(_('Warning!'),_('Please select more than one opportunity from the list view.'))
550 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
551 opportunities = self.browse(cr, uid, ids, context=context)
552 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
553 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
554 if ctx_opportunities :
555 first_opportunity = ctx_opportunities[0]
556 tail_opportunities = opportunities_list + ctx_opportunities[1:]
558 first_opportunity = opportunities_list[0]
559 tail_opportunities = opportunities_list[1:]
561 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',
562 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
563 'date_action_next', 'email_from', 'email_cc', 'partner_name']
565 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
567 # merge data into first opportunity
568 self.write(cr, uid, [first_opportunity.id], data, context=context)
570 #copy message and attachements into the first opportunity
571 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
572 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
574 #Notification about loss of information
575 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
576 #delete tail opportunities
577 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
579 #open first opportunity
580 self.case_open(cr, uid, [first_opportunity.id])
581 return first_opportunity.id
583 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
584 crm_stage = self.pool.get('crm.case.stage')
587 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
589 section_id = lead.section_id and lead.section_id.id or False
591 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
593 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
594 stage_id = stage_ids and stage_ids[0] or False
596 'planned_revenue': lead.planned_revenue,
597 'probability': lead.probability,
599 'partner_id': customer and customer.id or False,
600 'user_id': (lead.user_id and lead.user_id.id),
601 'type': 'opportunity',
602 'stage_id': stage_id or False,
603 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
604 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
605 'email_from': customer and customer.email or lead.email_from,
606 'phone': customer and customer.phone or lead.phone,
609 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
610 partner = self.pool.get('res.partner')
611 mail_message = self.pool.get('mail.message')
614 customer = partner.browse(cr, uid, partner_id, context=context)
615 for lead in self.browse(cr, uid, ids, context=context):
616 if lead.state in ('done', 'cancel'):
618 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
619 self.write(cr, uid, [lead.id], vals, context=context)
620 self.convert_opportunity_send_note(cr, uid, lead, context=context)
622 if user_ids or section_id:
623 self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
627 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
628 partner = self.pool.get('res.partner')
629 vals = { 'name': name,
630 'user_id': lead.user_id.id,
631 'comment': lead.description,
632 'section_id': lead.section_id.id or False,
633 'parent_id': parent_id,
635 'mobile': lead.mobile,
636 'email': lead.email_from and tools.email_split(lead.email_from)[0],
638 'title': lead.title and lead.title.id or False,
639 'function': lead.function,
640 'street': lead.street,
641 'street2': lead.street2,
644 'country_id': lead.country_id and lead.country_id.id or False,
645 'state_id': lead.state_id and lead.state_id.id or False,
646 'is_company': is_company,
649 partner = partner.create(cr, uid,vals, context)
652 def _create_lead_partner(self, cr, uid, lead, context=None):
654 if lead.partner_name and lead.contact_name:
655 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
656 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
657 elif lead.partner_name and not lead.contact_name:
658 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
659 elif not lead.partner_name and lead.contact_name:
660 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
662 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
665 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
667 res_partner = self.pool.get('res.partner')
669 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
670 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
671 res = lead.write({'partner_id' : partner_id, }, context=context)
672 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
675 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
677 This function convert partner based on action.
678 if action is 'create', create new partner with contact and assign lead to new partner_id.
679 otherwise assign lead to specified partner_id
684 force_partner_id = partner_id
685 for lead in self.browse(cr, uid, ids, context=context):
686 if action == 'create':
688 partner_id = self._create_lead_partner(cr, uid, lead, context)
689 partner_id = force_partner_id or self._create_lead_partner(cr, uid, lead, context=context)
690 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
691 partner_ids[lead.id] = partner_id
694 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
699 value['section_id'] = team_id
700 if index < len(user_ids):
701 value['user_id'] = user_ids[index]
704 self.write(cr, uid, [lead_id], value, context=context)
707 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):
709 action :('schedule','Schedule a call'), ('log','Log a call')
711 phonecall = self.pool.get('crm.phonecall')
712 model_data = self.pool.get('ir.model.data')
715 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
717 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
718 for lead in self.browse(cr, uid, ids, context=context):
720 section_id = lead.section_id and lead.section_id.id or False
722 user_id = lead.user_id and lead.user_id.id or False
724 'name' : call_summary,
725 'opportunity_id' : lead.id,
726 'user_id' : user_id or False,
727 'categ_id' : categ_id or False,
728 'description' : desc or '',
729 'date' : schedule_time,
730 'section_id' : section_id or False,
731 'partner_id': lead.partner_id and lead.partner_id.id or False,
732 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
733 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
734 'priority': lead.priority,
736 new_id = phonecall.create(cr, uid, vals, context=context)
737 phonecall.case_open(cr, uid, [new_id], context=context)
739 phonecall.case_close(cr, uid, [new_id], context=context)
740 phonecall_dict[lead.id] = new_id
741 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
742 return phonecall_dict
745 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
746 models_data = self.pool.get('ir.model.data')
748 # Get Opportunity views
749 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
750 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
752 'name': _('Opportunity'),
754 'view_mode': 'tree, form',
755 'res_model': 'crm.lead',
756 'domain': [('type', '=', 'opportunity')],
757 'res_id': int(opportunity_id),
759 'views': [(form_view and form_view[1] or False, 'form'),
760 (tree_view and tree_view[1] or False, 'tree'),
761 (False, 'calendar'), (False, 'graph')],
762 'type': 'ir.actions.act_window',
765 def action_makeMeeting(self, cr, uid, ids, context=None):
766 """ This opens Meeting's calendar view to schedule meeting on current Opportunity
767 @return : Dictionary value for created Meeting view
769 opportunity = self.browse(cr, uid, ids[0], context)
770 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
772 'default_opportunity_id': opportunity.id,
773 'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
774 'default_partner_ids' : opportunity.partner_id and [opportunity.partner_id.id] or False,
775 'default_user_id': uid,
776 'default_section_id': opportunity.section_id and opportunity.section_id.id or False,
777 'default_email_from': opportunity.email_from,
778 'default_state': 'open',
779 'default_name': opportunity.name,
783 def unlink(self, cr, uid, ids, context=None):
784 for lead in self.browse(cr, uid, ids, context):
785 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
786 raise osv.except_osv(_('Error!'),
787 _("You cannot delete lead '%s' because it is not in 'Draft' state. " \
788 "You can still cancel it, instead of deleting it.") % lead.name)
789 return super(crm_lead, self).unlink(cr, uid, ids, context)
791 def write(self, cr, uid, ids, vals, context=None):
792 if vals.get('stage_id') and not vals.get('probability'):
793 # change probability of lead(s) if required by stage
794 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
796 vals['probability'] = stage.probability
797 if vals.get('section_id'):
798 section_id = self.pool.get('crm.case.section').browse(cr, uid, vals.get('section_id'), context=context)
800 vals['message_follower_ids'] = [(4, follower.id) for follower in section_id.message_follower_ids]
801 return super(crm_lead,self).write(cr, uid, ids, vals, context)
803 # ----------------------------------------
805 # ----------------------------------------
807 def message_new(self, cr, uid, msg, custom_values=None, context=None):
808 """ Overrides mail_thread message_new that is called by the mailgateway
809 through message_process.
810 This override updates the document according to the email.
812 if custom_values is None: custom_values = {}
813 custom_values.update({
814 'name': msg.get('subject') or _("No Subject"),
815 'description': msg.get('body'),
816 'email_from': msg.get('from'),
817 'email_cc': msg.get('cc'),
820 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
821 custom_values['priority'] = msg.get('priority')
822 return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
824 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
825 """ Overrides mail_thread message_update that is called by the mailgateway
826 through message_process.
827 This method updates the document according to the email.
829 if isinstance(ids, (str, int, long)):
831 if update_vals is None: update_vals = {}
833 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
834 update_vals['priority'] = msg.get('priority')
836 'cost':'planned_cost',
837 'revenue': 'planned_revenue',
838 'probability':'probability',
840 for line in msg.get('body', '').split('\n'):
842 res = tools.misc.command_re.match(line)
843 if res and maps.get(res.group(1).lower()):
844 key = maps.get(res.group(1).lower())
845 update_vals[key] = res.group(2).lower()
847 return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
849 # ----------------------------------------
850 # OpenChatter methods and notifications
851 # ----------------------------------------
853 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
854 """ Override of the (void) default notification method. """
855 stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
856 return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
858 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
859 if isinstance(lead, (int, long)):
860 lead = self.browse(cr, uid, [lead], context=context)[0]
861 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
863 def create_send_note(self, cr, uid, ids, context=None):
865 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
866 self.message_post(cr, uid, [id], body=message, context=context)
869 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
870 message = _("Opportunity has been <b>lost</b>.")
871 return self.message_post(cr, uid, ids, body=message, context=context)
873 def case_mark_won_send_note(self, cr, uid, ids, context=None):
874 message = _("Opportunity has been <b>won</b>.")
875 return self.message_post(cr, uid, ids, body=message, context=context)
877 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
878 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
879 if action == 'log': prefix = 'Logged'
880 else: prefix = 'Scheduled'
881 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
882 return self.message_post(cr, uid, ids, body=message, context=context)
884 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
885 for lead in self.browse(cr, uid, ids, context=context):
886 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))
887 lead.message_post(body=message)
890 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
891 message = _("Lead has been <b>converted to an opportunity</b>.")
892 lead.message_post(body=message)
897 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: