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}
314 def _check(self, cr, uid, ids=False, context=None):
315 """ Override of the base.stage method.
316 Function called by the scheduler to process cases for date actions
317 Only works on not done and cancelled cases
319 cr.execute('select * from crm_case \
320 where (date_action_last<%s or date_action_last is null) \
321 and (date_action_next<=%s or date_action_next is null) \
322 and state not in (\'cancel\',\'done\')',
323 (time.strftime("%Y-%m-%d %H:%M:%S"),
324 time.strftime('%Y-%m-%d %H:%M:%S')))
326 ids2 = map(lambda x: x[0], cr.fetchall() or [])
327 cases = self.browse(cr, uid, ids2, context=context)
328 return self._action(cr, uid, cases, False, context=context)
330 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
331 """ Override of the base.stage method
332 Parameter of the stage search taken from the lead:
333 - type: stage type must be the same or 'both'
334 - section_id: if set, stages must belong to this section or
335 be a default stage; if not set, stages must be default
338 if isinstance(cases, (int, long)):
339 cases = self.browse(cr, uid, cases, context=context)
340 # collect all section_ids
344 section_ids.append(section_id)
347 section_ids.append(lead.section_id.id)
348 if lead.type not in types:
349 types.append(lead.type)
350 # OR all section_ids and OR with case_default
353 search_domain += [('|')] * len(section_ids)
354 for section_id in section_ids:
355 search_domain.append(('section_ids', '=', section_id))
356 search_domain.append(('case_default', '=', True))
357 # AND with cases types
358 search_domain.append(('type', 'in', types))
359 # AND with the domain in parameter
360 search_domain += list(domain)
361 # perform search, return the first found
362 stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
367 def case_cancel(self, cr, uid, ids, context=None):
368 """ Overrides case_cancel from base_stage to set probability """
369 res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
370 self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
373 def case_reset(self, cr, uid, ids, context=None):
374 """ Overrides case_reset from base_stage to set probability """
375 res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
376 self.write(cr, uid, ids, {'probability': 0.0}, context=context)
379 def case_mark_lost(self, cr, uid, ids, context=None):
380 """ Mark the case as lost: state=cancel and probability=0 """
381 for lead in self.browse(cr, uid, ids):
382 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
384 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
385 self.case_mark_lost_send_note(cr, uid, ids, context=context)
388 def case_mark_won(self, cr, uid, ids, context=None):
389 """ Mark the case as lost: state=done and probability=100 """
390 for lead in self.browse(cr, uid, ids):
391 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
393 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
394 self.case_mark_won_send_note(cr, uid, ids, context=context)
397 def set_priority(self, cr, uid, ids, priority):
398 """ Set lead priority
400 return self.write(cr, uid, ids, {'priority' : priority})
402 def set_high_priority(self, cr, uid, ids, context=None):
403 """ Set lead priority to high
405 return self.set_priority(cr, uid, ids, '1')
407 def set_normal_priority(self, cr, uid, ids, context=None):
408 """ Set lead priority to normal
410 return self.set_priority(cr, uid, ids, '3')
412 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
413 # prepare opportunity data into dictionary for merging
414 opportunities = self.browse(cr, uid, ids, context=context)
415 def _get_first_not_null(attr):
416 if hasattr(oldest, attr):
417 return getattr(oldest, attr)
418 for opportunity in opportunities:
419 if hasattr(opportunity, attr):
420 return getattr(opportunity, attr)
423 def _get_first_not_null_id(attr):
424 res = _get_first_not_null(attr)
425 return res and res.id or False
427 def _concat_all(attr):
428 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
431 for field_name in fields:
432 field_info = self._all_columns.get(field_name)
433 if field_info is None:
435 field = field_info.column
436 if field._type in ('many2many', 'one2many'):
438 elif field._type == 'many2one':
439 data[field_name] = _get_first_not_null_id(field_name) # !!
440 elif field._type == 'text':
441 data[field_name] = _concat_all(field_name) #not lost
443 data[field_name] = _get_first_not_null(field_name) #not lost
446 def _merge_find_oldest(self, cr, uid, ids, context=None):
449 #TOCHECK: where pass 'convert' in context ?
450 if context.get('convert'):
451 ids = list(set(ids) - set(context.get('lead_ids', False)) )
453 #search opportunities order by create date
454 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
455 oldest_id = opportunity_ids[0]
456 return self.browse(cr, uid, oldest_id, context=context)
458 def _mail_body(self, cr, uid, lead, fields, title=False, context=None):
461 body.append("%s\n" % (title))
462 for field_name in fields:
463 field_info = self._all_columns.get(field_name)
464 if field_info is None:
466 field = field_info.column
469 if field._type == 'selection':
470 if hasattr(field.selection, '__call__'):
471 key = field.selection(self, cr, uid, context=context)
473 key = field.selection
474 value = dict(key).get(lead[field_name], lead[field_name])
475 elif field._type == 'many2one':
477 value = lead[field_name].name_get()[0][1]
479 value = lead[field_name]
481 body.append("%s: %s" % (field.string, value or ''))
482 return "\n".join(body + ['---'])
484 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
485 #TOFIX: mail template should be used instead of fix body, subject text
487 merge_message = _('Merged opportunities')
488 subject = [merge_message]
489 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_ids', 'channel_id', 'company_id', 'contact_name',
490 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
491 'country_id', 'city', 'street', 'street2', 'zip']
492 for opportunity in opportunities:
493 subject.append(opportunity.name)
494 title = "%s : %s" % (merge_message, opportunity.name)
495 details.append(self._mail_body(cr, uid, opportunity, fields, title=title, context=context))
497 # Chatter message's subject
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 messages and attachements into the first opportunity
568 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
569 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
571 # Merge notifications about loss of information
572 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
573 # Write merged data into first opportunity
574 self.write(cr, uid, [first_opportunity.id], data, context=context)
575 # Delete tail opportunities
576 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
578 # Open first opportunity
579 self.case_open(cr, uid, [first_opportunity.id])
580 return first_opportunity.id
582 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
583 crm_stage = self.pool.get('crm.case.stage')
586 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
592 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
594 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
595 stage_id = stage_ids and stage_ids[0] or False
598 'planned_revenue': lead.planned_revenue,
599 'probability': lead.probability,
601 'partner_id': customer and customer.id or False,
602 'user_id': (lead.user_id and lead.user_id.id),
603 'type': 'opportunity',
604 'stage_id': stage_id or False,
605 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
606 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
607 'email_from': customer and customer.email or lead.email_from,
608 'phone': customer and customer.phone or lead.phone,
611 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
612 partner = self.pool.get('res.partner')
615 customer = partner.browse(cr, uid, partner_id, context=context)
616 for lead in self.browse(cr, uid, ids, context=context):
617 if lead.state in ('done', 'cancel'):
619 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
620 self.write(cr, uid, [lead.id], vals, context=context)
621 self.convert_opportunity_send_note(cr, uid, lead, context=context)
623 if user_ids or section_id:
624 self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
628 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
629 partner = self.pool.get('res.partner')
630 vals = { 'name': name,
631 'user_id': lead.user_id.id,
632 'comment': lead.description,
633 'section_id': lead.section_id.id or False,
634 'parent_id': parent_id,
636 'mobile': lead.mobile,
637 'email': lead.email_from and tools.email_split(lead.email_from)[0],
639 'title': lead.title and lead.title.id or False,
640 'function': lead.function,
641 'street': lead.street,
642 'street2': lead.street2,
645 'country_id': lead.country_id and lead.country_id.id or False,
646 'state_id': lead.state_id and lead.state_id.id or False,
647 'is_company': is_company,
650 partner = partner.create(cr, uid,vals, context)
653 def _create_lead_partner(self, cr, uid, lead, context=None):
655 if lead.partner_name and lead.contact_name:
656 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
657 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
658 elif lead.partner_name and not lead.contact_name:
659 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
660 elif not lead.partner_name and lead.contact_name:
661 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
663 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
666 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
668 res_partner = self.pool.get('res.partner')
670 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
671 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
672 res = lead.write({'partner_id' : partner_id, }, context=context)
673 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
676 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
678 This function convert partner based on action.
679 if action is 'create', create new partner with contact and assign lead to new partner_id.
680 otherwise assign lead to specified partner_id
685 force_partner_id = partner_id
686 for lead in self.browse(cr, uid, ids, context=context):
687 if action == 'create':
689 partner_id = self._create_lead_partner(cr, uid, lead, context)
690 partner_id = force_partner_id or self._create_lead_partner(cr, uid, lead, context=context)
691 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
692 partner_ids[lead.id] = partner_id
695 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
700 value['section_id'] = team_id
701 if index < len(user_ids):
702 value['user_id'] = user_ids[index]
705 self.write(cr, uid, [lead_id], value, context=context)
708 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):
710 action :('schedule','Schedule a call'), ('log','Log a call')
712 phonecall = self.pool.get('crm.phonecall')
713 model_data = self.pool.get('ir.model.data')
716 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
718 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
719 for lead in self.browse(cr, uid, ids, context=context):
721 section_id = lead.section_id and lead.section_id.id or False
723 user_id = lead.user_id and lead.user_id.id or False
725 'name' : call_summary,
726 'opportunity_id' : lead.id,
727 'user_id' : user_id or False,
728 'categ_id' : categ_id or False,
729 'description' : desc or '',
730 'date' : schedule_time,
731 'section_id' : section_id or False,
732 'partner_id': lead.partner_id and lead.partner_id.id or False,
733 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
734 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
735 'priority': lead.priority,
737 new_id = phonecall.create(cr, uid, vals, context=context)
738 phonecall.case_open(cr, uid, [new_id], context=context)
740 phonecall.case_close(cr, uid, [new_id], context=context)
741 phonecall_dict[lead.id] = new_id
742 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
743 return phonecall_dict
746 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
747 models_data = self.pool.get('ir.model.data')
749 # Get Opportunity views
750 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
751 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
753 'name': _('Opportunity'),
755 'view_mode': 'tree, form',
756 'res_model': 'crm.lead',
757 'domain': [('type', '=', 'opportunity')],
758 'res_id': int(opportunity_id),
760 'views': [(form_view and form_view[1] or False, 'form'),
761 (tree_view and tree_view[1] or False, 'tree'),
762 (False, 'calendar'), (False, 'graph')],
763 'type': 'ir.actions.act_window',
766 def action_makeMeeting(self, cr, uid, ids, context=None):
767 """ This opens Meeting's calendar view to schedule meeting on current Opportunity
768 @return : Dictionary value for created Meeting view
770 opportunity = self.browse(cr, uid, ids[0], context)
771 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
773 'default_opportunity_id': opportunity.id,
774 'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
775 'default_partner_ids' : opportunity.partner_id and [opportunity.partner_id.id] or False,
776 'default_user_id': uid,
777 'default_section_id': opportunity.section_id and opportunity.section_id.id or False,
778 'default_email_from': opportunity.email_from,
779 'default_state': 'open',
780 'default_name': opportunity.name,
784 def unlink(self, cr, uid, ids, context=None):
785 for lead in self.browse(cr, uid, ids, context):
786 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
787 raise osv.except_osv(_('Error!'),
788 _("You cannot delete lead '%s' because it is not in 'Draft' state. " \
789 "You can still cancel it, instead of deleting it.") % lead.name)
790 return super(crm_lead, self).unlink(cr, uid, ids, context)
792 def write(self, cr, uid, ids, vals, context=None):
793 if vals.get('stage_id') and not vals.get('probability'):
794 # change probability of lead(s) if required by stage
795 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
797 vals['probability'] = stage.probability
798 if vals.get('section_id'):
799 section_id = self.pool.get('crm.case.section').browse(cr, uid, vals.get('section_id'), context=context)
801 vals.setdefault('message_follower_ids', [])
802 vals['message_follower_ids'] += [(4, follower.id) for follower in section_id.message_follower_ids]
803 return super(crm_lead,self).write(cr, uid, ids, vals, context)
805 # ----------------------------------------
807 # ----------------------------------------
809 def message_new(self, cr, uid, msg, custom_values=None, context=None):
810 """ Overrides mail_thread message_new that is called by the mailgateway
811 through message_process.
812 This override updates the document according to the email.
814 if custom_values is None: custom_values = {}
815 custom_values.update({
816 'name': msg.get('subject') or _("No Subject"),
817 'description': msg.get('body'),
818 'email_from': msg.get('from'),
819 'email_cc': msg.get('cc'),
822 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
823 custom_values['priority'] = msg.get('priority')
824 return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
826 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
827 """ Overrides mail_thread message_update that is called by the mailgateway
828 through message_process.
829 This method updates the document according to the email.
831 if isinstance(ids, (str, int, long)):
833 if update_vals is None: update_vals = {}
835 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
836 update_vals['priority'] = msg.get('priority')
838 'cost':'planned_cost',
839 'revenue': 'planned_revenue',
840 'probability':'probability',
842 for line in msg.get('body', '').split('\n'):
844 res = tools.misc.command_re.match(line)
845 if res and maps.get(res.group(1).lower()):
846 key = maps.get(res.group(1).lower())
847 update_vals[key] = res.group(2).lower()
849 return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
851 # ----------------------------------------
852 # OpenChatter methods and notifications
853 # ----------------------------------------
855 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
856 """ Override of the (void) default notification method. """
857 stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
858 return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
860 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
861 if isinstance(lead, (int, long)):
862 lead = self.browse(cr, uid, [lead], context=context)[0]
863 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
865 def create_send_note(self, cr, uid, ids, context=None):
867 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
868 self.message_post(cr, uid, [id], body=message, context=context)
871 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
872 message = _("Opportunity has been <b>lost</b>.")
873 return self.message_post(cr, uid, ids, body=message, context=context)
875 def case_mark_won_send_note(self, cr, uid, ids, context=None):
876 message = _("Opportunity has been <b>won</b>.")
877 return self.message_post(cr, uid, ids, body=message, context=context)
879 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
880 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
881 if action == 'log': prefix = 'Logged'
882 else: prefix = 'Scheduled'
883 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
884 return self.message_post(cr, uid, ids, body=message, context=context)
886 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
887 for lead in self.browse(cr, uid, ids, context=context):
888 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))
889 lead.message_post(body=message)
892 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
893 message = _("Lead has been <b>converted to an opportunity</b>.")
894 lead.message_post(body=message)
899 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: