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_create_notification(self, cr, uid, ids, context=None):
275 for obj in self.browse(cr, uid, ids, context=context):
276 self.message_subscribe(cr, uid, ids, [obj.user_id.id], context=context)
277 if obj.type=="opportunity" and obj.state=="draft":
278 message = _("Opportunity is <b>created</b>.")
279 elif obj.type=="lead" :
280 message = _("Lead is <b>created</b>.")
282 message = _("The case has been <b>created</b>.")
283 self.message_append_note(cr, uid, ids, _('System notification'),
284 message, type='notification', need_action_user_id=obj.user_id.id, context=context)
287 def _case_open_notification(self, lead, context=None):
288 if lead.state != 'draft' and lead.state != 'pending':
290 if lead.type == 'lead':
291 message = _("The lead has been <b>opened</b>.")
292 elif lead.type == 'opportunity':
293 message = _("The opportunity has been <b>opened</b>.")
295 message = _("The case has been <b>opened</b>.")
296 lead.message_append_note('' ,message, need_action_user_id=lead.user_id.id)
298 def _case_close_notification(self, lead, context=None):
299 lead[0].message_mark_done(context)
300 if lead[0].type == 'lead':
301 message = _("The lead has been <b>closed</b>.")
302 elif lead[0].type == 'opportunity':
303 message = _("The opportunity has been <b>closed</b>.")
305 message = _("The case has been <b>closed</b>.")
306 lead[0].message_append_note('' ,message)
308 def _case_mark_lost_notification(self, lead, context=None):
309 lead.message_mark_done(context)
310 message = _("The opportunity has been <b>marked as lost</b>.")
311 lead.message_append_note('' ,message)
313 def _case_mark_won_notification(self, lead, context=None):
314 lead.message_mark_done(context)
315 message = _("The opportunity has been <b>won</b>.")
316 lead.message_append_note('' ,message)
318 def _case_cancel_notification(self, lead, context=None):
319 lead[0].message_mark_done(context)
320 if lead[0].type == 'lead':
321 message = _("The lead has been <b>cancelled</b>.")
322 elif lead[0].type == 'opportunity':
323 message = _("The opportunity has been <b>cancelled</b>.")
324 lead[0].message_append_note('' ,message)
326 def _case_pending_notification(self, case, context=None):
327 if case[0].type == 'lead':
328 message = _("The lead is <b>pending</b>.")
329 elif case[0].type == 'opportunity':
330 message = _("The opportunity is <b>pending</b>.")
331 case[0].message_append_note('' ,message)
333 def _case_escalate_notification(self, case, context=None):
334 message = _("The lead is <b>escalated</b>.")
335 case.message_append_note('' ,message)
337 def _case_phonecall_notification(self, cr, uid, ids, case, phonecall, action, context=None):
338 for obj in phonecall.browse(cr, uid, ids, context=context):
339 if action == "schedule" :
340 message = _("<b>%s a call</b> for the %s.") % (action, obj.date)
342 message = _("<b>%s a call</b>.") % (action)
343 case.message_append_note('', message)
344 if action == "schedule" :
345 phonecall.message_append_note(cr, uid, ids, '', message, need_action_user_id=obj.user_id.id)
347 def case_open(self, cr, uid, ids, context=None):
348 res = super(crm_lead, self).case_open(cr, uid, ids, context)
349 for lead in self.browse(cr, uid, ids, context=context):
350 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
351 self.write(cr, uid, [lead.id], value)
352 if lead.type == 'opportunity' and not lead.stage_id:
353 stage_id = self.stage_find(cr, uid, lead.section_id.id or False, [('sequence','>',0)])
355 self.stage_set(cr, uid, [lead.id], stage_id)
358 def case_close(self, cr, uid, ids, context=None):
359 res = super(crm_lead, self).case_close(cr, uid, ids, context)
360 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
363 def case_cancel(self, cr, uid, ids, context=None):
364 """Overrides cancel for crm_case for setting probability
366 res = super(crm_lead, self).case_cancel(cr, uid, ids, context)
367 self.write(cr, uid, ids, {'probability' : 0.0})
370 def case_reset(self, cr, uid, ids, context=None):
371 """Overrides reset as draft in order to set the stage field as empty
373 res = super(crm_lead, self).case_reset(cr, uid, ids, context)
374 self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
377 def case_mark_lost(self, cr, uid, ids, context=None):
378 """Mark the case as lost: state = done and probability = 0%
380 res = super(crm_lead, self).case_close(cr, uid, ids, context)
381 self.write(cr, uid, ids, {'probability' : 0.0})
382 for lead in self.browse(cr, uid, ids):
383 stage_id = self.stage_find_lost(cr, uid, lead.section_id.id or False)
385 self.stage_set(cr, uid, [lead.id], stage_id)
388 def case_mark_won(self, cr, uid, ids, context=None):
389 """Mark the case as lost: state = done and probability = 0%
391 res = super(crm_lead, self).case_close(cr, uid, ids, context=None)
392 self.write(cr, uid, ids, {'probability' : 100.0})
393 for lead in self.browse(cr, uid, ids):
394 stage_id = self.stage_find_won(cr, uid, lead.section_id.id or False)
396 self.stage_set(cr, uid, [lead.id], stage_id)
397 self._case_mark_won_notification(lead, context=context)
400 def set_priority(self, cr, uid, ids, priority):
403 return self.write(cr, uid, ids, {'priority' : priority})
405 def set_high_priority(self, cr, uid, ids, context=None):
406 """Set lead priority to high
408 return self.set_priority(cr, uid, ids, '1')
410 def set_normal_priority(self, cr, uid, ids, context=None):
411 """Set lead priority to normal
413 return self.set_priority(cr, uid, ids, '3')
416 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
417 # prepare opportunity data into dictionary for merging
418 opportunities = self.browse(cr, uid, ids, context=context)
419 def _get_first_not_null(attr):
420 if hasattr(oldest, attr):
421 return getattr(oldest, attr)
422 for opportunity in opportunities:
423 if hasattr(opportunity, attr):
424 return getattr(opportunity, attr)
427 def _get_first_not_null_id(attr):
428 res = _get_first_not_null(attr)
429 return res and res.id or False
431 def _concat_all(attr):
432 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
435 for field_name in fields:
436 field_info = self._all_columns.get(field_name)
437 if field_info is None:
439 field = field_info.column
440 if field._type in ('many2many', 'one2many'):
442 elif field._type == 'many2one':
443 data[field_name] = _get_first_not_null_id(field_name) # !!
444 elif field._type == 'text':
445 data[field_name] = _concat_all(field_name) #not lost
447 data[field_name] = _get_first_not_null(field_name) #not lost
450 def _merge_find_oldest(self, cr, uid, ids, context=None):
453 #TOCHECK: where pass 'convert' in context ?
454 if context.get('convert'):
455 ids = list(set(ids) - set(context.get('lead_ids', False)) )
457 #search opportunities order by create date
458 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
459 oldest_id = opportunity_ids[0]
460 return self.browse(cr, uid, oldest_id, context=context)
462 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
465 body.append("%s\n" % (title))
466 for field_name in fields:
467 field_info = self._all_columns.get(field_name)
468 if field_info is None:
470 field = field_info.column
473 if field._type == 'selection':
474 if hasattr(field.selection, '__call__'):
475 key = field.selection(self, cr, uid, context=context)
477 key = field.selection
478 value = dict(key).get(lead[field_name], lead[field_name])
479 elif field._type == 'many2one':
481 value = lead[field_name].name_get()[0][1]
483 value = lead[field_name]
485 body.append("%s: %s" % (field.string, value or ''))
486 return "\n".join(body + ['---'])
488 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
489 #TOFIX: mail template should be used instead of fix body, subject text
491 merge_message = _('Merged opportunities')
492 subject = [merge_message]
493 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
494 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
495 'country_id', 'city', 'street', 'street2', 'zip']
496 for opportunity in opportunities:
497 subject.append(opportunity.name)
498 title = "%s : %s" % (merge_message, opportunity.name)
499 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
501 subject = subject[0] + ", ".join(subject[1:])
502 details = "\n\n".join(details)
503 return opportunity.message_append_note(subject, body=details, need_action_user_id=opportunity.user_id.id)
505 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
506 message = self.pool.get('mail.message')
507 for opportunity in opportunities:
508 for history in opportunity.message_ids:
509 message.write(cr, uid, history.id, {
510 'res_id': opportunity_id,
511 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
516 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
517 attachment = self.pool.get('ir.attachment')
519 # return attachments of opportunity
520 def _get_attachments(opportunity_id):
521 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
522 return attachment.browse(cr, uid, attachment_ids, context=context)
525 first_attachments = _get_attachments(opportunity_id)
526 for opportunity in opportunities:
527 attachments = _get_attachments(opportunity.id)
528 for first in first_attachments:
529 for attachment in attachments:
530 if attachment.name == first.name:
532 name = "%s (%s)" % (attachment.name, count,),
533 res_id = opportunity_id,
535 attachment.write(values)
540 def merge_opportunity(self, cr, uid, ids, context=None):
542 To merge opportunities
543 :param ids: list of opportunities ids to merge
545 if context is None: context = {}
547 #TOCHECK: where pass lead_ids in context?
548 lead_ids = context and context.get('lead_ids', []) or []
551 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
553 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
554 opportunities = self.browse(cr, uid, ids, context=context)
555 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
556 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
557 if ctx_opportunities :
558 first_opportunity = ctx_opportunities[0]
559 tail_opportunities = opportunities_list
561 first_opportunity = opportunities_list[0]
562 tail_opportunities = opportunities_list[1:]
564 fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id',
565 'partner_address_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
566 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
567 'date_action_next', 'email_from', 'email_cc', 'partner_name']
569 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
571 # merge data into first opportunity
572 self.write(cr, uid, [first_opportunity.id], data, context=context)
574 #copy message and attachements into the first opportunity
575 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
576 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
578 #Notification about loss of information
579 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
580 #delete tail opportunities
581 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
583 #open first opportunity
584 self.case_open(cr, uid, [first_opportunity.id])
585 return first_opportunity.id
587 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
588 crm_stage = self.pool.get('crm.case.stage')
591 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
593 section_id = lead.section_id and lead.section_id.id or False
595 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
597 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
598 stage_id = stage_ids and stage_ids[0] or False
600 'planned_revenue': lead.planned_revenue,
601 'probability': lead.probability,
603 'partner_id': customer and customer.id or False,
604 'user_id': (lead.user_id and lead.user_id.id),
605 'type': 'opportunity',
606 'stage_id': stage_id or False,
607 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
608 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
609 'partner_address_id': contact_id,
612 def _convert_opportunity_notification(self, cr, uid, lead, context=None):
613 success_message = _("Lead is <b>converted to an opportunity</b>.")
614 lead.message_append_note(success_message ,success_message, need_action_user_id=lead.user_id.id)
617 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
618 partner = self.pool.get('res.partner')
619 mail_message = self.pool.get('mail.message')
622 customer = partner.browse(cr, uid, partner_id, context=context)
623 for lead in self.browse(cr, uid, ids, context=context):
624 if lead.state in ('done', 'cancel'):
626 if user_ids or section_id:
627 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
629 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
630 self.write(cr, uid, [lead.id], vals, context=context)
632 self._convert_opportunity_notification(cr, uid, lead, context=context)
633 #TOCHECK: why need to change partner details in all messages of lead ?
635 msg_ids = [ x.id for x in lead.message_ids]
636 mail_message.write(cr, uid, msg_ids, {
637 'partner_id': lead.partner_id.id
641 def _lead_create_partner(self, cr, uid, lead, context=None):
642 partner = self.pool.get('res.partner')
643 partner_id = partner.create(cr, uid, {
644 'name': lead.partner_name or lead.contact_name or lead.name,
645 'user_id': lead.user_id.id,
646 'comment': lead.description,
647 'section_id': lead.section_id.id or False,
652 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
654 res_partner = self.pool.get('res.partner')
656 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
657 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
658 res = lead.write({'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
662 def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
663 address = self.pool.get('res.partner.address')
664 return address.create(cr, uid, {
665 'partner_id': partner_id,
666 'name': lead.contact_name,
668 'mobile': lead.mobile,
669 'email': lead.email_from and to_email(lead.email_from)[0],
671 'title': lead.title and lead.title.id or False,
672 'function': lead.function,
673 'street': lead.street,
674 'street2': lead.street2,
677 'country_id': lead.country_id and lead.country_id.id or False,
678 'state_id': lead.state_id and lead.state_id.id or False,
681 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
683 This function convert partner based on action.
684 if action is 'create', create new partner with contact and assign lead to new partner_id.
685 otherwise assign lead to specified partner_id
690 for lead in self.browse(cr, uid, ids, context=context):
691 if action == 'create':
693 partner_id = self._lead_create_partner(cr, uid, lead, context=context)
694 self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
695 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
696 partner_ids[lead.id] = partner_id
699 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
701 Send mail to salesman with updated Lead details.
702 @ lead: browse record of 'crm.lead' object.
704 #TOFIX: mail template should be used here instead of fix subject, body text.
705 message = self.pool.get('mail.message')
706 email_to = lead.user_id and lead.user_id.user_email
710 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
711 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
712 subject = "lead %s converted into opportunity" % lead.name
713 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
714 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
717 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
722 value['section_id'] = team_id
723 if index < len(user_ids):
724 value['user_id'] = user_ids[index]
727 self.write(cr, uid, [lead_id], value, context=context)
730 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):
732 action :('schedule','Schedule a call'), ('log','Log a call')
734 phonecall = self.pool.get('crm.phonecall')
735 model_data = self.pool.get('ir.model.data')
738 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
740 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
741 for lead in self.browse(cr, uid, ids, context=context):
743 section_id = lead.section_id and lead.section_id.id or False
745 user_id = lead.user_id and lead.user_id.id or False
747 'name' : call_summary,
748 'opportunity_id' : lead.id,
749 'user_id' : user_id or False,
750 'categ_id' : categ_id or False,
751 'description' : desc or '',
752 'date' : schedule_time,
753 'section_id' : section_id or False,
754 'partner_id': lead.partner_id and lead.partner_id.id or False,
755 'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
756 'partner_phone' : phone or lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
757 'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
758 'priority': lead.priority,
761 new_id = phonecall.create(cr, uid, vals, context=context)
762 phonecall.case_open(cr, uid, [new_id])
764 phonecall.case_close(cr, uid, [new_id])
765 phonecall_dict[lead.id] = new_id
766 self._case_phonecall_notification(cr, uid, [new_id], lead, phonecall, action, context=context)
767 return phonecall_dict
770 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
771 models_data = self.pool.get('ir.model.data')
773 # Get Opportunity views
774 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
775 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
777 'name': _('Opportunity'),
779 'view_mode': 'tree, form',
780 'res_model': 'crm.lead',
781 'domain': [('type', '=', 'opportunity')],
782 'res_id': int(opportunity_id),
784 'views': [(form_view and form_view[1] or False, 'form'),
785 (tree_view and tree_view[1] or False, 'tree'),
786 (False, 'calendar'), (False, 'graph')],
787 'type': 'ir.actions.act_window',
791 def message_new(self, cr, uid, msg, custom_values=None, context=None):
792 """Automatically calls when new email message arrives"""
793 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
794 subject = msg.get('subject') or _("No Subject")
795 body = msg.get('body_text')
797 msg_from = msg.get('from')
798 priority = msg.get('priority')
801 'email_from': msg_from,
802 'email_cc': msg.get('cc'),
807 vals['priority'] = priority
808 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
809 self.write(cr, uid, [res_id], vals, context)
812 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
813 if isinstance(ids, (str, int, long)):
817 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
819 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
820 vals['priority'] = msg.get('priority')
822 'cost':'planned_cost',
823 'revenue': 'planned_revenue',
824 'probability':'probability'
827 for line in msg['body_text'].split('\n'):
829 res = tools.misc.command_re.match(line)
830 if res and maps.get(res.group(1).lower()):
831 key = maps.get(res.group(1).lower())
832 vls[key] = res.group(2).lower()
835 # Unfortunately the API is based on lists
836 # but we want to update the state based on the
837 # previous state, so we have to loop:
838 for case in self.browse(cr, uid, ids, context=context):
840 if case.state in CRM_LEAD_PENDING_STATES:
842 values.update(state=crm.AVAILABLE_STATES[1][0])
843 if not case.date_open:
844 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
845 res = self.write(cr, uid, [case.id], values, context=context)
848 def action_makeMeeting(self, cr, uid, ids, context=None):
850 This opens Meeting's calendar view to schedule meeting on current Opportunity
851 @return : Dictionary value for created Meeting view
856 data_obj = self.pool.get('ir.model.data')
857 for opp in self.browse(cr, uid, ids, context=context):
859 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
860 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
861 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
862 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
864 'default_opportunity_id': opp.id,
865 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
866 'default_user_id': uid,
867 'default_section_id': opp.section_id and opp.section_id.id or False,
868 'default_email_from': opp.email_from,
869 'default_state': 'open',
870 'default_name': opp.name
873 'name': _('Meetings'),
876 'view_mode': 'calendar,form,tree',
877 'res_model': 'crm.meeting',
879 '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')],
880 'type': 'ir.actions.act_window',
881 'search_view_id': search_view and search_view[1] or False,
887 def unlink(self, cr, uid, ids, context=None):
888 for lead in self.browse(cr, uid, ids, context):
889 if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
890 raise osv.except_osv(_('Error'),
891 _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
892 "You should better cancel it, instead of deleting it.") % lead.name)
893 return super(crm_lead, self).unlink(cr, uid, ids, context)
896 def write(self, cr, uid, ids, vals, context=None):
900 if 'date_closed' in vals:
901 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
903 if vals.get('stage_id'):
904 stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
905 # change probability of lead(s) if required by stage
906 if not vals.get('probability') and stage.on_change:
907 vals['probability'] = stage.probability
908 text = _("Changed Stage to: %s") % stage.name
910 for case in self.browse(cr, uid, ids, context=context):
911 if case.type == 'lead' or context.get('stage_type') == 'lead':
912 message = _("The stage of lead has been changed to <b>%s</b>.") % (stage.name)
913 case.message_append_note(text, message)
914 elif case.type == 'opportunity':
915 message = _("The stage of opportunity has been changed to <b>%s</b>.") % (stage.name)
916 case.message_append_note(text, message)
918 return super(crm_lead,self).write(cr, uid, ids, vals, context)
922 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: