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']
44 _mail_compose_message = True
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), ('fold', '=', False)]
94 search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)]
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])))
107 'stage_id': _read_group_stage_ids
110 def _compute_day(self, cr, uid, ids, fields, args, context=None):
112 @param cr: the current row, from the database cursor,
113 @param uid: the current user’s ID for security checks,
114 @param ids: List of Openday’s IDs
115 @return: difference between current date and log date
116 @param context: A standard dictionary for contextual values
118 cal_obj = self.pool.get('resource.calendar')
119 res_obj = self.pool.get('resource.resource')
122 for lead in self.browse(cr, uid, ids, context=context):
127 if field == 'day_open':
129 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
130 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
131 ans = date_open - date_create
132 date_until = lead.date_open
133 elif field == 'day_close':
135 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
136 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
137 date_until = lead.date_closed
138 ans = date_close - date_create
142 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
143 if len(resource_ids):
144 resource_id = resource_ids[0]
146 duration = float(ans.days)
147 if lead.section_id and lead.section_id.resource_calendar_id:
148 duration = float(ans.days) * 24
149 new_dates = cal_obj.interval_get(cr,
151 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
152 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
157 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
158 for in_time, out_time in new_dates:
159 if in_time.date not in no_days:
160 no_days.append(in_time.date)
161 if out_time > date_until:
163 duration = len(no_days)
164 res[lead.id][field] = abs(int(duration))
167 def _history_search(self, cr, uid, obj, name, args, context=None):
169 msg_obj = self.pool.get('mail.message')
170 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
171 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
174 return [('id', 'in', lead_ids)]
176 return [('id', '=', '0')]
178 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
180 for obj in self.browse(cr, uid, ids, context=context):
182 for msg in obj.message_ids:
184 res[obj.id] = msg.subject
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="['&', '|', ('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', '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),
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 self.create_send_note(cr, uid, [obj_id], context=context)
284 def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
287 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
288 if not stage.on_change:
290 return {'value':{'probability': stage.probability}}
292 def on_change_partner(self, cr, uid, ids, partner_id, context=None):
296 partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
298 'partner_name' : partner.name,
299 'street' : partner.street,
300 'street2' : partner.street2,
301 'city' : partner.city,
302 'state_id' : partner.state_id and partner.state_id.id or False,
303 'country_id' : partner.country_id and partner.country_id.id or False,
305 return {'value' : values}
308 def _check(self, cr, uid, ids=False, context=None):
309 """ Override of the base.stage method.
310 Function called by the scheduler to process cases for date actions
311 Only works on not done and cancelled cases
313 cr.execute('select * from crm_case \
314 where (date_action_last<%s or date_action_last is null) \
315 and (date_action_next<=%s or date_action_next is null) \
316 and state not in (\'cancel\',\'done\')',
317 (time.strftime("%Y-%m-%d %H:%M:%S"),
318 time.strftime('%Y-%m-%d %H:%M:%S')))
320 ids2 = map(lambda x: x[0], cr.fetchall() or [])
321 cases = self.browse(cr, uid, ids2, context=context)
322 return self._action(cr, uid, cases, False, context=context)
324 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
325 """ Override of the base.stage method
326 Parameter of the stage search taken from the lead:
327 - type: stage type must be the same or 'both'
328 - section_id: if set, stages must belong to this section or
329 be a default stage; if not set, stages must be default
332 if isinstance(cases, (int, long)):
333 cases = self.browse(cr, uid, cases, context=context)
334 # collect all section_ids
338 section_ids.append(section_id)
341 section_ids.append(lead.section_id.id)
342 if lead.type not in types:
343 types.append(lead.type)
344 # OR all section_ids and OR with case_default
347 search_domain += [('|')] * len(section_ids)
348 for section_id in section_ids:
349 search_domain.append(('section_ids', '=', section_id))
350 search_domain.append(('case_default', '=', True))
351 # AND with cases types
352 search_domain.append(('type', 'in', types))
353 # AND with the domain in parameter
354 search_domain += list(domain)
355 # perform search, return the first found
356 stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
361 def case_cancel(self, cr, uid, ids, context=None):
362 """ Overrides case_cancel from base_stage to set probability """
363 res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
364 self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
367 def case_reset(self, cr, uid, ids, context=None):
368 """ Overrides case_reset from base_stage to set probability """
369 res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
370 self.write(cr, uid, ids, {'probability': 0.0}, context=context)
373 def case_mark_lost(self, cr, uid, ids, context=None):
374 """ Mark the case as lost: state=cancel and probability=0 """
375 for lead in self.browse(cr, uid, ids):
376 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
378 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
379 self.case_mark_lost_send_note(cr, uid, ids, context=context)
382 def case_mark_won(self, cr, uid, ids, context=None):
383 """ Mark the case as lost: state=done and probability=100 """
384 for lead in self.browse(cr, uid, ids):
385 stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
387 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
388 self.case_mark_won_send_note(cr, uid, ids, context=context)
391 def set_priority(self, cr, uid, ids, priority):
392 """ Set lead priority
394 return self.write(cr, uid, ids, {'priority' : priority})
396 def set_high_priority(self, cr, uid, ids, context=None):
397 """ Set lead priority to high
399 return self.set_priority(cr, uid, ids, '1')
401 def set_normal_priority(self, cr, uid, ids, context=None):
402 """ Set lead priority to normal
404 return self.set_priority(cr, uid, ids, '3')
406 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
407 # prepare opportunity data into dictionary for merging
408 opportunities = self.browse(cr, uid, ids, context=context)
409 def _get_first_not_null(attr):
410 if hasattr(oldest, attr):
411 return getattr(oldest, attr)
412 for opportunity in opportunities:
413 if hasattr(opportunity, attr):
414 return getattr(opportunity, attr)
417 def _get_first_not_null_id(attr):
418 res = _get_first_not_null(attr)
419 return res and res.id or False
421 def _concat_all(attr):
422 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
425 for field_name in fields:
426 field_info = self._all_columns.get(field_name)
427 if field_info is None:
429 field = field_info.column
430 if field._type in ('many2many', 'one2many'):
432 elif field._type == 'many2one':
433 data[field_name] = _get_first_not_null_id(field_name) # !!
434 elif field._type == 'text':
435 data[field_name] = _concat_all(field_name) #not lost
437 data[field_name] = _get_first_not_null(field_name) #not lost
440 def _merge_find_oldest(self, cr, uid, ids, context=None):
443 #TOCHECK: where pass 'convert' in context ?
444 if context.get('convert'):
445 ids = list(set(ids) - set(context.get('lead_ids', False)) )
447 #search opportunities order by create date
448 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
449 oldest_id = opportunity_ids[0]
450 return self.browse(cr, uid, oldest_id, context=context)
452 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
455 body.append("%s\n" % (title))
456 for field_name in fields:
457 field_info = self._all_columns.get(field_name)
458 if field_info is None:
460 field = field_info.column
463 if field._type == 'selection':
464 if hasattr(field.selection, '__call__'):
465 key = field.selection(self, cr, uid, context=context)
467 key = field.selection
468 value = dict(key).get(lead[field_name], lead[field_name])
469 elif field._type == 'many2one':
471 value = lead[field_name].name_get()[0][1]
473 value = lead[field_name]
475 body.append("%s: %s" % (field.string, value or ''))
476 return "\n".join(body + ['---'])
478 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
479 #TOFIX: mail template should be used instead of fix body, subject text
481 merge_message = _('Merged opportunities')
482 subject = [merge_message]
483 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_ids', 'channel_id', 'company_id', 'contact_name',
484 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
485 'country_id', 'city', 'street', 'street2', 'zip']
486 for opportunity in opportunities:
487 subject.append(opportunity.name)
488 title = "%s : %s" % (merge_message, opportunity.name)
489 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
491 subject = subject[0] + ", ".join(subject[1:])
492 details = "\n\n".join(details)
493 return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
495 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
496 message = self.pool.get('mail.message')
497 for opportunity in opportunities:
498 for history in opportunity.message_ids:
499 message.write(cr, uid, history.id, {
500 'res_id': opportunity_id,
501 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
506 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
507 attachment = self.pool.get('ir.attachment')
509 # return attachments of opportunity
510 def _get_attachments(opportunity_id):
511 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
512 return attachment.browse(cr, uid, attachment_ids, context=context)
515 first_attachments = _get_attachments(opportunity_id)
516 for opportunity in opportunities:
517 attachments = _get_attachments(opportunity.id)
518 for first in first_attachments:
519 for attachment in attachments:
520 if attachment.name == first.name:
522 name = "%s (%s)" % (attachment.name, count,),
523 res_id = opportunity_id,
525 attachment.write(values)
530 def merge_opportunity(self, cr, uid, ids, context=None):
532 To merge opportunities
533 :param ids: list of opportunities ids to merge
535 if context is None: context = {}
537 #TOCHECK: where pass lead_ids in context?
538 lead_ids = context and context.get('lead_ids', []) or []
541 raise osv.except_osv(_('Warning!'),_('Please select more than one opportunity from the list view.'))
543 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
544 opportunities = self.browse(cr, uid, ids, context=context)
545 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
546 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
547 if ctx_opportunities :
548 first_opportunity = ctx_opportunities[0]
549 tail_opportunities = opportunities_list
551 first_opportunity = opportunities_list[0]
552 tail_opportunities = opportunities_list[1:]
554 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',
555 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
556 'date_action_next', 'email_from', 'email_cc', 'partner_name']
558 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
560 # merge data into first opportunity
561 self.write(cr, uid, [first_opportunity.id], data, context=context)
563 #copy message and attachements into the first opportunity
564 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
565 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
567 #Notification about loss of information
568 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
569 #delete tail opportunities
570 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
572 #open first opportunity
573 self.case_open(cr, uid, [first_opportunity.id])
574 return first_opportunity.id
576 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
577 crm_stage = self.pool.get('crm.case.stage')
580 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
582 section_id = lead.section_id and lead.section_id.id or False
584 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
586 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
587 stage_id = stage_ids and stage_ids[0] or False
589 'planned_revenue': lead.planned_revenue,
590 'probability': lead.probability,
592 'partner_id': customer and customer.id or False,
593 'user_id': (lead.user_id and lead.user_id.id),
594 'type': 'opportunity',
595 'stage_id': stage_id or False,
596 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
597 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
600 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
601 partner = self.pool.get('res.partner')
602 mail_message = self.pool.get('mail.message')
605 customer = partner.browse(cr, uid, partner_id, context=context)
606 for lead in self.browse(cr, uid, ids, context=context):
607 if lead.state in ('done', 'cancel'):
609 if user_ids or section_id:
610 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
612 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
613 self.write(cr, uid, [lead.id], vals, context=context)
615 self.convert_opportunity_send_note(cr, uid, lead, context=context)
616 #TOCHECK: why need to change partner details in all messages of lead ?
618 msg_ids = [ x.id for x in lead.message_ids]
619 mail_message.write(cr, uid, msg_ids, {
620 'partner_id': lead.partner_id.id
624 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
625 partner = self.pool.get('res.partner')
626 vals = { 'name': name,
627 'user_id': lead.user_id.id,
628 'comment': lead.description,
629 'section_id': lead.section_id.id or False,
630 'parent_id': parent_id,
632 'mobile': lead.mobile,
633 'email': lead.email_from and to_email(lead.email_from)[0],
635 'title': lead.title and lead.title.id or False,
636 'function': lead.function,
637 'street': lead.street,
638 'street2': lead.street2,
641 'country_id': lead.country_id and lead.country_id.id or False,
642 'state_id': lead.state_id and lead.state_id.id or False,
643 'is_company': is_company,
646 partner = partner.create(cr, uid,vals, context)
649 def _create_lead_partner(self, cr, uid, lead, context=None):
651 if lead.partner_name and lead.contact_name:
652 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
653 self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
654 elif lead.partner_name and not lead.contact_name:
655 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
656 elif not lead.partner_name and lead.contact_name:
657 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
659 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
662 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
664 res_partner = self.pool.get('res.partner')
666 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
667 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
668 res = lead.write({'partner_id' : partner_id, }, context=context)
669 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
672 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
674 This function convert partner based on action.
675 if action is 'create', create new partner with contact and assign lead to new partner_id.
676 otherwise assign lead to specified partner_id
681 for lead in self.browse(cr, uid, ids, context=context):
682 if action == 'create':
684 partner_id = self._create_lead_partner(cr, uid, lead, context)
685 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
686 partner_ids[lead.id] = partner_id
689 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
691 Send mail to salesman with updated Lead details.
692 @ lead: browse record of 'crm.lead' object.
694 #TOFIX: mail template should be used here instead of fix subject, body text.
695 message = self.pool.get('mail.message')
696 email_to = lead.user_id and lead.user_id.email
700 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.email or email_to
701 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
702 subject = "lead %s converted into opportunity" % lead.name
703 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
704 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
707 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
712 value['section_id'] = team_id
713 if index < len(user_ids):
714 value['user_id'] = user_ids[index]
717 self.write(cr, uid, [lead_id], value, context=context)
720 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):
722 action :('schedule','Schedule a call'), ('log','Log a call')
724 phonecall = self.pool.get('crm.phonecall')
725 model_data = self.pool.get('ir.model.data')
728 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
730 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
731 for lead in self.browse(cr, uid, ids, context=context):
733 section_id = lead.section_id and lead.section_id.id or False
735 user_id = lead.user_id and lead.user_id.id or False
737 'name' : call_summary,
738 'opportunity_id' : lead.id,
739 'user_id' : user_id or False,
740 'categ_id' : categ_id or False,
741 'description' : desc or '',
742 'date' : schedule_time,
743 'section_id' : section_id or False,
744 'partner_id': lead.partner_id and lead.partner_id.id or False,
745 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
746 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
747 'priority': lead.priority,
749 new_id = phonecall.create(cr, uid, vals, context=context)
750 phonecall.case_open(cr, uid, [new_id], context=context)
752 phonecall.case_close(cr, uid, [new_id], context=context)
753 phonecall_dict[lead.id] = new_id
754 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
755 return phonecall_dict
758 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
759 models_data = self.pool.get('ir.model.data')
761 # Get Opportunity views
762 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
763 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
765 'name': _('Opportunity'),
767 'view_mode': 'tree, form',
768 'res_model': 'crm.lead',
769 'domain': [('type', '=', 'opportunity')],
770 'res_id': int(opportunity_id),
772 'views': [(form_view and form_view[1] or False, 'form'),
773 (tree_view and tree_view[1] or False, 'tree'),
774 (False, 'calendar'), (False, 'graph')],
775 'type': 'ir.actions.act_window',
778 def action_makeMeeting(self, cr, uid, ids, context=None):
779 """ This opens Meeting's calendar view to schedule meeting on current Opportunity
780 @return : Dictionary value for created Meeting view
782 opportunity = self.browse(cr, uid, ids[0], context)
783 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
785 'default_opportunity_id': opportunity.id,
786 'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
787 'default_partner_ids' : opportunity.partner_id and [opportunity.partner_id.id] or False,
788 'default_user_id': uid,
789 'default_section_id': opportunity.section_id and opportunity.section_id.id or False,
790 'default_email_from': opportunity.email_from,
791 'default_state': 'open',
792 'default_name': opportunity.name,
796 def unlink(self, cr, uid, ids, context=None):
797 for lead in self.browse(cr, uid, ids, context):
798 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
799 raise osv.except_osv(_('Error!'),
800 _("You cannot delete lead '%s' because it is not in 'Draft' state. " \
801 "You can still cancel it, instead of deleting it.") % lead.name)
802 return super(crm_lead, self).unlink(cr, uid, ids, context)
804 def write(self, cr, uid, ids, vals, context=None):
805 if vals.get('stage_id') and not vals.get('probability'):
806 # change probability of lead(s) if required by stage
807 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
809 vals['probability'] = stage.probability
810 return super(crm_lead,self).write(cr, uid, ids, vals, context)
812 # ----------------------------------------
814 # ----------------------------------------
816 def message_new(self, cr, uid, msg, custom_values=None, context=None):
817 """ Overrides mail_thread message_new that is called by the mailgateway
818 through message_process.
819 This override updates the document according to the email.
821 if custom_values is None: custom_values = {}
822 custom_values.update({
823 'name': msg.get('subject') or _("No Subject"),
824 'description': msg.get('body_text'),
825 'email_from': msg.get('from'),
826 'email_cc': msg.get('cc'),
829 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
830 custom_values['priority'] = msg.get('priority')
831 custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from', False), context=context))
832 return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
834 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
835 """ Overrides mail_thread message_update that is called by the mailgateway
836 through message_process.
837 This method updates the document according to the email.
839 if isinstance(ids, (str, int, long)):
841 if update_vals is None: update_vals = {}
843 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
844 vals['priority'] = msg.get('priority')
846 'cost':'planned_cost',
847 'revenue': 'planned_revenue',
848 'probability':'probability',
850 for line in msg.get('body_text', '').split('\n'):
852 res = tools.misc.command_re.match(line)
853 if res and maps.get(res.group(1).lower()):
854 key = maps.get(res.group(1).lower())
855 vals[key] = res.group(2).lower()
857 return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
859 # ----------------------------------------
860 # OpenChatter methods and notifications
861 # ----------------------------------------
863 def message_get_monitored_follower_fields(self, cr, uid, ids, context=None):
864 """ Add 'user_id' to the monitored fields """
865 res = super(crm_lead, self).message_get_monitored_follower_fields(cr, uid, ids, context=context)
866 return res + ['user_id']
868 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
869 """ Override of the (void) default notification method. """
870 stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
871 return self.message_append_note(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
873 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
874 if isinstance(lead, (int, long)):
875 lead = self.browse(cr, uid, [lead], context=context)[0]
876 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
878 def create_send_note(self, cr, uid, ids, context=None):
880 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
881 self.message_append_note(cr, uid, [id], body=message, context=context)
884 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
885 message = _("Opportunity has been <b>lost</b>.")
886 return self.message_append_note(cr, uid, ids, body=message, context=context)
888 def case_mark_won_send_note(self, cr, uid, ids, context=None):
889 message = _("Opportunity has been <b>won</b>.")
890 return self.message_append_note(cr, uid, ids, body=message, context=context)
892 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
893 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
894 if action == 'log': prefix = 'Logged'
895 else: prefix = 'Scheduled'
896 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
897 return self.message_append_note(cr, uid, ids, body=message, context=context)
899 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
900 for lead in self.browse(cr, uid, ids, context=context):
901 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))
902 lead.message_append_note(body=message)
905 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
906 message = _("Lead has been <b>converted to an opportunity</b>.")
907 lead.message_append_note(body=message)
912 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: