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 ##############################################################################
23 from base_status.base_stage import base_stage
25 from datetime import datetime
26 from mail.mail_message import to_email
27 from osv import fields, osv
30 from tools.translate import _
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, osv.osv):
41 _description = "Lead/Opportunity"
42 _order = "priority,date_action,id desc"
43 _inherit = ['ir.needaction_mixin', 'mail.thread']
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), ('fold', '=', False)]
93 search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)]
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])))
106 'stage_id': _read_group_stage_ids
109 def _compute_day(self, cr, uid, ids, fields, args, context=None):
111 @param cr: the current row, from the database cursor,
112 @param uid: the current user’s ID for security checks,
113 @param ids: List of Openday’s IDs
114 @return: difference between current date and log date
115 @param context: A standard dictionary for contextual values
117 cal_obj = self.pool.get('resource.calendar')
118 res_obj = self.pool.get('resource.resource')
121 for lead in self.browse(cr, uid, ids, context=context):
126 if field == 'day_open':
128 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
129 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
130 ans = date_open - date_create
131 date_until = lead.date_open
132 elif field == 'day_close':
134 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
135 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
136 date_until = lead.date_closed
137 ans = date_close - date_create
141 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
142 if len(resource_ids):
143 resource_id = resource_ids[0]
145 duration = float(ans.days)
146 if lead.section_id and lead.section_id.resource_calendar_id:
147 duration = float(ans.days) * 24
148 new_dates = cal_obj.interval_get(cr,
150 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
151 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
156 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
157 for in_time, out_time in new_dates:
158 if in_time.date not in no_days:
159 no_days.append(in_time.date)
160 if out_time > date_until:
162 duration = len(no_days)
163 res[lead.id][field] = abs(int(duration))
166 def _history_search(self, cr, uid, obj, name, args, context=None):
168 msg_obj = self.pool.get('mail.message')
169 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
170 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
173 return [('id', 'in', lead_ids)]
175 return [('id', '=', '0')]
177 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
179 for obj in self.browse(cr, uid, ids, context=context):
181 for msg in obj.message_ids:
183 res[obj.id] = msg.subject
188 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
189 select=True, help="Optional linked partner, usually after conversion of the lead"),
191 'id': fields.integer('ID', readonly=True),
192 'name': fields.char('Subject', size=64, required=True, select=1),
193 'active': fields.boolean('Active', required=False),
194 'date_action_last': fields.datetime('Last Action', readonly=1),
195 'date_action_next': fields.datetime('Next Action', readonly=1),
196 'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
197 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
198 select=True, help='When sending mails, the default email address is taken from the sales team.'),
199 'create_date': fields.datetime('Creation Date' , readonly=True),
200 '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"),
201 'description': fields.text('Notes'),
202 'write_date': fields.datetime('Update Date' , readonly=True),
203 'categ_id': fields.many2one('crm.case.categ', 'Category', \
204 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
205 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
206 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
207 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
208 'contact_name': fields.char('Contact Name', size=64),
209 '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),
210 'opt_in': fields.boolean('Opt-In', oldname='optin', help="If opt-in is checked, this contact has accepted to receive emails."),
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="['&', '|', ('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\'.'),
231 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
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', 'user_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', domain="[('country_id','=',country_id)]"),
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),
267 'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
268 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
269 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
270 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
271 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
272 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
276 def get_needaction_user_ids(self, cr, uid, ids, context=None):
277 result = dict.fromkeys(ids, [])
278 for obj in self.browse(cr, uid, ids, context=context):
279 # salesman must perform an action when in draft mode
280 if obj.state == 'draft' and obj.user_id:
281 result[obj.id] = [obj.user_id.id]
284 def create(self, cr, uid, vals, context=None):
285 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
286 self.create_send_note(cr, uid, [obj_id], context=context)
289 def on_change_opt_in(self, cr, uid, ids, opt_in):
290 return {'value':{'opt_in':opt_in,'opt_out':False}}
292 def on_change_opt_out(self, cr, uid, ids, opt_out):
293 return {'value':{'opt_out':opt_out,'opt_in':False}}
295 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
298 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
299 if not stage.on_change:
301 return {'value':{'probability': stage.probability}}
303 def _check(self, cr, uid, ids=False, context=None):
304 """ Override of the base.stage method.
305 Function called by the scheduler to process cases for date actions
306 Only works on not done and cancelled cases
308 cr.execute('select * from crm_case \
309 where (date_action_last<%s or date_action_last is null) \
310 and (date_action_next<=%s or date_action_next is null) \
311 and state not in (\'cancel\',\'done\')',
312 (time.strftime("%Y-%m-%d %H:%M:%S"),
313 time.strftime('%Y-%m-%d %H:%M:%S')))
315 ids2 = map(lambda x: x[0], cr.fetchall() or [])
316 cases = self.browse(cr, uid, ids2, context=context)
317 return self._action(cr, uid, cases, False, context=context)
319 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
320 """ Override of the base.stage method
321 Parameter of the stage search taken from the lead:
322 - type: stage type must be the same or 'both'
323 - section_id: if set, stages must belong to this section or
324 be a default stage; if not set, stages must be default
327 if isinstance(cases, (int, long)):
328 cases = self.browse(cr, uid, cases, context=context)
329 # collect all section_ids
333 section_ids.append(section_id)
336 section_ids.append(lead.section_id.id)
337 if lead.type not in types:
338 types.append(lead.type)
339 # OR all section_ids and OR with case_default
342 search_domain += [('|')] * len(section_ids)
343 for section_id in section_ids:
344 search_domain.append(('section_ids', '=', section_id))
345 search_domain.append(('case_default', '=', True))
346 # AND with cases types
347 search_domain.append(('type', 'in', types))
348 # AND with the domain in parameter
349 search_domain += list(domain)
350 # perform search, return the first found
351 stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
356 def case_cancel(self, cr, uid, ids, context=None):
357 """ Overrides case_cancel from base_stage to set probability """
358 res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
359 self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
362 def case_reset(self, cr, uid, ids, context=None):
363 """ Overrides case_reset from base_stage to set probability """
364 res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
365 self.write(cr, uid, ids, {'probability': 0.0}, context=context)
368 def case_mark_lost(self, cr, uid, ids, context=None):
369 """ Mark the case as lost: state=cancel and probability=0 """
370 for lead in self.browse(cr, uid, ids):
371 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
373 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
374 self.case_mark_lost_send_note(cr, uid, ids, context=context)
377 def case_mark_won(self, cr, uid, ids, context=None):
378 """ Mark the case as lost: state=done and probability=100 """
379 for lead in self.browse(cr, uid, ids):
380 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
382 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
383 self.case_mark_won_send_note(cr, uid, ids, context=context)
386 def set_priority(self, cr, uid, ids, priority):
387 """ Set lead priority
389 return self.write(cr, uid, ids, {'priority' : priority})
391 def set_high_priority(self, cr, uid, ids, context=None):
392 """ Set lead priority to high
394 return self.set_priority(cr, uid, ids, '1')
396 def set_normal_priority(self, cr, uid, ids, context=None):
397 """ Set lead priority to normal
399 return self.set_priority(cr, uid, ids, '3')
401 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
402 # prepare opportunity data into dictionary for merging
403 opportunities = self.browse(cr, uid, ids, context=context)
404 def _get_first_not_null(attr):
405 if hasattr(oldest, attr):
406 return getattr(oldest, attr)
407 for opportunity in opportunities:
408 if hasattr(opportunity, attr):
409 return getattr(opportunity, attr)
412 def _get_first_not_null_id(attr):
413 res = _get_first_not_null(attr)
414 return res and res.id or False
416 def _concat_all(attr):
417 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
420 for field_name in fields:
421 field_info = self._all_columns.get(field_name)
422 if field_info is None:
424 field = field_info.column
425 if field._type in ('many2many', 'one2many'):
427 elif field._type == 'many2one':
428 data[field_name] = _get_first_not_null_id(field_name) # !!
429 elif field._type == 'text':
430 data[field_name] = _concat_all(field_name) #not lost
432 data[field_name] = _get_first_not_null(field_name) #not lost
435 def _merge_find_oldest(self, cr, uid, ids, context=None):
438 #TOCHECK: where pass 'convert' in context ?
439 if context.get('convert'):
440 ids = list(set(ids) - set(context.get('lead_ids', False)) )
442 #search opportunities order by create date
443 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
444 oldest_id = opportunity_ids[0]
445 return self.browse(cr, uid, oldest_id, context=context)
447 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
450 body.append("%s\n" % (title))
451 for field_name in fields:
452 field_info = self._all_columns.get(field_name)
453 if field_info is None:
455 field = field_info.column
458 if field._type == 'selection':
459 if hasattr(field.selection, '__call__'):
460 key = field.selection(self, cr, uid, context=context)
462 key = field.selection
463 value = dict(key).get(lead[field_name], lead[field_name])
464 elif field._type == 'many2one':
466 value = lead[field_name].name_get()[0][1]
468 value = lead[field_name]
470 body.append("%s: %s" % (field.string, value or ''))
471 return "\n".join(body + ['---'])
473 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
474 #TOFIX: mail template should be used instead of fix body, subject text
476 merge_message = _('Merged opportunities')
477 subject = [merge_message]
478 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
479 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
480 'country_id', 'city', 'street', 'street2', 'zip']
481 for opportunity in opportunities:
482 subject.append(opportunity.name)
483 title = "%s : %s" % (merge_message, opportunity.name)
484 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
486 subject = subject[0] + ", ".join(subject[1:])
487 details = "\n\n".join(details)
488 return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
490 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
491 message = self.pool.get('mail.message')
492 for opportunity in opportunities:
493 for history in opportunity.message_ids:
494 message.write(cr, uid, history.id, {
495 'res_id': opportunity_id,
496 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
501 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
502 attachment = self.pool.get('ir.attachment')
504 # return attachments of opportunity
505 def _get_attachments(opportunity_id):
506 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
507 return attachment.browse(cr, uid, attachment_ids, context=context)
510 first_attachments = _get_attachments(opportunity_id)
511 for opportunity in opportunities:
512 attachments = _get_attachments(opportunity.id)
513 for first in first_attachments:
514 for attachment in attachments:
515 if attachment.name == first.name:
517 name = "%s (%s)" % (attachment.name, count,),
518 res_id = opportunity_id,
520 attachment.write(values)
525 def merge_opportunity(self, cr, uid, ids, context=None):
527 To merge opportunities
528 :param ids: list of opportunities ids to merge
530 if context is None: context = {}
532 #TOCHECK: where pass lead_ids in context?
533 lead_ids = context and context.get('lead_ids', []) or []
536 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
538 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
539 opportunities = self.browse(cr, uid, ids, context=context)
540 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
541 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
542 if ctx_opportunities :
543 first_opportunity = ctx_opportunities[0]
544 tail_opportunities = opportunities_list
546 first_opportunity = opportunities_list[0]
547 tail_opportunities = opportunities_list[1:]
549 fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
550 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
551 'date_action_next', 'email_from', 'email_cc', 'partner_name']
553 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
555 # merge data into first opportunity
556 self.write(cr, uid, [first_opportunity.id], data, context=context)
558 #copy message and attachements into the first opportunity
559 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
560 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
562 #Notification about loss of information
563 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
564 #delete tail opportunities
565 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
567 #open first opportunity
568 self.case_open(cr, uid, [first_opportunity.id])
569 return first_opportunity.id
571 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
572 crm_stage = self.pool.get('crm.case.stage')
575 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
577 section_id = lead.section_id and lead.section_id.id or False
579 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
581 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
582 stage_id = stage_ids and stage_ids[0] or False
584 'planned_revenue': lead.planned_revenue,
585 'probability': lead.probability,
587 'partner_id': customer and customer.id or False,
588 'user_id': (lead.user_id and lead.user_id.id),
589 'type': 'opportunity',
590 'stage_id': stage_id or False,
591 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
592 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
595 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
596 partner = self.pool.get('res.partner')
597 mail_message = self.pool.get('mail.message')
600 customer = partner.browse(cr, uid, partner_id, context=context)
601 for lead in self.browse(cr, uid, ids, context=context):
602 if lead.state in ('done', 'cancel'):
604 if user_ids or section_id:
605 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
607 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
608 self.write(cr, uid, [lead.id], vals, context=context)
610 self.convert_opportunity_send_note(cr, uid, lead, context=context)
611 #TOCHECK: why need to change partner details in all messages of lead ?
613 msg_ids = [ x.id for x in lead.message_ids]
614 mail_message.write(cr, uid, msg_ids, {
615 'partner_id': lead.partner_id.id
619 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
620 partner = self.pool.get('res.partner')
621 vals = { 'name': name,
622 'user_id': lead.user_id.id,
623 'comment': lead.description,
624 'section_id': lead.section_id.id or False,
625 'parent_id': parent_id,
627 'mobile': lead.mobile,
628 'email': lead.email_from and to_email(lead.email_from)[0],
630 'title': lead.title and lead.title.id or False,
631 'function': lead.function,
632 'street': lead.street,
633 'street2': lead.street2,
636 'country_id': lead.country_id and lead.country_id.id or False,
637 'state_id': lead.state_id and lead.state_id.id or False,
638 'is_company': is_company,
641 partner = partner.create(cr, uid,vals, context)
644 def _create_lead_partner(self, cr, uid, lead, context=None):
646 if lead.partner_name and lead.contact_name:
647 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
648 self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
649 elif lead.partner_name and not lead.contact_name:
650 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
651 elif not lead.partner_name and lead.contact_name:
652 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
654 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
657 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
659 res_partner = self.pool.get('res.partner')
661 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
662 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
663 res = lead.write({'partner_id' : partner_id, }, context=context)
664 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
667 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
669 This function convert partner based on action.
670 if action is 'create', create new partner with contact and assign lead to new partner_id.
671 otherwise assign lead to specified partner_id
676 for lead in self.browse(cr, uid, ids, context=context):
677 if action == 'create':
679 partner_id = self._create_lead_partner(cr, uid, lead, context)
680 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
681 partner_ids[lead.id] = partner_id
684 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
686 Send mail to salesman with updated Lead details.
687 @ lead: browse record of 'crm.lead' object.
689 #TOFIX: mail template should be used here instead of fix subject, body text.
690 message = self.pool.get('mail.message')
691 email_to = lead.user_id and lead.user_id.user_email
695 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
696 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
697 subject = "lead %s converted into opportunity" % lead.name
698 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
699 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
702 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
707 value['section_id'] = team_id
708 if index < len(user_ids):
709 value['user_id'] = user_ids[index]
712 self.write(cr, uid, [lead_id], value, context=context)
715 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):
717 action :('schedule','Schedule a call'), ('log','Log a call')
719 phonecall = self.pool.get('crm.phonecall')
720 model_data = self.pool.get('ir.model.data')
723 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
725 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
726 for lead in self.browse(cr, uid, ids, context=context):
728 section_id = lead.section_id and lead.section_id.id or False
730 user_id = lead.user_id and lead.user_id.id or False
732 'name' : call_summary,
733 'opportunity_id' : lead.id,
734 'user_id' : user_id or False,
735 'categ_id' : categ_id or False,
736 'description' : desc or '',
737 'date' : schedule_time,
738 'section_id' : section_id or False,
739 'partner_id': lead.partner_id and lead.partner_id.id or False,
740 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
741 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
742 'priority': lead.priority,
744 new_id = phonecall.create(cr, uid, vals, context=context)
745 phonecall.case_open(cr, uid, [new_id], context=context)
747 phonecall.case_close(cr, uid, [new_id], context=context)
748 phonecall_dict[lead.id] = new_id
749 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
750 return phonecall_dict
753 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
754 models_data = self.pool.get('ir.model.data')
756 # Get Opportunity views
757 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
758 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
760 'name': _('Opportunity'),
762 'view_mode': 'tree, form',
763 'res_model': 'crm.lead',
764 'domain': [('type', '=', 'opportunity')],
765 'res_id': int(opportunity_id),
767 'views': [(form_view and form_view[1] or False, 'form'),
768 (tree_view and tree_view[1] or False, 'tree'),
769 (False, 'calendar'), (False, 'graph')],
770 'type': 'ir.actions.act_window',
774 def message_new(self, cr, uid, msg, custom_values=None, context=None):
775 """Automatically calls when new email message arrives"""
776 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
777 subject = msg.get('subject') or _("No Subject")
778 body = msg.get('body_text')
780 msg_from = msg.get('from')
781 priority = msg.get('priority')
784 'email_from': msg_from,
785 'email_cc': msg.get('cc'),
790 vals['priority'] = priority
791 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
792 self.write(cr, uid, [res_id], vals, context)
795 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
796 if isinstance(ids, (str, int, long)):
800 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
802 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
803 vals['priority'] = msg.get('priority')
805 'cost':'planned_cost',
806 'revenue': 'planned_revenue',
807 'probability':'probability'
810 for line in msg['body_text'].split('\n'):
812 res = tools.misc.command_re.match(line)
813 if res and maps.get(res.group(1).lower()):
814 key = maps.get(res.group(1).lower())
815 vls[key] = res.group(2).lower()
818 # Unfortunately the API is based on lists
819 # but we want to update the state based on the
820 # previous state, so we have to loop:
821 for case in self.browse(cr, uid, ids, context=context):
823 if case.state in CRM_LEAD_PENDING_STATES:
825 values.update(state=crm.AVAILABLE_STATES[1][0])
826 if not case.date_open:
827 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
828 res = self.write(cr, uid, [case.id], values, context=context)
831 def action_makeMeeting(self, cr, uid, ids, context=None):
833 This opens Meeting's calendar view to schedule meeting on current Opportunity
834 @return : Dictionary value for created Meeting view
839 data_obj = self.pool.get('ir.model.data')
840 for opp in self.browse(cr, uid, ids, context=context):
842 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
843 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
844 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
845 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
847 'default_opportunity_id': opp.id,
848 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
849 'default_user_id': uid,
850 'default_section_id': opp.section_id and opp.section_id.id or False,
851 'default_email_from': opp.email_from,
852 'default_state': 'open',
853 'default_name': opp.name
856 'name': _('Meetings'),
859 'view_mode': 'calendar,form,tree',
860 'res_model': 'crm.meeting',
862 'views': [(calander_view and calander_view[1] or False, 'calendar'), (form_view and form_view[1] or False, 'form'), (tree_view and tree_view[1] or False, 'tree')],
863 'type': 'ir.actions.act_window',
864 'search_view_id': search_view and search_view[1] or False,
870 def unlink(self, cr, uid, ids, context=None):
871 for lead in self.browse(cr, uid, ids, context):
872 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
873 raise osv.except_osv(_('Error'),
874 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
875 "You should better cancel it, instead of deleting it.") % lead.name)
876 return super(crm_lead, self).unlink(cr, uid, ids, context)
878 def write(self, cr, uid, ids, vals, context=None):
879 if vals.get('stage_id') and not vals.get('probability'):
880 # change probability of lead(s) if required by stage
881 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
883 vals['probability'] = stage.probability
884 return super(crm_lead,self).write(cr, uid, ids, vals, context)
886 # ----------------------------------------
887 # OpenChatter methods and notifications
888 # ----------------------------------------
890 def message_get_subscribers(self, cr, uid, ids, context=None):
891 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
892 # add salesman to the subscribers
893 for obj in self.browse(cr, uid, ids, context=context):
895 sub_ids.append(obj.user_id.id)
896 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
898 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
899 """ Override of the (void) default notification method. """
900 stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
901 return self.message_append_note(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
903 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
904 if isinstance(lead, (int, long)):
905 lead = self.browse(cr, uid, [lead], context=context)[0]
906 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
908 def create_send_note(self, cr, uid, ids, context=None):
910 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
911 self.message_append_note(cr, uid, [id], body=message, context=context)
914 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
915 message = _("Opportunity has been <b>lost</b>.")
916 return self.message_append_note(cr, uid, ids, body=message, context=context)
918 def case_mark_won_send_note(self, cr, uid, ids, context=None):
919 message = _("Opportunity has been <b>won</b>.")
920 return self.message_append_note(cr, uid, ids, body=message, context=context)
922 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
923 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
924 if action == 'log': prefix = 'Logged'
925 else: prefix = 'Scheduled'
926 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
927 return self.message_append_note(cr, uid, ids, body=message, context=context)
929 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
930 for lead in self.browse(cr, uid, ids, context=context):
931 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))
932 lead.message_append_note(body=message)
935 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
936 message = _("Lead has been <b>converted to an opportunity</b>.")
937 lead.message_append_note(body=message)
942 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: