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._case_create_notification(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 _case_reset_notification(self, cr, uid, ids, context=None):
275 for lead in self.browse(cr, uid, ids, context=context):
276 if lead.type == 'lead':
277 message = _("Lead is <b>renewed</b>.")
278 elif lead.type == 'opportunity':
279 message = _("Opportunity is <b>renewed</b>.")
280 lead.message_append_note('' ,message, need_action_user_id=lead.user_id.id)
282 def _case_create_notification(self, cr, uid, ids, context=None):
283 for obj in self.browse(cr, uid, ids, context=context):
284 self.message_subscribe(cr, uid, ids, [obj.user_id.id], context=context)
285 if obj.type=="opportunity" and obj.state=="draft":
286 message = _("Opportunity is <b>created</b>.")
287 elif obj.type=="lead" :
288 message = _("Lead is <b>created</b>.")
290 message = _("Case is <b>created</b>.")
291 self.message_append_note(cr, uid, ids, _('System notification'),
292 message, type='notification', need_action_user_id=obj.user_id.id, context=context)
295 def _case_open_notification(self, lead, context=None):
296 if lead.state != 'draft' and lead.state != 'pending':
298 if lead.type == 'lead':
299 message = _("Lead is <b>opened</b>.")
300 elif lead.type == 'opportunity':
301 message = _("Opportunity is <b>opened</b>.")
303 message = _("Case is <b>opened</b>.")
304 lead.message_append_note('' ,message, need_action_user_id=lead.user_id.id)
306 def _case_close_notification(self, lead, context=None):
307 lead[0].message_mark_done(context)
308 if lead[0].type == 'lead':
309 message = _("Lead is <b>closed</b>.")
310 elif lead[0].type == 'opportunity':
311 message = _("Opportunity is <b>closed</b>.")
313 message = _("Case is <b>closed</b>.")
314 lead[0].message_append_note('' ,message)
316 def _case_mark_lost_notification(self, lead, context=None):
317 lead.message_mark_done(context)
318 message = _("Opportunity is <b>marked as lost</b>.")
319 lead.message_append_note('' ,message)
321 def _case_mark_won_notification(self, lead, context=None):
322 lead.message_mark_done(context)
323 message = _("Opportunity is <b>won</b>.")
324 lead.message_append_note('' ,message)
326 def _case_cancel_notification(self, lead, context=None):
327 lead[0].message_mark_done(context)
328 if lead[0].type == 'lead':
329 message = _("Lead is <b>cancelled</b>.")
330 elif lead[0].type == 'opportunity':
331 message = _("Opportunity is <b>cancelled</b>.")
332 lead[0].message_append_note('' ,message)
334 def _case_pending_notification(self, case, context=None):
335 if case[0].type == 'lead':
336 message = _("Lead is <b>pending</b>.")
337 elif case[0].type == 'opportunity':
338 message = _("Opportunity is <b>pending</b>.")
339 case[0].message_append_note('' ,message)
341 def _case_escalate_notification(self, case, context=None):
342 message = _("Lead is <b>escalated</b>.")
343 case.message_append_note('' ,message)
345 def _case_phonecall_notification(self, cr, uid, ids, case, phonecall, action, context=None):
346 for obj in phonecall.browse(cr, uid, ids, context=context):
347 if action == "schedule" :
348 message = _("<b>%s a call</b> for the %s.") % (action, obj.date)
350 message = _("<b>%s a call</b>.") % (action)
351 case.message_append_note('', message)
353 def _case_partner_notification(self, lead, context=None):
354 if lead.type == 'lead':
355 message = _("Partner is <b>created</b> for this lead.")
356 elif lead.type == 'opportunity':
357 message = _("Partner is <b>created</b> for this opportunity.")
358 lead.message_append_note('' ,message)
360 def case_open(self, cr, uid, ids, context=None):
361 res = super(crm_lead, self).case_open(cr, uid, ids, context)
362 for lead in self.browse(cr, uid, ids, context=context):
363 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
364 self.write(cr, uid, [lead.id], value)
365 if lead.type == 'opportunity' and not lead.stage_id:
366 stage_id = self.stage_find(cr, uid, lead.section_id.id or False, [('sequence','>',0)])
368 self.stage_set(cr, uid, [lead.id], stage_id)
371 def case_close(self, cr, uid, ids, context=None):
372 res = super(crm_lead, self).case_close(cr, uid, ids, context)
373 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
376 def case_cancel(self, cr, uid, ids, context=None):
377 """Overrides cancel for crm_case for setting probability
379 res = super(crm_lead, self).case_cancel(cr, uid, ids, context)
380 self.write(cr, uid, ids, {'probability' : 0.0})
383 def case_reset(self, cr, uid, ids, context=None):
384 """Overrides reset as draft in order to set the stage field as empty
386 res = super(crm_lead, self).case_reset(cr, uid, ids, context)
387 self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
388 self._case_reset_notification(cr, uid, ids, context=context)
391 def case_mark_lost(self, cr, uid, ids, context=None):
392 """Mark the case as lost: state = done and probability = 0%
394 res = super(crm_lead, self).case_close(cr, uid, ids, context)
395 self.write(cr, uid, ids, {'probability' : 0.0})
396 for lead in self.browse(cr, uid, ids):
397 stage_id = self.stage_find_lost(cr, uid, lead.section_id.id or False)
399 self.stage_set(cr, uid, [lead.id], stage_id)
402 def case_mark_won(self, cr, uid, ids, context=None):
403 """Mark the case as lost: state = done and probability = 0%
405 res = super(crm_lead, self).case_close(cr, uid, ids, context=None)
406 self.write(cr, uid, ids, {'probability' : 100.0})
407 for lead in self.browse(cr, uid, ids):
408 stage_id = self.stage_find_won(cr, uid, lead.section_id.id or False)
410 self.stage_set(cr, uid, [lead.id], stage_id)
411 self._case_mark_won_notification(lead, context=context)
414 def set_priority(self, cr, uid, ids, priority):
417 return self.write(cr, uid, ids, {'priority' : priority})
419 def set_high_priority(self, cr, uid, ids, context=None):
420 """Set lead priority to high
422 return self.set_priority(cr, uid, ids, '1')
424 def set_normal_priority(self, cr, uid, ids, context=None):
425 """Set lead priority to normal
427 return self.set_priority(cr, uid, ids, '3')
430 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
431 # prepare opportunity data into dictionary for merging
432 opportunities = self.browse(cr, uid, ids, context=context)
433 def _get_first_not_null(attr):
434 if hasattr(oldest, attr):
435 return getattr(oldest, attr)
436 for opportunity in opportunities:
437 if hasattr(opportunity, attr):
438 return getattr(opportunity, attr)
441 def _get_first_not_null_id(attr):
442 res = _get_first_not_null(attr)
443 return res and res.id or False
445 def _concat_all(attr):
446 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
449 for field_name in fields:
450 field_info = self._all_columns.get(field_name)
451 if field_info is None:
453 field = field_info.column
454 if field._type in ('many2many', 'one2many'):
456 elif field._type == 'many2one':
457 data[field_name] = _get_first_not_null_id(field_name) # !!
458 elif field._type == 'text':
459 data[field_name] = _concat_all(field_name) #not lost
461 data[field_name] = _get_first_not_null(field_name) #not lost
464 def _merge_find_oldest(self, cr, uid, ids, context=None):
467 #TOCHECK: where pass 'convert' in context ?
468 if context.get('convert'):
469 ids = list(set(ids) - set(context.get('lead_ids', False)) )
471 #search opportunities order by create date
472 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
473 oldest_id = opportunity_ids[0]
474 return self.browse(cr, uid, oldest_id, context=context)
476 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
479 body.append("%s\n" % (title))
480 for field_name in fields:
481 field_info = self._all_columns.get(field_name)
482 if field_info is None:
484 field = field_info.column
487 if field._type == 'selection':
488 if hasattr(field.selection, '__call__'):
489 key = field.selection(self, cr, uid, context=context)
491 key = field.selection
492 value = dict(key).get(lead[field_name], lead[field_name])
493 elif field._type == 'many2one':
495 value = lead[field_name].name_get()[0][1]
497 value = lead[field_name]
499 body.append("%s: %s" % (field.string, value or ''))
500 return "\n".join(body + ['---'])
502 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
503 #TOFIX: mail template should be used instead of fix body, subject text
505 merge_message = _('Merged opportunities')
506 subject = [merge_message]
507 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
508 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
509 'country_id', 'city', 'street', 'street2', 'zip']
510 for opportunity in opportunities:
511 subject.append(opportunity.name)
512 title = "%s : %s" % (merge_message, opportunity.name)
513 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
515 subject = subject[0] + ", ".join(subject[1:])
516 details = "\n\n".join(details)
517 return opportunity.message_append_note(subject, body=details, need_action_user_id=opportunity.user_id.id)
519 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
520 message = self.pool.get('mail.message')
521 for opportunity in opportunities:
522 for history in opportunity.message_ids:
523 message.write(cr, uid, history.id, {
524 'res_id': opportunity_id,
525 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
530 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
531 attachment = self.pool.get('ir.attachment')
533 # return attachments of opportunity
534 def _get_attachments(opportunity_id):
535 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
536 return attachment.browse(cr, uid, attachment_ids, context=context)
539 first_attachments = _get_attachments(opportunity_id)
540 for opportunity in opportunities:
541 attachments = _get_attachments(opportunity.id)
542 for first in first_attachments:
543 for attachment in attachments:
544 if attachment.name == first.name:
546 name = "%s (%s)" % (attachment.name, count,),
547 res_id = opportunity_id,
549 attachment.write(values)
554 def merge_opportunity(self, cr, uid, ids, context=None):
556 To merge opportunities
557 :param ids: list of opportunities ids to merge
559 if context is None: context = {}
561 #TOCHECK: where pass lead_ids in context?
562 lead_ids = context and context.get('lead_ids', []) or []
565 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
567 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
568 opportunities = self.browse(cr, uid, ids, context=context)
569 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
570 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
571 if ctx_opportunities :
572 first_opportunity = ctx_opportunities[0]
573 tail_opportunities = opportunities_list
575 first_opportunity = opportunities_list[0]
576 tail_opportunities = opportunities_list[1:]
578 fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id',
579 'partner_address_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
580 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
581 'date_action_next', 'email_from', 'email_cc', 'partner_name']
583 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
585 # merge data into first opportunity
586 self.write(cr, uid, [first_opportunity.id], data, context=context)
588 #copy message and attachements into the first opportunity
589 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
590 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
592 #Notification about loss of information
593 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
594 #delete tail opportunities
595 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
597 #open first opportunity
598 self.case_open(cr, uid, [first_opportunity.id])
599 return first_opportunity.id
601 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
602 crm_stage = self.pool.get('crm.case.stage')
605 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
607 section_id = lead.section_id and lead.section_id.id or False
609 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
611 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
612 stage_id = stage_ids and stage_ids[0] or False
614 'planned_revenue': lead.planned_revenue,
615 'probability': lead.probability,
617 'partner_id': customer and customer.id or False,
618 'user_id': (lead.user_id and lead.user_id.id),
619 'type': 'opportunity',
620 'stage_id': stage_id or False,
621 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
622 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
623 'partner_address_id': contact_id,
626 def _convert_opportunity_notification(self, cr, uid, lead, context=None):
627 success_message = _("Lead is <b>converted to an opportunity</b>.")
628 lead.message_append_note(success_message ,success_message, need_action_user_id=lead.user_id.id)
631 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
632 partner = self.pool.get('res.partner')
633 mail_message = self.pool.get('mail.message')
636 customer = partner.browse(cr, uid, partner_id, context=context)
637 for lead in self.browse(cr, uid, ids, context=context):
638 if lead.state in ('done', 'cancel'):
640 if user_ids or section_id:
641 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
643 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
644 self.write(cr, uid, [lead.id], vals, context=context)
646 self._convert_opportunity_notification(cr, uid, lead, context=context)
647 #TOCHECK: why need to change partner details in all messages of lead ?
649 msg_ids = [ x.id for x in lead.message_ids]
650 mail_message.write(cr, uid, msg_ids, {
651 'partner_id': lead.partner_id.id
655 def _lead_create_partner(self, cr, uid, lead, context=None):
656 partner = self.pool.get('res.partner')
657 partner_id = partner.create(cr, uid, {
658 'name': lead.partner_name or lead.contact_name or lead.name,
659 'user_id': lead.user_id.id,
660 'comment': lead.description,
661 'section_id': lead.section_id.id or False,
666 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
668 res_partner = self.pool.get('res.partner')
670 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
671 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
672 res = lead.write({'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
673 self._case_partner_notification(lead,context)
676 def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
677 address = self.pool.get('res.partner.address')
678 return address.create(cr, uid, {
679 'partner_id': partner_id,
680 'name': lead.contact_name,
682 'mobile': lead.mobile,
683 'email': lead.email_from and to_email(lead.email_from)[0],
685 'title': lead.title and lead.title.id or False,
686 'function': lead.function,
687 'street': lead.street,
688 'street2': lead.street2,
691 'country_id': lead.country_id and lead.country_id.id or False,
692 'state_id': lead.state_id and lead.state_id.id or False,
695 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
697 This function convert partner based on action.
698 if action is 'create', create new partner with contact and assign lead to new partner_id.
699 otherwise assign lead to specified partner_id
704 for lead in self.browse(cr, uid, ids, context=context):
705 if action == 'create':
707 partner_id = self._lead_create_partner(cr, uid, lead, context=context)
708 self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
709 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
710 partner_ids[lead.id] = partner_id
713 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
715 Send mail to salesman with updated Lead details.
716 @ lead: browse record of 'crm.lead' object.
718 #TOFIX: mail template should be used here instead of fix subject, body text.
719 message = self.pool.get('mail.message')
720 email_to = lead.user_id and lead.user_id.user_email
724 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
725 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
726 subject = "lead %s converted into opportunity" % lead.name
727 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
728 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
731 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
736 value['section_id'] = team_id
737 if index < len(user_ids):
738 value['user_id'] = user_ids[index]
741 self.write(cr, uid, [lead_id], value, context=context)
744 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):
746 action :('schedule','Schedule a call'), ('log','Log a call')
748 phonecall = self.pool.get('crm.phonecall')
749 model_data = self.pool.get('ir.model.data')
752 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
754 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
755 for lead in self.browse(cr, uid, ids, context=context):
757 section_id = lead.section_id and lead.section_id.id or False
759 user_id = lead.user_id and lead.user_id.id or False
761 'name' : call_summary,
762 'opportunity_id' : lead.id,
763 'user_id' : user_id or False,
764 'categ_id' : categ_id or False,
765 'description' : desc or '',
766 'date' : schedule_time,
767 'section_id' : section_id or False,
768 'partner_id': lead.partner_id and lead.partner_id.id or False,
769 'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
770 'partner_phone' : phone or lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
771 'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
772 'priority': lead.priority,
774 new_id = phonecall.create(cr, uid, vals, context=context)
775 phonecall.case_open(cr, uid, [new_id])
777 phonecall.case_close(cr, uid, [new_id])
778 phonecall_dict[lead.id] = new_id
779 self._case_phonecall_notification(cr, uid, [new_id], lead, phonecall, action, context=context)
780 return phonecall_dict
783 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
784 models_data = self.pool.get('ir.model.data')
786 # Get Opportunity views
787 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
788 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
790 'name': _('Opportunity'),
792 'view_mode': 'tree, form',
793 'res_model': 'crm.lead',
794 'domain': [('type', '=', 'opportunity')],
795 'res_id': int(opportunity_id),
797 'views': [(form_view and form_view[1] or False, 'form'),
798 (tree_view and tree_view[1] or False, 'tree'),
799 (False, 'calendar'), (False, 'graph')],
800 'type': 'ir.actions.act_window',
804 def message_new(self, cr, uid, msg, custom_values=None, context=None):
805 """Automatically calls when new email message arrives"""
806 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
807 subject = msg.get('subject') or _("No Subject")
808 body = msg.get('body_text')
810 msg_from = msg.get('from')
811 priority = msg.get('priority')
814 'email_from': msg_from,
815 'email_cc': msg.get('cc'),
820 vals['priority'] = priority
821 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
822 self.write(cr, uid, [res_id], vals, context)
825 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
826 if isinstance(ids, (str, int, long)):
830 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
832 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
833 vals['priority'] = msg.get('priority')
835 'cost':'planned_cost',
836 'revenue': 'planned_revenue',
837 'probability':'probability'
840 for line in msg['body_text'].split('\n'):
842 res = tools.misc.command_re.match(line)
843 if res and maps.get(res.group(1).lower()):
844 key = maps.get(res.group(1).lower())
845 vls[key] = res.group(2).lower()
848 # Unfortunately the API is based on lists
849 # but we want to update the state based on the
850 # previous state, so we have to loop:
851 for case in self.browse(cr, uid, ids, context=context):
853 if case.state in CRM_LEAD_PENDING_STATES:
855 values.update(state=crm.AVAILABLE_STATES[1][0])
856 if not case.date_open:
857 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
858 res = self.write(cr, uid, [case.id], values, context=context)
861 def action_makeMeeting(self, cr, uid, ids, context=None):
863 This opens Meeting's calendar view to schedule meeting on current Opportunity
864 @return : Dictionary value for created Meeting view
869 data_obj = self.pool.get('ir.model.data')
870 for opp in self.browse(cr, uid, ids, context=context):
872 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
873 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
874 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
875 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
877 'default_opportunity_id': opp.id,
878 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
879 'default_user_id': uid,
880 'default_section_id': opp.section_id and opp.section_id.id or False,
881 'default_email_from': opp.email_from,
882 'default_state': 'open',
883 'default_name': opp.name
886 'name': _('Meetings'),
889 'view_mode': 'calendar,form,tree',
890 'res_model': 'crm.meeting',
892 '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')],
893 'type': 'ir.actions.act_window',
894 'search_view_id': search_view and search_view[1] or False,
900 def unlink(self, cr, uid, ids, context=None):
901 for lead in self.browse(cr, uid, ids, context):
902 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
903 raise osv.except_osv(_('Error'),
904 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
905 "You should better cancel it, instead of deleting it.") % lead.name)
906 return super(crm_lead, self).unlink(cr, uid, ids, context)
909 def write(self, cr, uid, ids, vals, context=None):
913 if 'date_closed' in vals:
914 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
916 if vals.get('stage_id'):
917 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
918 # change probability of lead(s) if required by stage
919 if not vals.get('probability') and stage.on_change:
920 vals['probability'] = stage.probability
921 for case in self.browse(cr, uid, ids, context=context):
922 if case.type == 'lead' or context.get('stage_type') == 'lead':
923 message = _("Changed to <b>%s</b>.") % (stage.name)
924 case.message_append_note('', message)
925 elif case.type == 'opportunity':
926 message = _("Changed to <b>%s</b>.") % (stage.name)
927 case.message_append_note('', message)
929 return super(crm_lead,self).write(cr, uid, ids, vals, context)
933 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: