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 = _("The lead is <b>renewed</b>.")
278 elif lead.type == 'opportunity':
279 message = _("The 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 = _("The case has been <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 = _("The lead has been <b>opened</b>.")
300 elif lead.type == 'opportunity':
301 message = _("The opportunity has been <b>opened</b>.")
303 message = _("The case has been <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 = _("The lead has been <b>closed</b>.")
310 elif lead[0].type == 'opportunity':
311 message = _("The opportunity has been <b>closed</b>.")
313 message = _("The case has been <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 = _("The opportunity has been <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 = _("The opportunity has been <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 = _("The lead has been <b>cancelled</b>.")
330 elif lead[0].type == 'opportunity':
331 message = _("The opportunity has been <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 = _("The lead is <b>pending</b>.")
337 elif case[0].type == 'opportunity':
338 message = _("The opportunity is <b>pending</b>.")
339 case[0].message_append_note('' ,message)
341 def _case_escalate_notification(self, case, context=None):
342 message = _("The 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)
352 if action == "schedule" :
353 phonecall.message_append_note(cr, uid, ids, '', message, need_action_user_id=obj.user_id.id)
355 def case_open(self, cr, uid, ids, context=None):
356 res = super(crm_lead, self).case_open(cr, uid, ids, context)
357 for lead in self.browse(cr, uid, ids, context=context):
358 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
359 self.write(cr, uid, [lead.id], value)
360 if lead.type == 'opportunity' and not lead.stage_id:
361 stage_id = self.stage_find(cr, uid, lead.section_id.id or False, [('sequence','>',0)])
363 self.stage_set(cr, uid, [lead.id], stage_id)
366 def case_close(self, cr, uid, ids, context=None):
367 res = super(crm_lead, self).case_close(cr, uid, ids, context)
368 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
371 def case_cancel(self, cr, uid, ids, context=None):
372 """Overrides cancel for crm_case for setting probability
374 res = super(crm_lead, self).case_cancel(cr, uid, ids, context)
375 self.write(cr, uid, ids, {'probability' : 0.0})
378 def case_reset(self, cr, uid, ids, context=None):
379 """Overrides reset as draft in order to set the stage field as empty
381 res = super(crm_lead, self).case_reset(cr, uid, ids, context)
382 self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
383 self._case_reset_notification(cr, uid, ids, context=context)
386 def case_mark_lost(self, cr, uid, ids, context=None):
387 """Mark the case as lost: state = done and probability = 0%
389 res = super(crm_lead, self).case_close(cr, uid, ids, context)
390 self.write(cr, uid, ids, {'probability' : 0.0})
391 for lead in self.browse(cr, uid, ids):
392 stage_id = self.stage_find_lost(cr, uid, lead.section_id.id or False)
394 self.stage_set(cr, uid, [lead.id], stage_id)
397 def case_mark_won(self, cr, uid, ids, context=None):
398 """Mark the case as lost: state = done and probability = 0%
400 res = super(crm_lead, self).case_close(cr, uid, ids, context=None)
401 self.write(cr, uid, ids, {'probability' : 100.0})
402 for lead in self.browse(cr, uid, ids):
403 stage_id = self.stage_find_won(cr, uid, lead.section_id.id or False)
405 self.stage_set(cr, uid, [lead.id], stage_id)
406 self._case_mark_won_notification(lead, context=context)
409 def set_priority(self, cr, uid, ids, priority):
412 return self.write(cr, uid, ids, {'priority' : priority})
414 def set_high_priority(self, cr, uid, ids, context=None):
415 """Set lead priority to high
417 return self.set_priority(cr, uid, ids, '1')
419 def set_normal_priority(self, cr, uid, ids, context=None):
420 """Set lead priority to normal
422 return self.set_priority(cr, uid, ids, '3')
425 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
426 # prepare opportunity data into dictionary for merging
427 opportunities = self.browse(cr, uid, ids, context=context)
428 def _get_first_not_null(attr):
429 if hasattr(oldest, attr):
430 return getattr(oldest, attr)
431 for opportunity in opportunities:
432 if hasattr(opportunity, attr):
433 return getattr(opportunity, attr)
436 def _get_first_not_null_id(attr):
437 res = _get_first_not_null(attr)
438 return res and res.id or False
440 def _concat_all(attr):
441 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
444 for field_name in fields:
445 field_info = self._all_columns.get(field_name)
446 if field_info is None:
448 field = field_info.column
449 if field._type in ('many2many', 'one2many'):
451 elif field._type == 'many2one':
452 data[field_name] = _get_first_not_null_id(field_name) # !!
453 elif field._type == 'text':
454 data[field_name] = _concat_all(field_name) #not lost
456 data[field_name] = _get_first_not_null(field_name) #not lost
459 def _merge_find_oldest(self, cr, uid, ids, context=None):
462 #TOCHECK: where pass 'convert' in context ?
463 if context.get('convert'):
464 ids = list(set(ids) - set(context.get('lead_ids', False)) )
466 #search opportunities order by create date
467 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
468 oldest_id = opportunity_ids[0]
469 return self.browse(cr, uid, oldest_id, context=context)
471 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
474 body.append("%s\n" % (title))
475 for field_name in fields:
476 field_info = self._all_columns.get(field_name)
477 if field_info is None:
479 field = field_info.column
482 if field._type == 'selection':
483 if hasattr(field.selection, '__call__'):
484 key = field.selection(self, cr, uid, context=context)
486 key = field.selection
487 value = dict(key).get(lead[field_name], lead[field_name])
488 elif field._type == 'many2one':
490 value = lead[field_name].name_get()[0][1]
492 value = lead[field_name]
494 body.append("%s: %s" % (field.string, value or ''))
495 return "\n".join(body + ['---'])
497 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
498 #TOFIX: mail template should be used instead of fix body, subject text
500 merge_message = _('Merged opportunities')
501 subject = [merge_message]
502 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
503 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
504 'country_id', 'city', 'street', 'street2', 'zip']
505 for opportunity in opportunities:
506 subject.append(opportunity.name)
507 title = "%s : %s" % (merge_message, opportunity.name)
508 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
510 subject = subject[0] + ", ".join(subject[1:])
511 details = "\n\n".join(details)
512 return opportunity.message_append_note(subject, body=details, need_action_user_id=opportunity.user_id.id)
514 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
515 message = self.pool.get('mail.message')
516 for opportunity in opportunities:
517 for history in opportunity.message_ids:
518 message.write(cr, uid, history.id, {
519 'res_id': opportunity_id,
520 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
525 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
526 attachment = self.pool.get('ir.attachment')
528 # return attachments of opportunity
529 def _get_attachments(opportunity_id):
530 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
531 return attachment.browse(cr, uid, attachment_ids, context=context)
534 first_attachments = _get_attachments(opportunity_id)
535 for opportunity in opportunities:
536 attachments = _get_attachments(opportunity.id)
537 for first in first_attachments:
538 for attachment in attachments:
539 if attachment.name == first.name:
541 name = "%s (%s)" % (attachment.name, count,),
542 res_id = opportunity_id,
544 attachment.write(values)
549 def merge_opportunity(self, cr, uid, ids, context=None):
551 To merge opportunities
552 :param ids: list of opportunities ids to merge
554 if context is None: context = {}
556 #TOCHECK: where pass lead_ids in context?
557 lead_ids = context and context.get('lead_ids', []) or []
560 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
562 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
563 opportunities = self.browse(cr, uid, ids, context=context)
564 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
565 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
566 if ctx_opportunities :
567 first_opportunity = ctx_opportunities[0]
568 tail_opportunities = opportunities_list
570 first_opportunity = opportunities_list[0]
571 tail_opportunities = opportunities_list[1:]
573 fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id',
574 'partner_address_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
575 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
576 'date_action_next', 'email_from', 'email_cc', 'partner_name']
578 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
580 # merge data into first opportunity
581 self.write(cr, uid, [first_opportunity.id], data, context=context)
583 #copy message and attachements into the first opportunity
584 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
585 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
587 #Notification about loss of information
588 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
589 #delete tail opportunities
590 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
592 #open first opportunity
593 self.case_open(cr, uid, [first_opportunity.id])
594 return first_opportunity.id
596 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
597 crm_stage = self.pool.get('crm.case.stage')
600 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
602 section_id = lead.section_id and lead.section_id.id or False
604 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
606 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
607 stage_id = stage_ids and stage_ids[0] or False
609 'planned_revenue': lead.planned_revenue,
610 'probability': lead.probability,
612 'partner_id': customer and customer.id or False,
613 'user_id': (lead.user_id and lead.user_id.id),
614 'type': 'opportunity',
615 'stage_id': stage_id or False,
616 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
617 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
618 'partner_address_id': contact_id,
621 def _convert_opportunity_notification(self, cr, uid, lead, context=None):
622 success_message = _("Lead is <b>converted to an opportunity</b>.")
623 lead.message_append_note(success_message ,success_message, need_action_user_id=lead.user_id.id)
626 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
627 partner = self.pool.get('res.partner')
628 mail_message = self.pool.get('mail.message')
631 customer = partner.browse(cr, uid, partner_id, context=context)
632 for lead in self.browse(cr, uid, ids, context=context):
633 if lead.state in ('done', 'cancel'):
635 if user_ids or section_id:
636 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
638 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
639 self.write(cr, uid, [lead.id], vals, context=context)
641 self._convert_opportunity_notification(cr, uid, lead, context=context)
642 #TOCHECK: why need to change partner details in all messages of lead ?
644 msg_ids = [ x.id for x in lead.message_ids]
645 mail_message.write(cr, uid, msg_ids, {
646 'partner_id': lead.partner_id.id
650 def _lead_create_partner(self, cr, uid, lead, context=None):
651 partner = self.pool.get('res.partner')
652 partner_id = partner.create(cr, uid, {
653 'name': lead.partner_name or lead.contact_name or lead.name,
654 'user_id': lead.user_id.id,
655 'comment': lead.description,
656 'section_id': lead.section_id.id or False,
661 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
663 res_partner = self.pool.get('res.partner')
665 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
666 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
667 res = lead.write({'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
671 def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
672 address = self.pool.get('res.partner.address')
673 return address.create(cr, uid, {
674 'partner_id': partner_id,
675 'name': lead.contact_name,
677 'mobile': lead.mobile,
678 'email': lead.email_from and to_email(lead.email_from)[0],
680 'title': lead.title and lead.title.id or False,
681 'function': lead.function,
682 'street': lead.street,
683 'street2': lead.street2,
686 'country_id': lead.country_id and lead.country_id.id or False,
687 'state_id': lead.state_id and lead.state_id.id or False,
690 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
692 This function convert partner based on action.
693 if action is 'create', create new partner with contact and assign lead to new partner_id.
694 otherwise assign lead to specified partner_id
699 for lead in self.browse(cr, uid, ids, context=context):
700 if action == 'create':
702 partner_id = self._lead_create_partner(cr, uid, lead, context=context)
703 self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
704 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
705 partner_ids[lead.id] = partner_id
708 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
710 Send mail to salesman with updated Lead details.
711 @ lead: browse record of 'crm.lead' object.
713 #TOFIX: mail template should be used here instead of fix subject, body text.
714 message = self.pool.get('mail.message')
715 email_to = lead.user_id and lead.user_id.user_email
719 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
720 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
721 subject = "lead %s converted into opportunity" % lead.name
722 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
723 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
726 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
731 value['section_id'] = team_id
732 if index < len(user_ids):
733 value['user_id'] = user_ids[index]
736 self.write(cr, uid, [lead_id], value, context=context)
739 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):
741 action :('schedule','Schedule a call'), ('log','Log a call')
743 phonecall = self.pool.get('crm.phonecall')
744 model_data = self.pool.get('ir.model.data')
747 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
749 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
750 for lead in self.browse(cr, uid, ids, context=context):
752 section_id = lead.section_id and lead.section_id.id or False
754 user_id = lead.user_id and lead.user_id.id or False
756 'name' : call_summary,
757 'opportunity_id' : lead.id,
758 'user_id' : user_id or False,
759 'categ_id' : categ_id or False,
760 'description' : desc or '',
761 'date' : schedule_time,
762 'section_id' : section_id or False,
763 'partner_id': lead.partner_id and lead.partner_id.id or False,
764 'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
765 'partner_phone' : phone or lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
766 'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
767 'priority': lead.priority,
770 new_id = phonecall.create(cr, uid, vals, context=context)
771 phonecall.case_open(cr, uid, [new_id])
773 phonecall.case_close(cr, uid, [new_id])
774 phonecall_dict[lead.id] = new_id
775 self._case_phonecall_notification(cr, uid, [new_id], lead, phonecall, action, context=context)
776 return phonecall_dict
779 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
780 models_data = self.pool.get('ir.model.data')
782 # Get Opportunity views
783 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
784 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
786 'name': _('Opportunity'),
788 'view_mode': 'tree, form',
789 'res_model': 'crm.lead',
790 'domain': [('type', '=', 'opportunity')],
791 'res_id': int(opportunity_id),
793 'views': [(form_view and form_view[1] or False, 'form'),
794 (tree_view and tree_view[1] or False, 'tree'),
795 (False, 'calendar'), (False, 'graph')],
796 'type': 'ir.actions.act_window',
800 def message_new(self, cr, uid, msg, custom_values=None, context=None):
801 """Automatically calls when new email message arrives"""
802 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
803 subject = msg.get('subject') or _("No Subject")
804 body = msg.get('body_text')
806 msg_from = msg.get('from')
807 priority = msg.get('priority')
810 'email_from': msg_from,
811 'email_cc': msg.get('cc'),
816 vals['priority'] = priority
817 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
818 self.write(cr, uid, [res_id], vals, context)
821 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
822 if isinstance(ids, (str, int, long)):
826 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
828 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
829 vals['priority'] = msg.get('priority')
831 'cost':'planned_cost',
832 'revenue': 'planned_revenue',
833 'probability':'probability'
836 for line in msg['body_text'].split('\n'):
838 res = tools.misc.command_re.match(line)
839 if res and maps.get(res.group(1).lower()):
840 key = maps.get(res.group(1).lower())
841 vls[key] = res.group(2).lower()
844 # Unfortunately the API is based on lists
845 # but we want to update the state based on the
846 # previous state, so we have to loop:
847 for case in self.browse(cr, uid, ids, context=context):
849 if case.state in CRM_LEAD_PENDING_STATES:
851 values.update(state=crm.AVAILABLE_STATES[1][0])
852 if not case.date_open:
853 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
854 res = self.write(cr, uid, [case.id], values, context=context)
857 def action_makeMeeting(self, cr, uid, ids, context=None):
859 This opens Meeting's calendar view to schedule meeting on current Opportunity
860 @return : Dictionary value for created Meeting view
865 data_obj = self.pool.get('ir.model.data')
866 for opp in self.browse(cr, uid, ids, context=context):
868 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
869 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
870 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
871 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
873 'default_opportunity_id': opp.id,
874 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
875 'default_user_id': uid,
876 'default_section_id': opp.section_id and opp.section_id.id or False,
877 'default_email_from': opp.email_from,
878 'default_state': 'open',
879 'default_name': opp.name
882 'name': _('Meetings'),
885 'view_mode': 'calendar,form,tree',
886 'res_model': 'crm.meeting',
888 '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')],
889 'type': 'ir.actions.act_window',
890 'search_view_id': search_view and search_view[1] or False,
896 def unlink(self, cr, uid, ids, context=None):
897 for lead in self.browse(cr, uid, ids, context):
898 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
899 raise osv.except_osv(_('Error'),
900 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
901 "You should better cancel it, instead of deleting it.") % lead.name)
902 return super(crm_lead, self).unlink(cr, uid, ids, context)
905 def write(self, cr, uid, ids, vals, context=None):
909 if 'date_closed' in vals:
910 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
912 if vals.get('stage_id'):
913 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
914 # change probability of lead(s) if required by stage
915 if not vals.get('probability') and stage.on_change:
916 vals['probability'] = stage.probability
917 for case in self.browse(cr, uid, ids, context=context):
918 if case.type == 'lead' or context.get('stage_type') == 'lead':
919 message = _("The stage of lead has been changed to <b>%s</b>.") % (stage.name)
920 case.message_append_note('', message)
921 elif case.type == 'opportunity':
922 message = _("The stage of opportunity has been changed to <b>%s</b>.") % (stage.name)
923 case.message_append_note('', message)
925 return super(crm_lead,self).write(cr, uid, ids, vals, context)
929 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: