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 = ['mail.thread','res.partner.address']
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 # overridden because res.partner.address has an inconvenient name_get,
64 # especially if base_contact is installed.
65 def name_get(self, cr, user, ids, context=None):
66 if isinstance(ids, (int, long)):
68 return [(r['id'], tools.ustr(r[self._rec_name]))
69 for r in self.read(cr, user, ids, [self._rec_name], context)]
71 def _compute_day(self, cr, uid, ids, fields, args, context=None):
73 @param cr: the current row, from the database cursor,
74 @param uid: the current user’s ID for security checks,
75 @param ids: List of Openday’s IDs
76 @return: difference between current date and log date
77 @param context: A standard dictionary for contextual values
79 cal_obj = self.pool.get('resource.calendar')
80 res_obj = self.pool.get('resource.resource')
83 for lead in self.browse(cr, uid, ids, context=context):
88 if field == 'day_open':
90 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
91 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
92 ans = date_open - date_create
93 date_until = lead.date_open
94 elif field == 'day_close':
96 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
97 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
98 date_until = lead.date_closed
99 ans = date_close - date_create
103 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
104 if len(resource_ids):
105 resource_id = resource_ids[0]
107 duration = float(ans.days)
108 if lead.section_id and lead.section_id.resource_calendar_id:
109 duration = float(ans.days) * 24
110 new_dates = cal_obj.interval_get(cr,
112 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
113 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
118 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
119 for in_time, out_time in new_dates:
120 if in_time.date not in no_days:
121 no_days.append(in_time.date)
122 if out_time > date_until:
124 duration = len(no_days)
125 res[lead.id][field] = abs(int(duration))
128 def _history_search(self, cr, uid, obj, name, args, context=None):
130 msg_obj = self.pool.get('mail.message')
131 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
132 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
135 return [('id', 'in', lead_ids)]
137 return [('id', '=', '0')]
139 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
141 for obj in self.browse(cr, uid, ids, context=context):
143 for msg in obj.message_ids:
145 res[obj.id] = msg.subject
150 # Overridden from res.partner.address:
151 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
152 select=True, help="Optional linked partner, usually after conversion of the lead"),
154 'id': fields.integer('ID', readonly=True),
155 'name': fields.char('Name', size=64, select=1),
156 'active': fields.boolean('Active', required=False),
157 'date_action_last': fields.datetime('Last Action', readonly=1),
158 'date_action_next': fields.datetime('Next Action', readonly=1),
159 'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
160 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
161 select=True, help='When sending mails, the default email address is taken from the sales team.'),
162 'create_date': fields.datetime('Creation Date' , readonly=True),
163 '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"),
164 'description': fields.text('Notes'),
165 'write_date': fields.datetime('Update Date' , readonly=True),
167 'categ_id': fields.many2one('crm.case.categ', 'Category', \
168 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
169 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
170 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
171 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
172 'contact_name': fields.char('Contact Name', size=64),
173 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner that will be created while converting the lead into opportunity', select=1),
174 'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
175 'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
176 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
177 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
178 'date_closed': fields.datetime('Closed', readonly=True),
179 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
180 'user_id': fields.many2one('res.users', 'Salesman'),
181 'referred': fields.char('Referred By', size=64),
182 'date_open': fields.datetime('Opened', readonly=True),
183 'day_open': fields.function(_compute_day, string='Days to Open', \
184 multi='day_open', type="float", store=True),
185 'day_close': fields.function(_compute_day, string='Days to Close', \
186 multi='day_close', type="float", store=True),
187 'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
188 help='The state is set to \'Draft\', when a case is created.\
189 \nIf the case is in progress the state is set to \'Open\'.\
190 \nWhen the case is over, the state is set to \'Done\'.\
191 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
192 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
193 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
195 # Only used for type opportunity
196 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"),
197 'probability': fields.float('Probability (%)',group_operator="avg"),
198 'planned_revenue': fields.float('Expected Revenue'),
199 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
200 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
201 'phone': fields.char("Phone", size=64),
202 'date_deadline': fields.date('Expected Closing'),
203 'date_action': fields.date('Next Action Date', select=True),
204 'title_action': fields.char('Next Action', size=64),
205 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
206 'color': fields.integer('Color Index'),
207 'partner_address_name': fields.related('partner_address_id', 'name', type='char', string='Partner Contact Name', readonly=True),
208 'partner_address_email': fields.related('partner_address_id', 'email', type='char', string='Partner Contact Email', readonly=True),
209 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
210 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
211 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
216 'active': lambda *a: 1,
217 'user_id': crm_case._get_default_user,
218 'email_from': crm_case._get_default_email,
219 'state': lambda *a: 'draft',
220 'type': lambda *a: 'lead',
221 'section_id': crm_case._get_section,
222 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
223 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
227 def create(self, cr, uid, vals, context=None):
228 obj_id = super(crm_lead, self).create(cr, uid, vals, context)
229 self.create_send_note(cr, uid, [obj_id], context=context)
233 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
234 """This function returns value of partner email based on Partner Address
237 return {'value': {'email_from': False, 'country_id': False}}
238 address = self.pool.get('res.partner.address').browse(cr, uid, add)
239 return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
241 def on_change_optin(self, cr, uid, ids, optin):
242 return {'value':{'optin':optin,'optout':False}}
244 def on_change_optout(self, cr, uid, ids, optout):
245 return {'value':{'optout':optout,'optin':False}}
247 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
250 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
251 if not stage.on_change:
253 return {'value':{'probability': stage.probability}}
255 def stage_find_percent(self, cr, uid, percent, section_id):
256 """ Return the first stage with a probability == percent
258 stage_pool = self.pool.get('crm.case.stage')
260 ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
262 ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
268 def stage_find_lost(self, cr, uid, section_id):
269 return self.stage_find_percent(cr, uid, 0.0, section_id)
271 def stage_find_won(self, cr, uid, section_id):
272 return self.stage_find_percent(cr, uid, 100.0, section_id)
274 def get_needaction_user_ids(self, cr, uid, ids, context=None):
275 result = dict.fromkeys(ids, [])
276 for obj in self.browse(cr, uid, ids, context=context):
277 # salesman must perform an action when in draft mode
278 if obj.state == 'draft' and obj.user_id:
279 result[obj.id] = [obj.user_id.id]
282 def message_get_subscribers(self, cr, uid, ids, context=None):
283 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
284 # add salesman to the subscribers
285 for obj in self.browse(cr, uid, ids, context=context):
287 sub_ids.append(obj.user_id.id)
288 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
290 def create_send_note(self, cr, uid, ids, context=None):
292 message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
293 self.message_append_note(cr, uid, [id], _('System notification'),
294 message, type='notification', context=context)
297 def case_get_note_msg_prefix(self, cr, uid, id, context=None):
298 lead = self.browse(cr, uid, [id], context=context)[0]
299 return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
301 def case_mark_lost_send_note(self, cr, uid, ids, context=None):
302 message = _("Opportunity has been <b>lost</b>.")
303 return self.message_append_note(cr, uid, ids, message, context=context)
305 def case_mark_won_send_note(self, cr, uid, ids, context=None):
306 message = _("Opportunity has been <b>won</b>.")
307 return self.message_append_note(cr, uid, ids, message, context=context)
309 def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
310 phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
311 if action == 'log': prefix = 'Logged'
312 else: prefix = 'Scheduled'
313 message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
314 return self. message_append_note(cr, uid, ids, 'System Notification', message, context=context)
316 def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
317 for lead in self.browse(cr, uid, ids, context=context):
318 message = _("%s <b>partner</b> is now set to <em>%s</em>." % (self.case_get_note_msg_prefix(cr, uid, lead.id, context=context), lead.partner_id.name))
319 lead.message_append_note('System Notification' ,message)
322 def case_open(self, cr, uid, ids, context=None):
323 for lead in self.browse(cr, uid, ids, context=context):
324 if lead.state == 'draft':
325 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
326 self.write(cr, uid, [lead.id], value)
327 if lead.type == 'opportunity' and not lead.stage_id:
328 stage_id = self.stage_find(cr, uid, lead.section_id.id or False, [('sequence','>',0)])
330 self.stage_set(cr, uid, [lead.id], stage_id)
331 res = super(crm_lead, self).case_open(cr, uid, ids, context)
334 def case_close(self, cr, uid, ids, context=None):
335 res = super(crm_lead, self).case_close(cr, uid, ids, context)
336 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
339 def case_cancel(self, cr, uid, ids, context=None):
340 """Overrides cancel for crm_case for setting probability
342 res = super(crm_lead, self).case_cancel(cr, uid, ids, context)
343 self.write(cr, uid, ids, {'probability' : 0.0})
346 def case_reset(self, cr, uid, ids, context=None):
347 """Overrides reset as draft in order to set the stage field as empty
349 res = super(crm_lead, self).case_reset(cr, uid, ids, context)
350 self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
353 def case_mark_lost(self, cr, uid, ids, context=None):
354 """Mark the case as lost: state = done and probability = 0%
356 res = super(crm_lead, self).case_close(cr, uid, ids, context)
357 self.write(cr, uid, ids, {'probability' : 0.0})
358 for lead in self.browse(cr, uid, ids):
359 stage_id = self.stage_find_lost(cr, uid, lead.section_id.id or False)
361 self.stage_set(cr, uid, [lead.id], stage_id)
364 def case_mark_won(self, cr, uid, ids, context=None):
365 """Mark the case as lost: state = done and probability = 0%
367 res = super(crm_lead, self).case_close(cr, uid, ids, context=None)
368 self.write(cr, uid, ids, {'probability' : 100.0})
369 for lead in self.browse(cr, uid, ids):
370 stage_id = self.stage_find_won(cr, uid, lead.section_id.id or False)
372 self.stage_set(cr, uid, [lead.id], stage_id)
373 self.case_mark_won_send_note(cr, uid, [lead.id], context=context)
376 def set_priority(self, cr, uid, ids, priority):
379 return self.write(cr, uid, ids, {'priority' : priority})
381 def set_high_priority(self, cr, uid, ids, context=None):
382 """Set lead priority to high
384 return self.set_priority(cr, uid, ids, '1')
386 def set_normal_priority(self, cr, uid, ids, context=None):
387 """Set lead priority to normal
389 return self.set_priority(cr, uid, ids, '3')
392 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
393 # prepare opportunity data into dictionary for merging
394 opportunities = self.browse(cr, uid, ids, context=context)
395 def _get_first_not_null(attr):
396 if hasattr(oldest, attr):
397 return getattr(oldest, attr)
398 for opportunity in opportunities:
399 if hasattr(opportunity, attr):
400 return getattr(opportunity, attr)
403 def _get_first_not_null_id(attr):
404 res = _get_first_not_null(attr)
405 return res and res.id or False
407 def _concat_all(attr):
408 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
411 for field_name in fields:
412 field_info = self._all_columns.get(field_name)
413 if field_info is None:
415 field = field_info.column
416 if field._type in ('many2many', 'one2many'):
418 elif field._type == 'many2one':
419 data[field_name] = _get_first_not_null_id(field_name) # !!
420 elif field._type == 'text':
421 data[field_name] = _concat_all(field_name) #not lost
423 data[field_name] = _get_first_not_null(field_name) #not lost
426 def _merge_find_oldest(self, cr, uid, ids, context=None):
429 #TOCHECK: where pass 'convert' in context ?
430 if context.get('convert'):
431 ids = list(set(ids) - set(context.get('lead_ids', False)) )
433 #search opportunities order by create date
434 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
435 oldest_id = opportunity_ids[0]
436 return self.browse(cr, uid, oldest_id, context=context)
438 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
441 body.append("%s\n" % (title))
442 for field_name in fields:
443 field_info = self._all_columns.get(field_name)
444 if field_info is None:
446 field = field_info.column
449 if field._type == 'selection':
450 if hasattr(field.selection, '__call__'):
451 key = field.selection(self, cr, uid, context=context)
453 key = field.selection
454 value = dict(key).get(lead[field_name], lead[field_name])
455 elif field._type == 'many2one':
457 value = lead[field_name].name_get()[0][1]
459 value = lead[field_name]
461 body.append("%s: %s" % (field.string, value or ''))
462 return "\n".join(body + ['---'])
464 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
465 #TOFIX: mail template should be used instead of fix body, subject text
467 merge_message = _('Merged opportunities')
468 subject = [merge_message]
469 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
470 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
471 'country_id', 'city', 'street', 'street2', 'zip']
472 for opportunity in opportunities:
473 subject.append(opportunity.name)
474 title = "%s : %s" % (merge_message, opportunity.name)
475 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
477 subject = subject[0] + ", ".join(subject[1:])
478 details = "\n\n".join(details)
479 return self.message_append_note(cr, uid, [opportunity_id], subject, body=details)
481 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
482 message = self.pool.get('mail.message')
483 for opportunity in opportunities:
484 for history in opportunity.message_ids:
485 message.write(cr, uid, history.id, {
486 'res_id': opportunity_id,
487 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
492 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
493 attachment = self.pool.get('ir.attachment')
495 # return attachments of opportunity
496 def _get_attachments(opportunity_id):
497 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
498 return attachment.browse(cr, uid, attachment_ids, context=context)
501 first_attachments = _get_attachments(opportunity_id)
502 for opportunity in opportunities:
503 attachments = _get_attachments(opportunity.id)
504 for first in first_attachments:
505 for attachment in attachments:
506 if attachment.name == first.name:
508 name = "%s (%s)" % (attachment.name, count,),
509 res_id = opportunity_id,
511 attachment.write(values)
516 def merge_opportunity(self, cr, uid, ids, context=None):
518 To merge opportunities
519 :param ids: list of opportunities ids to merge
521 if context is None: context = {}
523 #TOCHECK: where pass lead_ids in context?
524 lead_ids = context and context.get('lead_ids', []) or []
527 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
529 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
530 opportunities = self.browse(cr, uid, ids, context=context)
531 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
532 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
533 if ctx_opportunities :
534 first_opportunity = ctx_opportunities[0]
535 tail_opportunities = opportunities_list
537 first_opportunity = opportunities_list[0]
538 tail_opportunities = opportunities_list[1:]
540 fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id',
541 'partner_address_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
542 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
543 'date_action_next', 'email_from', 'email_cc', 'partner_name']
545 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
547 # merge data into first opportunity
548 self.write(cr, uid, [first_opportunity.id], data, context=context)
550 #copy message and attachements into the first opportunity
551 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
552 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
554 #Notification about loss of information
555 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
556 #delete tail opportunities
557 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
559 #open first opportunity
560 self.case_open(cr, uid, [first_opportunity.id])
561 return first_opportunity.id
563 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
564 crm_stage = self.pool.get('crm.case.stage')
567 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
569 section_id = lead.section_id and lead.section_id.id or False
571 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
573 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
574 stage_id = stage_ids and stage_ids[0] or False
576 'planned_revenue': lead.planned_revenue,
577 'probability': lead.probability,
579 'partner_id': customer and customer.id or False,
580 'user_id': (lead.user_id and lead.user_id.id),
581 'type': 'opportunity',
582 'stage_id': stage_id or False,
583 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
584 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
585 'partner_address_id': contact_id,
588 def convert_opportunity_send_note(self, cr, uid, lead, context=None):
589 message = _("Lead has been <b>converted to an opportunity</b>.")
590 lead.message_append_note('' ,message)
593 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
594 partner = self.pool.get('res.partner')
595 mail_message = self.pool.get('mail.message')
598 customer = partner.browse(cr, uid, partner_id, context=context)
599 for lead in self.browse(cr, uid, ids, context=context):
600 if lead.state in ('done', 'cancel'):
602 if user_ids or section_id:
603 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
605 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
606 self.write(cr, uid, [lead.id], vals, context=context)
608 self.convert_opportunity_send_note(cr, uid, lead, context=context)
609 #TOCHECK: why need to change partner details in all messages of lead ?
611 msg_ids = [ x.id for x in lead.message_ids]
612 mail_message.write(cr, uid, msg_ids, {
613 'partner_id': lead.partner_id.id
617 def _lead_create_partner(self, cr, uid, lead, context=None):
618 partner = self.pool.get('res.partner')
619 partner_id = partner.create(cr, uid, {
620 'name': lead.partner_name or lead.contact_name or lead.name,
621 'user_id': lead.user_id.id,
622 'comment': lead.description,
623 'section_id': lead.section_id.id or False,
628 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
630 res_partner = self.pool.get('res.partner')
632 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
633 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
634 res = lead.write({'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
635 self._lead_set_partner_send_note(cr, uid, [lead.id], context)
639 def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
640 address = self.pool.get('res.partner.address')
641 return address.create(cr, uid, {
642 'partner_id': partner_id,
643 'name': lead.contact_name,
645 'mobile': lead.mobile,
646 'email': lead.email_from and to_email(lead.email_from)[0],
648 'title': lead.title and lead.title.id or False,
649 'function': lead.function,
650 'street': lead.street,
651 'street2': lead.street2,
654 'country_id': lead.country_id and lead.country_id.id or False,
655 'state_id': lead.state_id and lead.state_id.id or False,
658 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
660 This function convert partner based on action.
661 if action is 'create', create new partner with contact and assign lead to new partner_id.
662 otherwise assign lead to specified partner_id
667 for lead in self.browse(cr, uid, ids, context=context):
668 if action == 'create':
670 partner_id = self._lead_create_partner(cr, uid, lead, context=context)
671 self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
672 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
673 partner_ids[lead.id] = partner_id
676 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
678 Send mail to salesman with updated Lead details.
679 @ lead: browse record of 'crm.lead' object.
681 #TOFIX: mail template should be used here instead of fix subject, body text.
682 message = self.pool.get('mail.message')
683 email_to = lead.user_id and lead.user_id.user_email
687 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
688 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
689 subject = "lead %s converted into opportunity" % lead.name
690 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
691 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
694 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
699 value['section_id'] = team_id
700 if index < len(user_ids):
701 value['user_id'] = user_ids[index]
704 self.write(cr, uid, [lead_id], value, context=context)
707 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):
709 action :('schedule','Schedule a call'), ('log','Log a call')
711 phonecall = self.pool.get('crm.phonecall')
712 model_data = self.pool.get('ir.model.data')
715 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
717 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
718 for lead in self.browse(cr, uid, ids, context=context):
720 section_id = lead.section_id and lead.section_id.id or False
722 user_id = lead.user_id and lead.user_id.id or False
724 'name' : call_summary,
725 'opportunity_id' : lead.id,
726 'user_id' : user_id or False,
727 'categ_id' : categ_id or False,
728 'description' : desc or '',
729 'date' : schedule_time,
730 'section_id' : section_id or False,
731 'partner_id': lead.partner_id and lead.partner_id.id or False,
732 'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
733 'partner_phone' : phone or lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
734 'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
735 'priority': lead.priority,
737 new_id = phonecall.create(cr, uid, vals, context=context)
738 phonecall.case_open(cr, uid, [new_id], context=context)
740 phonecall.case_close(cr, uid, [new_id], context=context)
741 phonecall_dict[lead.id] = new_id
742 self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
743 return phonecall_dict
746 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
747 models_data = self.pool.get('ir.model.data')
749 # Get Opportunity views
750 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
751 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
753 'name': _('Opportunity'),
755 'view_mode': 'tree, form',
756 'res_model': 'crm.lead',
757 'domain': [('type', '=', 'opportunity')],
758 'res_id': int(opportunity_id),
760 'views': [(form_view and form_view[1] or False, 'form'),
761 (tree_view and tree_view[1] or False, 'tree'),
762 (False, 'calendar'), (False, 'graph')],
763 'type': 'ir.actions.act_window',
767 def message_new(self, cr, uid, msg, custom_values=None, context=None):
768 """Automatically calls when new email message arrives"""
769 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
770 subject = msg.get('subject') or _("No Subject")
771 body = msg.get('body_text')
773 msg_from = msg.get('from')
774 priority = msg.get('priority')
777 'email_from': msg_from,
778 'email_cc': msg.get('cc'),
783 vals['priority'] = priority
784 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
785 self.write(cr, uid, [res_id], vals, context)
788 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
789 if isinstance(ids, (str, int, long)):
793 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
795 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
796 vals['priority'] = msg.get('priority')
798 'cost':'planned_cost',
799 'revenue': 'planned_revenue',
800 'probability':'probability'
803 for line in msg['body_text'].split('\n'):
805 res = tools.misc.command_re.match(line)
806 if res and maps.get(res.group(1).lower()):
807 key = maps.get(res.group(1).lower())
808 vls[key] = res.group(2).lower()
811 # Unfortunately the API is based on lists
812 # but we want to update the state based on the
813 # previous state, so we have to loop:
814 for case in self.browse(cr, uid, ids, context=context):
816 if case.state in CRM_LEAD_PENDING_STATES:
818 values.update(state=crm.AVAILABLE_STATES[1][0])
819 if not case.date_open:
820 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
821 res = self.write(cr, uid, [case.id], values, context=context)
824 def action_makeMeeting(self, cr, uid, ids, context=None):
826 This opens Meeting's calendar view to schedule meeting on current Opportunity
827 @return : Dictionary value for created Meeting view
832 data_obj = self.pool.get('ir.model.data')
833 for opp in self.browse(cr, uid, ids, context=context):
835 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
836 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
837 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
838 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
840 'default_opportunity_id': opp.id,
841 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
842 'default_user_id': uid,
843 'default_section_id': opp.section_id and opp.section_id.id or False,
844 'default_email_from': opp.email_from,
845 'default_state': 'open',
846 'default_name': opp.name
849 'name': _('Meetings'),
852 'view_mode': 'calendar,form,tree',
853 'res_model': 'crm.meeting',
855 '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')],
856 'type': 'ir.actions.act_window',
857 'search_view_id': search_view and search_view[1] or False,
863 def unlink(self, cr, uid, ids, context=None):
864 for lead in self.browse(cr, uid, ids, context):
865 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
866 raise osv.except_osv(_('Error'),
867 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
868 "You should better cancel it, instead of deleting it.") % lead.name)
869 return super(crm_lead, self).unlink(cr, uid, ids, context)
872 def write(self, cr, uid, ids, vals, context=None):
876 if 'date_closed' in vals:
877 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
879 if vals.get('stage_id'):
880 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
881 # change probability of lead(s) if required by stage
882 if not vals.get('probability') and stage.on_change:
883 vals['probability'] = stage.probability
884 for case in self.browse(cr, uid, ids, context=context):
885 message = _("Stage changed to <b>%s</b>.") % (stage.name)
886 case.message_append_note('', message)
887 return super(crm_lead,self).write(cr, uid, ids, vals, context)
891 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: