1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-today OpenERP SA (<http://www.openerp.com>)
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from base_status.base_stage import base_stage
24 from datetime import datetime
25 from osv import fields, osv
28 from tools.translate import _
29 from tools import html2plaintext
31 from base.res.res_partner import format_address
33 CRM_LEAD_PENDING_STATES = (
34 crm.AVAILABLE_STATES[2][0], # Cancelled
35 crm.AVAILABLE_STATES[3][0], # Done
36 crm.AVAILABLE_STATES[4][0], # Pending
39 class crm_lead(base_stage, format_address, osv.osv):
42 _description = "Lead/Opportunity"
43 _order = "priority,date_action,id desc"
44 _inherit = ['mail.thread','ir.needaction_mixin']
46 def _get_default_section_id(self, cr, uid, context=None):
47 """ Gives default section by checking if present in the context """
48 return (self._resolve_section_id_from_context(cr, uid, context=context) or False)
50 def _get_default_stage_id(self, cr, uid, context=None):
51 """ Gives default stage_id """
52 section_id = self._get_default_section_id(cr, uid, context=context)
53 return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft'), ('type', '=', 'both')], context=context)
55 def _resolve_section_id_from_context(self, cr, uid, context=None):
56 """ Returns ID of section based on the value of 'section_id'
57 context key, or None if it cannot be resolved to a single
62 if type(context.get('default_section_id')) in (int, long):
63 return context.get('default_section_id')
64 if isinstance(context.get('default_section_id'), basestring):
65 section_name = context['default_section_id']
66 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=section_name, context=context)
67 if len(section_ids) == 1:
68 return int(section_ids[0][0])
71 def _resolve_type_from_context(self, cr, uid, context=None):
72 """ Returns the type (lead or opportunity) from the type context
73 key. Returns None if it cannot be resolved.
77 return context.get('default_type')
79 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
80 access_rights_uid = access_rights_uid or uid
81 stage_obj = self.pool.get('crm.case.stage')
82 order = stage_obj._order
83 # lame hack to allow reverting search, should just work in the trivial case
84 if read_group_order == 'stage_id desc':
85 order = "%s desc" % order
86 # retrieve section_id from the context and write the domain
87 # - ('id', 'in', 'ids'): add columns that should be present
88 # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
89 # - OR ('section_ids', '=', section_id), ('fold', '=', False) if section_id: add section columns that are not folded
91 section_id = self._resolve_section_id_from_context(cr, uid, context=context)
93 search_domain += ['|', ('section_ids', '=', section_id)]
94 search_domain += ['|', ('id', 'in', ids), ('case_default', '=', True)]
95 # retrieve type from the context (if set: choose 'type' or 'both')
96 type = self._resolve_type_from_context(cr, uid, context=context)
98 search_domain += ['|', ('type', '=', type), ('type', '=', 'both')]
100 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
101 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
102 # restore order of the search
103 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
106 for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
107 fold[stage.id] = stage.fold or False
111 def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
112 res = super(crm_lead,self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
113 if view_type == 'form':
114 res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
118 'stage_id': _read_group_stage_ids
121 def _compute_day(self, cr, uid, ids, fields, args, context=None):
123 @param cr: the current row, from the database cursor,
124 @param uid: the current user’s ID for security checks,
125 @param ids: List of Openday’s IDs
126 @return: difference between current date and log date
127 @param context: A standard dictionary for contextual values
129 cal_obj = self.pool.get('resource.calendar')
130 res_obj = self.pool.get('resource.resource')
133 for lead in self.browse(cr, uid, ids, context=context):
138 if field == 'day_open':
140 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
141 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
142 ans = date_open - date_create
143 date_until = lead.date_open
144 elif field == 'day_close':
146 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
147 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
148 date_until = lead.date_closed
149 ans = date_close - date_create
153 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
154 if len(resource_ids):
155 resource_id = resource_ids[0]
157 duration = float(ans.days)
158 if lead.section_id and lead.section_id.resource_calendar_id:
159 duration = float(ans.days) * 24
160 new_dates = cal_obj.interval_get(cr,
162 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
163 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
168 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
169 for in_time, out_time in new_dates:
170 if in_time.date not in no_days:
171 no_days.append(in_time.date)
172 if out_time > date_until:
174 duration = len(no_days)
175 res[lead.id][field] = abs(int(duration))
178 def _history_search(self, cr, uid, obj, name, args, context=None):
180 msg_obj = self.pool.get('mail.message')
181 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
182 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
185 return [('id', 'in', lead_ids)]
187 return [('id', '=', '0')]
190 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
191 select=True, help="Optional linked partner, usually after conversion of the lead"),
193 'id': fields.integer('ID', readonly=True),
194 'name': fields.char('Subject', size=64, required=True, select=1),
195 'active': fields.boolean('Active', required=False),
196 'date_action_last': fields.datetime('Last Action', readonly=1),
197 'date_action_next': fields.datetime('Next Action', readonly=1),
198 'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
199 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
200 select=True, help='When sending mails, the default email address is taken from the sales team.'),
201 'create_date': fields.datetime('Creation Date' , readonly=True),
202 '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"),
203 'description': fields.text('Notes'),
204 'write_date': fields.datetime('Update Date' , readonly=True),
205 'categ_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Categories', \
206 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
207 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
208 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
209 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
210 'contact_name': fields.char('Contact Name', size=64),
211 '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),
212 '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."),
213 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
214 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
215 'date_closed': fields.datetime('Closed', readonly=True),
216 'stage_id': fields.many2one('crm.case.stage', 'Stage',
217 domain="['&', ('fold', '=', False), '&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
218 'user_id': fields.many2one('res.users', 'Salesperson'),
219 'referred': fields.char('Referred By', size=64),
220 'date_open': fields.datetime('Opened', readonly=True),
221 'day_open': fields.function(_compute_day, string='Days to Open', \
222 multi='day_open', type="float", store=True),
223 'day_close': fields.function(_compute_day, string='Days to Close', \
224 multi='day_close', type="float", store=True),
225 'state': fields.related('stage_id', 'state', type="selection", store=True,
226 selection=crm.AVAILABLE_STATES, string="Status", readonly=True,
227 help='The Status is set to \'Draft\', when a case is created.\
228 If the case is in progress the Status is set to \'Open\'.\
229 When the case is over, the Status is set to \'Done\'.\
230 If the case needs to be reviewed then the Status is \
231 set to \'Pending\'.'),
233 # Only used for type opportunity
234 'probability': fields.float('Success Rate (%)',group_operator="avg"),
235 'planned_revenue': fields.float('Expected Revenue'),
236 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
237 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
238 'phone': fields.char("Phone", size=64),
239 'date_deadline': fields.date('Expected Closing'),
240 'date_action': fields.date('Next Action Date', select=True),
241 'title_action': fields.char('Next Action', size=64),
242 'color': fields.integer('Color Index'),
243 'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
244 'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
245 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
246 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
247 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
249 # Fields for address, due to separation from crm and res.partner
250 'street': fields.char('Street', size=128),
251 'street2': fields.char('Street2', size=128),
252 'zip': fields.char('Zip', change_default=True, size=24),
253 'city': fields.char('City', size=128),
254 'state_id': fields.many2one("res.country.state", 'State'),
255 'country_id': fields.many2one('res.country', 'Country'),
256 'phone': fields.char('Phone', size=64),
257 'fax': fields.char('Fax', size=64),
258 'mobile': fields.char('Mobile', size=64),
259 'function': fields.char('Function', size=128),
260 'title': fields.many2one('res.partner.title', 'Title'),
261 'company_id': fields.many2one('res.company', 'Company', select=1),
262 'payment_mode': fields.many2one('crm.payment.mode', 'Payment Mode', \
263 domain="[('section_id','=',section_id)]"),
264 'planned_cost': fields.float('Planned Costs'),
270 'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
271 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
272 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
273 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
274 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
275 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
279 def create(self, cr, uid, vals, context=None):
280 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
281 section_id = self.browse(cr, uid, obj_id, context=context).section_id
283 followers = [follow.id for follow in section_id.message_follower_ids]
284 self.message_subscribe(cr, uid, [obj_id], followers, context=context)
285 self.create_send_note(cr, uid, [obj_id], context=context)
288 def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
291 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
292 if not stage.on_change:
294 return {'value':{'probability': stage.probability}}
296 def on_change_partner(self, cr, uid, ids, partner_id, context=None):
300 partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
302 'partner_name' : partner.name,
303 'street' : partner.street,
304 'street2' : partner.street2,
305 'city' : partner.city,
306 'state_id' : partner.state_id and partner.state_id.id or False,
307 'country_id' : partner.country_id and partner.country_id.id or False,
308 'email_from' : partner.email,
309 'phone' : partner.phone,
310 'mobile' : partner.mobile,
313 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 # Chatter message's subject
499 subject = subject[0] + ": " + ", ".join(subject[1:])
500 details = "\n\n".join(details)
501 return self.message_post(cr, uid, [opportunity_id], body=details, subject=subject, context=context)
503 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
504 message = self.pool.get('mail.message')
505 for opportunity in opportunities:
506 for history in opportunity.message_ids:
507 message.write(cr, uid, history.id, {
508 'res_id': opportunity_id,
509 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
514 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
515 attachment = self.pool.get('ir.attachment')
517 # return attachments of opportunity
518 def _get_attachments(opportunity_id):
519 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
520 return attachment.browse(cr, uid, attachment_ids, context=context)
523 first_attachments = _get_attachments(opportunity_id)
524 for opportunity in opportunities:
525 attachments = _get_attachments(opportunity.id)
526 for first in first_attachments:
527 for attachment in attachments:
528 if attachment.name == first.name:
530 name = "%s (%s)" % (attachment.name, count,),
531 res_id = opportunity_id,
533 attachment.write(values)
538 def merge_opportunity(self, cr, uid, ids, context=None):
540 To merge opportunities
541 :param ids: list of opportunities ids to merge
543 if context is None: context = {}
545 #TOCHECK: where pass lead_ids in context?
546 lead_ids = context and context.get('lead_ids', []) or []
549 raise osv.except_osv(_('Warning!'),_('Please select more than one opportunity from the list view.'))
551 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
552 opportunities = self.browse(cr, uid, ids, context=context)
553 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
554 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
555 if ctx_opportunities :
556 first_opportunity = ctx_opportunities[0]
557 tail_opportunities = opportunities_list + ctx_opportunities[1:]
559 first_opportunity = opportunities_list[0]
560 tail_opportunities = opportunities_list[1:]
562 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',
563 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
564 'date_action_next', 'email_from', 'email_cc', 'partner_name']
566 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
568 # Merge messages and attachements into the first opportunity
569 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
570 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
572 # Merge notifications about loss of information
573 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
574 # Write merged data into first opportunity
575 self.write(cr, uid, [first_opportunity.id], data, 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']
590 section_id = lead.section_id and lead.section_id.id or False
593 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
595 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
596 stage_id = stage_ids and stage_ids[0] or False
599 'planned_revenue': lead.planned_revenue,
600 'probability': lead.probability,
602 'partner_id': customer and customer.id or False,
603 'user_id': (lead.user_id and lead.user_id.id),
604 'type': 'opportunity',
605 'stage_id': stage_id or False,
606 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
607 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
608 'email_from': customer and customer.email or lead.email_from,
609 'phone': customer and customer.phone or lead.phone,
612 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
613 partner = self.pool.get('res.partner')
616 customer = partner.browse(cr, uid, partner_id, context=context)
617 for lead in self.browse(cr, uid, ids, context=context):
618 if lead.state in ('done', 'cancel'):
620 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
621 self.write(cr, uid, [lead.id], vals, context=context)
622 self.convert_opportunity_send_note(cr, uid, lead, context=context)
624 if user_ids or section_id:
625 self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
629 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
630 partner = self.pool.get('res.partner')
631 vals = { 'name': name,
632 'user_id': lead.user_id.id,
633 'comment': lead.description,
634 'section_id': lead.section_id.id or False,
635 'parent_id': parent_id,
637 'mobile': lead.mobile,
638 'email': lead.email_from and tools.email_split(lead.email_from)[0],
640 'title': lead.title and lead.title.id or False,
641 'function': lead.function,
642 'street': lead.street,
643 'street2': lead.street2,
646 'country_id': lead.country_id and lead.country_id.id or False,
647 'state_id': lead.state_id and lead.state_id.id or False,
648 'is_company': is_company,
651 partner = partner.create(cr, uid,vals, context)
654 def _create_lead_partner(self, cr, uid, lead, context=None):
656 if lead.partner_name and lead.contact_name:
657 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
658 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
659 elif lead.partner_name and not lead.contact_name:
660 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
661 elif not lead.partner_name and lead.contact_name:
662 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
664 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
667 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
669 res_partner = self.pool.get('res.partner')
671 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
672 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
673 res = lead.write({'partner_id' : partner_id, }, context=context)
674 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
677 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
679 This function convert partner based on action.
680 if action is 'create', create new partner with contact and assign lead to new partner_id.
681 otherwise assign lead to specified partner_id
686 force_partner_id = partner_id
687 for lead in self.browse(cr, uid, ids, context=context):
688 if action == 'create':
690 partner_id = self._create_lead_partner(cr, uid, lead, context)
691 partner_id = force_partner_id or self._create_lead_partner(cr, uid, lead, context=context)
692 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
693 partner_ids[lead.id] = partner_id
696 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
701 value['section_id'] = team_id
702 if index < len(user_ids):
703 value['user_id'] = user_ids[index]
706 self.write(cr, uid, [lead_id], value, context=context)
709 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):
711 action :('schedule','Schedule a call'), ('log','Log a call')
713 phonecall = self.pool.get('crm.phonecall')
714 model_data = self.pool.get('ir.model.data')
717 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
719 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
720 for lead in self.browse(cr, uid, ids, context=context):
722 section_id = lead.section_id and lead.section_id.id or False
724 user_id = lead.user_id and lead.user_id.id or False
726 'name' : call_summary,
727 'opportunity_id' : lead.id,
728 'user_id' : user_id or False,
729 'categ_id' : categ_id or False,
730 'description' : desc or '',
731 'date' : schedule_time,
732 'section_id' : section_id or False,
733 'partner_id': lead.partner_id and lead.partner_id.id or False,
734 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
735 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
736 'priority': lead.priority,
738 new_id = phonecall.create(cr, uid, vals, context=context)
739 phonecall.case_open(cr, uid, [new_id], context=context)
741 phonecall.case_close(cr, uid, [new_id], context=context)
742 phonecall_dict[lead.id] = new_id
743 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
744 return phonecall_dict
747 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
748 models_data = self.pool.get('ir.model.data')
750 # Get Opportunity views
751 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
752 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
754 'name': _('Opportunity'),
756 'view_mode': 'tree, form',
757 'res_model': 'crm.lead',
758 'domain': [('type', '=', 'opportunity')],
759 'res_id': int(opportunity_id),
761 'views': [(form_view and form_view[1] or False, 'form'),
762 (tree_view and tree_view[1] or False, 'tree'),
763 (False, 'calendar'), (False, 'graph')],
764 'type': 'ir.actions.act_window',
767 def action_makeMeeting(self, cr, uid, ids, context=None):
768 """ This opens Meeting's calendar view to schedule meeting on current Opportunity
769 @return : Dictionary value for created Meeting view
771 opportunity = self.browse(cr, uid, ids[0], context)
772 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
774 'default_opportunity_id': opportunity.id,
775 'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
776 'default_partner_ids' : opportunity.partner_id and [opportunity.partner_id.id] or False,
777 'default_user_id': uid,
778 'default_section_id': opportunity.section_id and opportunity.section_id.id or False,
779 'default_email_from': opportunity.email_from,
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 = {}
816 desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
817 custom_values.update({
818 'name': msg.get('subject') or _("No Subject"),
820 'email_from': msg.get('from'),
821 'email_cc': msg.get('cc'),
824 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
825 custom_values['priority'] = msg.get('priority')
826 return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
828 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
829 """ Overrides mail_thread message_update that is called by the mailgateway
830 through message_process.
831 This method updates the document according to the email.
833 if isinstance(ids, (str, int, long)):
835 if update_vals is None: update_vals = {}
837 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
838 update_vals['priority'] = msg.get('priority')
840 'cost':'planned_cost',
841 'revenue': 'planned_revenue',
842 'probability':'probability',
844 for line in msg.get('body', '').split('\n'):
846 res = tools.misc.command_re.match(line)
847 if res and maps.get(res.group(1).lower()):
848 key = maps.get(res.group(1).lower())
849 update_vals[key] = res.group(2).lower()
851 return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
853 # ----------------------------------------
854 # OpenChatter methods and notifications
855 # ----------------------------------------
857 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
858 """ Override of the (void) default notification method. """
859 stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
860 return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
862 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
863 if isinstance(lead, (int, long)):
864 lead = self.browse(cr, uid, [lead], context=context)[0]
865 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
867 def create_send_note(self, cr, uid, ids, context=None):
869 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
870 self.message_post(cr, uid, [id], body=message, context=context)
873 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
874 message = _("Opportunity has been <b>lost</b>.")
875 return self.message_post(cr, uid, ids, body=message, context=context)
877 def case_mark_won_send_note(self, cr, uid, ids, context=None):
878 message = _("Opportunity has been <b>won</b>.")
879 return self.message_post(cr, uid, ids, body=message, context=context)
881 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
882 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
883 if action == 'log': prefix = 'Logged'
884 else: prefix = 'Scheduled'
885 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
886 return self.message_post(cr, uid, ids, body=message, context=context)
888 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
889 for lead in self.browse(cr, uid, ids, context=context):
890 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))
891 lead.message_post(body=message)
894 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
895 message = _("Lead has been <b>converted to an opportunity</b>.")
896 lead.message_post(body=message)
899 def onchange_state(self, cr, uid, ids, state_id, context=None):
901 country_id=self.pool.get('res.country.state').browse(cr, uid, state_id, context).country_id.id
902 return {'value':{'country_id':country_id}}
905 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: