1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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 osv import fields, osv
23 from datetime import datetime
26 from tools.translate import _
27 from crm import crm_case
30 from mail.mail_message import to_email
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(crm_case, osv.osv):
41 _description = "Lead/Opportunity"
42 _order = "priority,date_action,id desc"
43 _inherit = ['ir.needaction_mixin', 'mail.thread','res.partner']
45 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
46 access_rights_uid = access_rights_uid or uid
47 stage_obj = self.pool.get('crm.case.stage')
48 order = stage_obj._order
49 if read_group_order == 'stage_id desc':
50 # lame hack to allow reverting search, should just work in the trivial case
51 order = "%s desc" % order
52 stage_ids = stage_obj._search(cr, uid, ['|', ('id','in',ids),('case_default','=',1)], order=order,
53 access_rights_uid=access_rights_uid, context=context)
54 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
55 # restore order of the search
56 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
60 'stage_id': _read_group_stage_ids
63 def _compute_day(self, cr, uid, ids, fields, args, context=None):
65 @param cr: the current row, from the database cursor,
66 @param uid: the current user’s ID for security checks,
67 @param ids: List of Openday’s IDs
68 @return: difference between current date and log date
69 @param context: A standard dictionary for contextual values
71 cal_obj = self.pool.get('resource.calendar')
72 res_obj = self.pool.get('resource.resource')
75 for lead in self.browse(cr, uid, ids, context=context):
80 if field == 'day_open':
82 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
83 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
84 ans = date_open - date_create
85 date_until = lead.date_open
86 elif field == 'day_close':
88 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
89 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
90 date_until = lead.date_closed
91 ans = date_close - date_create
95 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
97 resource_id = resource_ids[0]
99 duration = float(ans.days)
100 if lead.section_id and lead.section_id.resource_calendar_id:
101 duration = float(ans.days) * 24
102 new_dates = cal_obj.interval_get(cr,
104 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
105 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
110 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
111 for in_time, out_time in new_dates:
112 if in_time.date not in no_days:
113 no_days.append(in_time.date)
114 if out_time > date_until:
116 duration = len(no_days)
117 res[lead.id][field] = abs(int(duration))
120 def _history_search(self, cr, uid, obj, name, args, context=None):
122 msg_obj = self.pool.get('mail.message')
123 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
124 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
127 return [('id', 'in', lead_ids)]
129 return [('id', '=', '0')]
131 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
133 for obj in self.browse(cr, uid, ids, context=context):
135 for msg in obj.message_ids:
137 res[obj.id] = msg.subject
142 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
143 select=True, help="Optional linked partner, usually after conversion of the lead"),
145 'id': fields.integer('ID', readonly=True),
146 'name': fields.char('Name', size=64, select=1),
147 'active': fields.boolean('Active', required=False),
148 'date_action_last': fields.datetime('Last Action', readonly=1),
149 'date_action_next': fields.datetime('Next Action', readonly=1),
150 'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
151 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
152 select=True, help='When sending mails, the default email address is taken from the sales team.'),
153 'create_date': fields.datetime('Creation Date' , readonly=True),
154 '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"),
155 'description': fields.text('Notes'),
156 'write_date': fields.datetime('Update Date' , readonly=True),
158 'categ_id': fields.many2one('crm.case.categ', 'Category', \
159 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
160 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
161 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
162 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
163 'contact_name': fields.char('Contact Name', size=64),
164 '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),
165 'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
166 'opt_out': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
167 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
168 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
169 'date_closed': fields.datetime('Closed', readonly=True),
170 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
171 'user_id': fields.many2one('res.users', 'Salesman'),
172 'referred': fields.char('Referred By', size=64),
173 'date_open': fields.datetime('Opened', readonly=True),
174 'day_open': fields.function(_compute_day, string='Days to Open', \
175 multi='day_open', type="float", store=True),
176 'day_close': fields.function(_compute_day, string='Days to Close', \
177 multi='day_close', type="float", store=True),
178 'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
179 help='The state is set to \'Draft\', when a case is created.\
180 \nIf the case is in progress the state is set to \'Open\'.\
181 \nWhen the case is over, the state is set to \'Done\'.\
182 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
183 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
184 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
186 # Only used for type opportunity
187 'probability': fields.float('Probability (%)',group_operator="avg"),
188 'planned_revenue': fields.float('Expected Revenue'),
189 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
190 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
191 'phone': fields.char("Phone", size=64),
192 'date_deadline': fields.date('Expected Closing'),
193 'date_action': fields.date('Next Action Date', select=True),
194 'title_action': fields.char('Next Action', size=64),
195 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
196 'color': fields.integer('Color Index'),
197 'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
198 'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
199 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
200 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
201 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
206 'active': lambda *a: 1,
207 'user_id': crm_case._get_default_user,
208 'email_from': crm_case._get_default_email,
209 'state': lambda *a: 'draft',
210 'type': lambda *a: 'lead',
211 'section_id': crm_case._get_section,
212 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
213 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
217 def get_needaction_user_ids(self, cr, uid, ids, context=None):
218 result = dict.fromkeys(ids, [])
219 for obj in self.browse(cr, uid, ids, context=context):
220 # salesman must perform an action when in draft mode
221 if obj.state == 'draft' and obj.user_id:
222 result[obj.id] = [obj.user_id.id]
225 def create(self, cr, uid, vals, context=None):
226 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
227 self.create_send_note(cr, uid, [obj_id], context=context)
230 def on_change_optin(self, cr, uid, ids, optin):
231 return {'value':{'optin':optin,'opt_out':False}}
233 def on_change_optout(self, cr, uid, ids, optout):
234 return {'value':{'opt_out':optout,'optin':False}}
236 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
239 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
240 if not stage.on_change:
242 return {'value':{'probability': stage.probability}}
244 def stage_find_percent(self, cr, uid, percent, section_id):
245 """ Return the first stage with a probability == percent
247 stage_pool = self.pool.get('crm.case.stage')
249 ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
251 ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
257 def stage_find_lost(self, cr, uid, section_id):
258 return self.stage_find_percent(cr, uid, 0.0, section_id)
260 def stage_find_won(self, cr, uid, section_id):
261 return self.stage_find_percent(cr, uid, 100.0, section_id)
263 def case_open(self, cr, uid, ids, context=None):
264 for lead in self.browse(cr, uid, ids, context=context):
265 if lead.state == 'draft':
266 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
267 self.write(cr, uid, [lead.id], value)
268 if lead.type == 'opportunity' and not lead.stage_id:
269 stage_id = self.stage_find(cr, uid, lead.section_id.id or False, [('sequence','>',0)])
271 self.stage_set(cr, uid, [lead.id], stage_id)
272 res = super(crm_lead, self).case_open(cr, uid, ids, context)
275 def case_close(self, cr, uid, ids, context=None):
276 res = super(crm_lead, self).case_close(cr, uid, ids, context)
277 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
280 def case_cancel(self, cr, uid, ids, context=None):
281 """Overrides cancel for crm_case for setting probability
283 res = super(crm_lead, self).case_cancel(cr, uid, ids, context)
284 self.write(cr, uid, ids, {'probability' : 0.0})
287 def case_reset(self, cr, uid, ids, context=None):
288 """Overrides reset as draft in order to set the stage field as empty
290 res = super(crm_lead, self).case_reset(cr, uid, ids, context)
291 self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
294 def case_mark_lost(self, cr, uid, ids, context=None):
295 """Mark the case as lost: state = done and probability = 0%
297 res = super(crm_lead, self).case_close(cr, uid, ids, context)
298 self.write(cr, uid, ids, {'probability' : 0.0})
299 for lead in self.browse(cr, uid, ids):
300 stage_id = self.stage_find_lost(cr, uid, lead.section_id.id or False)
302 self.stage_set(cr, uid, [lead.id], stage_id)
305 def case_mark_won(self, cr, uid, ids, context=None):
306 """Mark the case as lost: state = done and probability = 0%
308 res = super(crm_lead, self).case_close(cr, uid, ids, context=None)
309 self.write(cr, uid, ids, {'probability' : 100.0})
310 for lead in self.browse(cr, uid, ids):
311 stage_id = self.stage_find_won(cr, uid, lead.section_id.id or False)
313 self.stage_set(cr, uid, [lead.id], stage_id)
314 self.case_mark_won_send_note(cr, uid, [lead.id], context=context)
317 def set_priority(self, cr, uid, ids, priority):
320 return self.write(cr, uid, ids, {'priority' : priority})
322 def set_high_priority(self, cr, uid, ids, context=None):
323 """Set lead priority to high
325 return self.set_priority(cr, uid, ids, '1')
327 def set_normal_priority(self, cr, uid, ids, context=None):
328 """Set lead priority to normal
330 return self.set_priority(cr, uid, ids, '3')
333 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
334 # prepare opportunity data into dictionary for merging
335 opportunities = self.browse(cr, uid, ids, context=context)
336 def _get_first_not_null(attr):
337 if hasattr(oldest, attr):
338 return getattr(oldest, attr)
339 for opportunity in opportunities:
340 if hasattr(opportunity, attr):
341 return getattr(opportunity, attr)
344 def _get_first_not_null_id(attr):
345 res = _get_first_not_null(attr)
346 return res and res.id or False
348 def _concat_all(attr):
349 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
352 for field_name in fields:
353 field_info = self._all_columns.get(field_name)
354 if field_info is None:
356 field = field_info.column
357 if field._type in ('many2many', 'one2many'):
359 elif field._type == 'many2one':
360 data[field_name] = _get_first_not_null_id(field_name) # !!
361 elif field._type == 'text':
362 data[field_name] = _concat_all(field_name) #not lost
364 data[field_name] = _get_first_not_null(field_name) #not lost
367 def _merge_find_oldest(self, cr, uid, ids, context=None):
370 #TOCHECK: where pass 'convert' in context ?
371 if context.get('convert'):
372 ids = list(set(ids) - set(context.get('lead_ids', False)) )
374 #search opportunities order by create date
375 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
376 oldest_id = opportunity_ids[0]
377 return self.browse(cr, uid, oldest_id, context=context)
379 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
382 body.append("%s\n" % (title))
383 for field_name in fields:
384 field_info = self._all_columns.get(field_name)
385 if field_info is None:
387 field = field_info.column
390 if field._type == 'selection':
391 if hasattr(field.selection, '__call__'):
392 key = field.selection(self, cr, uid, context=context)
394 key = field.selection
395 value = dict(key).get(lead[field_name], lead[field_name])
396 elif field._type == 'many2one':
398 value = lead[field_name].name_get()[0][1]
400 value = lead[field_name]
402 body.append("%s: %s" % (field.string, value or ''))
403 return "\n".join(body + ['---'])
405 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
406 #TOFIX: mail template should be used instead of fix body, subject text
408 merge_message = _('Merged opportunities')
409 subject = [merge_message]
410 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
411 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
412 'country_id', 'city', 'street', 'street2', 'zip']
413 for opportunity in opportunities:
414 subject.append(opportunity.name)
415 title = "%s : %s" % (merge_message, opportunity.name)
416 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
418 subject = subject[0] + ", ".join(subject[1:])
419 details = "\n\n".join(details)
420 return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
422 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
423 message = self.pool.get('mail.message')
424 for opportunity in opportunities:
425 for history in opportunity.message_ids:
426 message.write(cr, uid, history.id, {
427 'res_id': opportunity_id,
428 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
433 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
434 attachment = self.pool.get('ir.attachment')
436 # return attachments of opportunity
437 def _get_attachments(opportunity_id):
438 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
439 return attachment.browse(cr, uid, attachment_ids, context=context)
442 first_attachments = _get_attachments(opportunity_id)
443 for opportunity in opportunities:
444 attachments = _get_attachments(opportunity.id)
445 for first in first_attachments:
446 for attachment in attachments:
447 if attachment.name == first.name:
449 name = "%s (%s)" % (attachment.name, count,),
450 res_id = opportunity_id,
452 attachment.write(values)
457 def merge_opportunity(self, cr, uid, ids, context=None):
459 To merge opportunities
460 :param ids: list of opportunities ids to merge
462 if context is None: context = {}
464 #TOCHECK: where pass lead_ids in context?
465 lead_ids = context and context.get('lead_ids', []) or []
468 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
470 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
471 opportunities = self.browse(cr, uid, ids, context=context)
472 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
473 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
474 if ctx_opportunities :
475 first_opportunity = ctx_opportunities[0]
476 tail_opportunities = opportunities_list
478 first_opportunity = opportunities_list[0]
479 tail_opportunities = opportunities_list[1:]
481 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',
482 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
483 'date_action_next', 'email_from', 'email_cc', 'partner_name']
485 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
487 # merge data into first opportunity
488 self.write(cr, uid, [first_opportunity.id], data, context=context)
490 #copy message and attachements into the first opportunity
491 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
492 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
494 #Notification about loss of information
495 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
496 #delete tail opportunities
497 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
499 #open first opportunity
500 self.case_open(cr, uid, [first_opportunity.id])
501 return first_opportunity.id
503 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
504 crm_stage = self.pool.get('crm.case.stage')
507 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
509 section_id = lead.section_id and lead.section_id.id or False
511 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
513 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
514 stage_id = stage_ids and stage_ids[0] or False
516 'planned_revenue': lead.planned_revenue,
517 'probability': lead.probability,
519 'partner_id': customer and customer.id or False,
520 'user_id': (lead.user_id and lead.user_id.id),
521 'type': 'opportunity',
522 'stage_id': stage_id or False,
523 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
524 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
527 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
528 partner = self.pool.get('res.partner')
529 mail_message = self.pool.get('mail.message')
532 customer = partner.browse(cr, uid, partner_id, context=context)
533 for lead in self.browse(cr, uid, ids, context=context):
534 if lead.state in ('done', 'cancel'):
536 if user_ids or section_id:
537 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
539 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
540 self.write(cr, uid, [lead.id], vals, context=context)
542 self.convert_opportunity_send_note(cr, uid, lead, context=context)
543 #TOCHECK: why need to change partner details in all messages of lead ?
545 msg_ids = [ x.id for x in lead.message_ids]
546 mail_message.write(cr, uid, msg_ids, {
547 'partner_id': lead.partner_id.id
551 def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
552 partner = self.pool.get('res.partner')
553 vals = { 'name': name,
554 'user_id': lead.user_id.id,
555 'comment': lead.description,
556 'section_id': lead.section_id.id or False,
557 'parent_id': parent_id,
559 'mobile': lead.mobile,
560 'email': lead.email_from and to_email(lead.email_from)[0],
562 'title': lead.title and lead.title.id or False,
563 'function': lead.function,
564 'street': lead.street,
565 'street2': lead.street2,
568 'country_id': lead.country_id and lead.country_id.id or False,
569 'state_id': lead.state_id and lead.state_id.id or False,
570 'is_company': is_company,
573 partner = partner.create(cr, uid,vals, context)
576 def _create_lead_partner(self, cr, uid, lead, context=None):
578 if lead.partner_name and lead.contact_name:
579 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
580 self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
581 elif lead.partner_name and not lead.contact_name:
582 partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
583 elif not lead.partner_name and lead.contact_name:
584 partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
586 partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
589 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
591 res_partner = self.pool.get('res.partner')
593 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
594 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
595 res = lead.write({'partner_id' : partner_id, }, context=context)
596 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
599 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
601 This function convert partner based on action.
602 if action is 'create', create new partner with contact and assign lead to new partner_id.
603 otherwise assign lead to specified partner_id
608 for lead in self.browse(cr, uid, ids, context=context):
609 if action == 'create':
611 partner_id = self._create_lead_partner(cr, uid, lead, context)
612 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
613 partner_ids[lead.id] = partner_id
616 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
618 Send mail to salesman with updated Lead details.
619 @ lead: browse record of 'crm.lead' object.
621 #TOFIX: mail template should be used here instead of fix subject, body text.
622 message = self.pool.get('mail.message')
623 email_to = lead.user_id and lead.user_id.user_email
627 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
628 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
629 subject = "lead %s converted into opportunity" % lead.name
630 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
631 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
634 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
639 value['section_id'] = team_id
640 if index < len(user_ids):
641 value['user_id'] = user_ids[index]
644 self.write(cr, uid, [lead_id], value, context=context)
647 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):
649 action :('schedule','Schedule a call'), ('log','Log a call')
651 phonecall = self.pool.get('crm.phonecall')
652 model_data = self.pool.get('ir.model.data')
655 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
657 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
658 for lead in self.browse(cr, uid, ids, context=context):
660 section_id = lead.section_id and lead.section_id.id or False
662 user_id = lead.user_id and lead.user_id.id or False
664 'name' : call_summary,
665 'opportunity_id' : lead.id,
666 'user_id' : user_id or False,
667 'categ_id' : categ_id or False,
668 'description' : desc or '',
669 'date' : schedule_time,
670 'section_id' : section_id or False,
671 'partner_id': lead.partner_id and lead.partner_id.id or False,
672 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
673 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
674 'priority': lead.priority,
676 new_id = phonecall.create(cr, uid, vals, context=context)
677 phonecall.case_open(cr, uid, [new_id], context=context)
679 phonecall.case_close(cr, uid, [new_id], context=context)
680 phonecall_dict[lead.id] = new_id
681 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
682 return phonecall_dict
685 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
686 models_data = self.pool.get('ir.model.data')
688 # Get Opportunity views
689 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
690 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
692 'name': _('Opportunity'),
694 'view_mode': 'tree, form',
695 'res_model': 'crm.lead',
696 'domain': [('type', '=', 'opportunity')],
697 'res_id': int(opportunity_id),
699 'views': [(form_view and form_view[1] or False, 'form'),
700 (tree_view and tree_view[1] or False, 'tree'),
701 (False, 'calendar'), (False, 'graph')],
702 'type': 'ir.actions.act_window',
706 def message_new(self, cr, uid, msg, custom_values=None, context=None):
707 """Automatically calls when new email message arrives"""
708 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
709 subject = msg.get('subject') or _("No Subject")
710 body = msg.get('body_text')
712 msg_from = msg.get('from')
713 priority = msg.get('priority')
716 'email_from': msg_from,
717 'email_cc': msg.get('cc'),
722 vals['priority'] = priority
723 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
724 self.write(cr, uid, [res_id], vals, context)
727 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
728 if isinstance(ids, (str, int, long)):
732 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
734 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
735 vals['priority'] = msg.get('priority')
737 'cost':'planned_cost',
738 'revenue': 'planned_revenue',
739 'probability':'probability'
742 for line in msg['body_text'].split('\n'):
744 res = tools.misc.command_re.match(line)
745 if res and maps.get(res.group(1).lower()):
746 key = maps.get(res.group(1).lower())
747 vls[key] = res.group(2).lower()
750 # Unfortunately the API is based on lists
751 # but we want to update the state based on the
752 # previous state, so we have to loop:
753 for case in self.browse(cr, uid, ids, context=context):
755 if case.state in CRM_LEAD_PENDING_STATES:
757 values.update(state=crm.AVAILABLE_STATES[1][0])
758 if not case.date_open:
759 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
760 res = self.write(cr, uid, [case.id], values, context=context)
763 def action_makeMeeting(self, cr, uid, ids, context=None):
765 This opens Meeting's calendar view to schedule meeting on current Opportunity
766 @return : Dictionary value for created Meeting view
771 data_obj = self.pool.get('ir.model.data')
772 for opp in self.browse(cr, uid, ids, context=context):
774 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
775 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
776 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
777 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
779 'default_opportunity_id': opp.id,
780 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
781 'default_user_id': uid,
782 'default_section_id': opp.section_id and opp.section_id.id or False,
783 'default_email_from': opp.email_from,
784 'default_state': 'open',
785 'default_name': opp.name
788 'name': _('Meetings'),
791 'view_mode': 'calendar,form,tree',
792 'res_model': 'crm.meeting',
794 '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')],
795 'type': 'ir.actions.act_window',
796 'search_view_id': search_view and search_view[1] or False,
802 def unlink(self, cr, uid, ids, context=None):
803 for lead in self.browse(cr, uid, ids, context):
804 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
805 raise osv.except_osv(_('Error'),
806 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
807 "You should better cancel it, instead of deleting it.") % lead.name)
808 return super(crm_lead, self).unlink(cr, uid, ids, context)
811 def write(self, cr, uid, ids, vals, context=None):
815 if 'date_closed' in vals:
816 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
818 if vals.get('stage_id'):
819 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
820 # change probability of lead(s) if required by stage
821 if not vals.get('probability') and stage.on_change:
822 vals['probability'] = stage.probability
823 for case in self.browse(cr, uid, ids, context=context):
824 message = _("Stage changed to <b>%s</b>.") % (stage.name)
825 case.message_append_note(body=message)
826 return super(crm_lead,self).write(cr, uid, ids, vals, context)
828 # ----------------------------------------
829 # OpenChatter methods and notifications
830 # ----------------------------------------
832 def message_get_subscribers(self, cr, uid, ids, context=None):
833 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
834 # add salesman to the subscribers
835 for obj in self.browse(cr, uid, ids, context=context):
837 sub_ids.append(obj.user_id.id)
838 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
840 def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
841 if isinstance(lead, (int, long)):
842 lead = self.browse(cr, uid, [lead], context=context)[0]
843 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
845 def create_send_note(self, cr, uid, ids, context=None):
847 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
848 self.message_append_note(cr, uid, [id], body=message, context=context)
851 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
852 message = _("Opportunity has been <b>lost</b>.")
853 return self.message_append_note(cr, uid, ids, body=message, context=context)
855 def case_mark_won_send_note(self, cr, uid, ids, context=None):
856 message = _("Opportunity has been <b>won</b>.")
857 return self.message_append_note(cr, uid, ids, body=message, context=context)
859 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
860 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
861 if action == 'log': prefix = 'Logged'
862 else: prefix = 'Scheduled'
863 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
864 return self. message_append_note(cr, uid, ids, body=message, context=context)
866 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
867 for lead in self.browse(cr, uid, ids, context=context):
868 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))
869 lead.message_append_note(body=message)
872 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
873 message = _("Lead has been <b>converted to an opportunity</b>.")
874 lead.message_append_note(body=message)
879 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: