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, context=None):
46 context = context or {}
47 stage_obj = self.pool.get('crm.case.stage')
48 stage_ids = stage_obj.search(cr, uid, ['|', ('id','in',ids), ('case_default','=',1)], context=context)
49 return stage_obj.name_get(cr, uid, stage_ids, context=context)
52 'stage_id': _read_group_stage_ids
55 # overridden because res.partner.address has an inconvenient name_get,
56 # especially if base_contact is installed.
57 def name_get(self, cr, user, ids, context=None):
58 if isinstance(ids, (int, long)):
60 return [(r['id'], tools.ustr(r[self._rec_name]))
61 for r in self.read(cr, user, ids, [self._rec_name], context)]
63 def _compute_day(self, cr, uid, ids, fields, args, context=None):
65 @param cr: the current row, from the database cursor,
66 @param uid: the current user’s ID for security checks,
67 @param ids: List of Openday’s IDs
68 @return: difference between current date and log date
69 @param context: A standard dictionary for contextual values
71 cal_obj = self.pool.get('resource.calendar')
72 res_obj = self.pool.get('resource.resource')
75 for lead in self.browse(cr, uid, ids, context=context):
80 if field == 'day_open':
82 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
83 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
84 ans = date_open - date_create
85 date_until = lead.date_open
86 elif field == 'day_close':
88 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
89 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
90 date_until = lead.date_closed
91 ans = date_close - date_create
95 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
97 resource_id = resource_ids[0]
99 duration = float(ans.days)
100 if lead.section_id and lead.section_id.resource_calendar_id:
101 duration = float(ans.days) * 24
102 new_dates = cal_obj.interval_get(cr,
104 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
105 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
110 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
111 for in_time, out_time in new_dates:
112 if in_time.date not in no_days:
113 no_days.append(in_time.date)
114 if out_time > date_until:
116 duration = len(no_days)
117 res[lead.id][field] = abs(int(duration))
120 def _history_search(self, cr, uid, obj, name, args, context=None):
122 msg_obj = self.pool.get('mail.message')
123 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
124 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
127 return [('id', 'in', lead_ids)]
129 return [('id', '=', '0')]
131 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
133 for obj in self.browse(cr, uid, ids, context=context):
135 for msg in obj.message_ids:
137 res[obj.id] = msg.subject
142 # Overridden from res.partner.address:
143 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
144 select=True, help="Optional linked partner, usually after conversion of the lead"),
146 'id': fields.integer('ID', readonly=True),
147 'name': fields.char('Name', size=64, select=1),
148 'active': fields.boolean('Active', required=False),
149 'date_action_last': fields.datetime('Last Action', readonly=1),
150 'date_action_next': fields.datetime('Next Action', readonly=1),
151 'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
152 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
153 select=True, help='When sending mails, the default email address is taken from the sales team.'),
154 'create_date': fields.datetime('Creation Date' , readonly=True),
155 '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"),
156 'description': fields.text('Notes'),
157 'write_date': fields.datetime('Update Date' , readonly=True),
159 'categ_id': fields.many2one('crm.case.categ', 'Category', \
160 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
161 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
162 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
163 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
164 'contact_name': fields.char('Contact Name', size=64),
165 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner that will be created while converting the into opportunity', select=1),
166 'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
167 'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
168 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
169 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
170 'date_closed': fields.datetime('Closed', readonly=True),
171 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
172 'user_id': fields.many2one('res.users', 'Salesman'),
173 'referred': fields.char('Referred By', size=64),
174 'date_open': fields.datetime('Opened', readonly=True),
175 'day_open': fields.function(_compute_day, string='Days to Open', \
176 multi='day_open', type="float", store=True),
177 'day_close': fields.function(_compute_day, string='Days to Close', \
178 multi='day_close', type="float", store=True),
179 'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
180 help='The state is set to \'Draft\', when a case is created.\
181 \nIf the case is in progress the state is set to \'Open\'.\
182 \nWhen the case is over, the state is set to \'Done\'.\
183 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
184 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
185 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
188 # Only used for type opportunity
189 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"),
190 'probability': fields.float('Probability (%)',group_operator="avg"),
191 'planned_revenue': fields.float('Expected Revenue'),
192 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
193 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
194 'phone': fields.char("Phone", size=64),
195 'date_deadline': fields.date('Expected Closing'),
196 'date_action': fields.date('Next Action Date'),
197 'title_action': fields.char('Next Action', size=64),
198 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
199 'color': fields.integer('Color Index'),
200 'partner_address_name': fields.related('partner_address_id', 'name', type='char', string='Partner Contact Name', readonly=True),
201 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
202 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
203 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
208 'active': lambda *a: 1,
209 'user_id': crm_case._get_default_user,
210 'email_from': crm_case._get_default_email,
211 'state': lambda *a: 'draft',
212 'type': lambda *a: 'lead',
213 'section_id': crm_case._get_section,
214 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
215 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
219 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
220 """This function returns value of partner email based on Partner Address
223 return {'value': {'email_from': False, 'country_id': False}}
224 address = self.pool.get('res.partner.address').browse(cr, uid, add)
225 return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
227 def on_change_optin(self, cr, uid, ids, optin):
228 return {'value':{'optin':optin,'optout':False}}
230 def on_change_optout(self, cr, uid, ids, optout):
231 return {'value':{'optout':optout,'optin':False}}
233 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
236 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
237 if not stage.on_change:
239 return {'value':{'probability': stage.probability}}
241 def stage_find_percent(self, cr, uid, percent, section_id):
242 """ Return the first stage with a probability == percent
244 stage_pool = self.pool.get('crm.case.stage')
246 ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
248 ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
254 def stage_find_lost(self, cr, uid, section_id):
255 return self.stage_find_percent(cr, uid, 0.0, section_id)
257 def stage_find_won(self, cr, uid, section_id):
258 return self.stage_find_percent(cr, uid, 100.0, section_id)
260 def case_open(self, cr, uid, ids, *args):
261 for l in self.browse(cr, uid, ids):
262 # When coming from draft override date and stage otherwise just set state
263 if l.state == 'draft':
265 message = _("The lead '%s' has been opened.") % l.name
266 elif l.type == 'opportunity':
267 message = _("The opportunity '%s' has been opened.") % l.name
269 message = _("The case '%s' has been opened.") % l.name
270 self.log(cr, uid, l.id, message)
271 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
272 self.write(cr, uid, [l.id], value)
273 if l.type == 'opportunity' and not l.stage_id:
274 stage_id = self.stage_find(cr, uid, l.section_id.id or False, [('sequence','>',0)])
276 self.stage_set(cr, uid, [l.id], stage_id)
277 res = super(crm_lead, self).case_open(cr, uid, ids, *args)
280 def case_close(self, cr, uid, ids, *args):
281 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
282 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
283 for case in self.browse(cr, uid, ids):
284 if case.type == 'lead':
285 message = _("The lead '%s' has been closed.") % case.name
287 message = _("The case '%s' has been closed.") % case.name
288 self.log(cr, uid, case.id, message)
291 def case_cancel(self, cr, uid, ids, *args):
292 """Overrides cancel for crm_case for setting probability
294 res = super(crm_lead, self).case_cancel(cr, uid, ids, args)
295 self.write(cr, uid, ids, {'probability' : 0.0})
298 def case_reset(self, cr, uid, ids, *args):
299 """Overrides reset as draft in order to set the stage field as empty
301 res = super(crm_lead, self).case_reset(cr, uid, ids, *args)
302 self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
305 def case_mark_lost(self, cr, uid, ids, *args):
306 """Mark the case as lost: state = done and probability = 0%
308 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
309 self.write(cr, uid, ids, {'probability' : 0.0})
310 for l in self.browse(cr, uid, ids):
311 stage_id = self.stage_find_lost(cr, uid, l.section_id.id or False)
313 self.stage_set(cr, uid, [l.id], stage_id)
314 message = _("The opportunity '%s' has been marked as lost.") % l.name
315 self.log(cr, uid, l.id, message)
318 def case_mark_won(self, cr, uid, ids, *args):
319 """Mark the case as lost: state = done and probability = 0%
321 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
322 self.write(cr, uid, ids, {'probability' : 100.0})
323 for l in self.browse(cr, uid, ids):
324 stage_id = self.stage_find_won(cr, uid, l.section_id.id or False)
326 self.stage_set(cr, uid, [l.id], stage_id)
327 message = _("The opportunity '%s' has been been won.") % l.name
328 self.log(cr, uid, l.id, message)
331 def set_priority(self, cr, uid, ids, priority):
334 return self.write(cr, uid, ids, {'priority' : priority})
336 def set_high_priority(self, cr, uid, ids, *args):
337 """Set lead priority to high
339 return self.set_priority(cr, uid, ids, '1')
341 def set_normal_priority(self, cr, uid, ids, *args):
342 """Set lead priority to normal
344 return self.set_priority(cr, uid, ids, '3')
347 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
348 # prepare opportunity data into dictionary for merging
349 opportunities = self.browse(cr, uid, ids, context=context)
350 def _get_first_not_null(attr):
351 if hasattr(oldest, attr):
352 return getattr(oldest, attr)
353 for opportunity in opportunities:
354 if hasattr(opportunity, attr):
355 return getattr(opportunity, attr)
358 def _get_first_not_null_id(attr):
359 res = _get_first_not_null(attr)
360 return res and res.id or False
362 def _concat_all(attr):
363 return ', '.join([getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)])
366 for field_name in fields:
367 field_info = self._all_columns.get(field_name)
368 if field_info is None:
370 field = field_info.column
371 if field._type in ('many2many', 'one2many'):
373 elif field._type == 'many2one':
374 data[field_name] = _get_first_not_null_id(field_name) # !!
375 elif field._type == 'text':
376 data[field_name] = _concat_all(field_name) #not lost
378 data[field_name] = _get_first_not_null(field_name) #not lost
381 def _merge_find_oldest(self, cr, uid, ids, context=None):
384 #TOCHECK: where pass 'convert' in context ?
385 if context.get('convert'):
386 ids = list(set(ids) - set(context.get('lead_ids', False)) )
388 #search opportunities order by create date
389 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
390 oldest_id = opportunity_ids[0]
391 return self.browse(cr, uid, oldest_id, context=context)
393 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
396 body.append("%s\n" % (title))
397 for field_name in fields:
398 field_info = self._all_columns.get(field_name)
399 if field_info is None:
401 field = field_info.column
404 if field._type == 'selection':
405 if hasattr(field.selection, '__call__'):
406 key = field.selection(self, cr, uid, context=context)
408 key = field.selection
409 value = dict(key).get(lead[field_name], lead[field_name])
410 elif field._type == 'many2one':
412 value = lead[field_name].name_get()[0][1]
414 value = lead[field_name]
416 body.append("%s: %s\n" % (field.string, value or ''))
417 return "\n".join(body + ['---'])
419 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
420 #TOFIX: mail template should be used instead of fix body, subject text
422 merge_message = _('Merged opportunities')
423 subject = [merge_message]
424 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
425 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
426 'country_id', 'city', 'street', 'street2', 'zip']
427 for opportunity in opportunities:
428 subject.append(opportunity.name)
429 title = "%s : %s" % (merge_message, opportunity.name)
430 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
432 subject = subject[0] + ", ".join(subject[1:])
433 details = "\n\n".join(details)
434 return self.message_append(cr, uid, [opportunity_id], subject, body_text=details, context=context)
436 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
437 message = self.pool.get('mail.message')
438 for opportunity in opportunities:
439 for history in opportunity.message_ids:
440 message.write(cr, uid, history.id, {
441 'res_id': opportunity_id,
442 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
447 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
448 attachment = self.pool.get('ir.attachment')
450 # return attachments of opportunity
451 def _get_attachments(opportunity_id):
452 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
453 return attachment.browse(cr, uid, attachment_ids, context=context)
456 first_attachments = _get_attachments(opportunity_id)
457 for opportunity in opportunities:
458 attachments = _get_attachments(opportunity.id)
459 for first in first_attachments:
460 for attachment in attachments:
461 if attachment.name == first.name:
463 name = "%s (%s)" % (attachment.name, count,),
464 res_id = opportunity_id,
466 attachment.write(values)
471 def merge_opportunity(self, cr, uid, ids, context=None):
473 To merge opportunities
474 :param ids: list of opportunities ids to merge
476 if context is None: context = {}
478 #TOCHECK: where pass lead_ids in context?
479 lead_ids = context and context.get('lead_ids', []) or []
482 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunities.'))
484 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
485 opportunities = self.browse(cr, uid, ids, context=context)
486 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
487 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
488 if ctx_opportunities :
489 first_opportunity = ctx_opportunities[0]
490 tail_opportunities = opportunities_list
492 first_opportunity = opportunities_list[0]
493 tail_opportunities = opportunities_list[1:]
495 fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id',
496 'partner_address_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
497 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
498 'date_action_next', 'email_from', 'email_cc', 'partner_name']
500 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
502 # merge data into first opportunity
503 self.write(cr, uid, [first_opportunity.id], data, context=context)
505 #copy message and attachements into the first opportunity
506 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
507 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
509 #Notification about loss of information
510 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
511 #delete tail opportunities
512 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
514 #open first opportunity
515 self.case_open(cr, uid, [first_opportunity.id])
516 return first_opportunity.id
518 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
519 crm_stage = self.pool.get('crm.case.stage')
520 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
522 section_id = lead.section_id and lead.section_id.id or False
524 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
526 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
527 stage_id = stage_ids and stage_ids[0] or False
529 'planned_revenue': lead.planned_revenue,
530 'probability': lead.probability,
532 'partner_id': customer.id,
533 'user_id': (lead.user_id and lead.user_id.id),
534 'type': 'opportunity',
535 'stage_id': stage_id or False,
536 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
537 'partner_address_id': contact_id
539 def _convert_opportunity_notification(self, cr, uid, lead, context=None):
540 success_message = _("Lead '%s' has been converted to an opportunity.") % lead.name
541 self.message_append(cr, uid, [lead.id], success_message, body_text=success_message, context=context)
542 self.log(cr, uid, lead.id, success_message)
543 self._send_mail_to_salesman(cr, uid, lead, context=context)
546 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
547 partner = self.pool.get('res.partner')
548 mail_message = self.pool.get('mail.message')
550 customer = partner.browse(cr, uid, partner_id, context=context)
552 for lead in self.browse(cr, uid, ids, context=context):
553 if lead.state in ('done', 'cancel'):
555 if user_ids or section_id:
556 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
558 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
559 self.write(cr, uid, [lead.id], vals, context=context)
561 self._convert_opportunity_notification(cr, uid, lead, context=context)
562 #TOCHECK: why need to change partner details in all messages of lead ?
564 msg_ids = [ x.id for x in lead.message_ids]
565 mail_message.write(cr, uid, msg_ids, {
566 'partner_id': lead.partner_id.id
570 def _lead_create_partner(self, cr, uid, lead, context=None):
571 partner = self.pool.get('res.partner')
572 partner_id = partner.create(cr, uid, {
573 'name': lead.partner_name or lead.contact_name or lead.name,
574 'user_id': lead.user_id.id,
575 'comment': lead.description,
580 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
582 res_partner = self.pool.get('res.partner')
584 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
585 res = lead.write({'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
589 def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
590 address = self.pool.get('res.partner.address')
591 return address.create(cr, uid, {
592 'partner_id': partner_id,
593 'name': lead.contact_name,
595 'mobile': lead.mobile,
596 'email': lead.email_from and to_email(lead.email_from)[0],
598 'title': lead.title and lead.title.id or False,
599 'function': lead.function,
600 'street': lead.street,
601 'street2': lead.street2,
604 'country_id': lead.country_id and lead.country_id.id or False,
605 'state_id': lead.state_id and lead.state_id.id or False,
608 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
610 This function convert partner based on action.
611 if action is 'create', create new partner with contact and assign lead to new partner_id.
612 otherwise assign lead to specified partner_id
617 for lead in self.browse(cr, uid, ids, context=context):
618 if action == 'create':
620 partner_id = self._lead_create_partner(cr, uid, lead, context=context)
621 self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
622 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
623 partner_ids[lead.id] = partner_id
626 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
628 Send mail to salesman with updated Lead details.
629 @ lead: browse record of 'crm.lead' object.
631 #TOFIX: mail template should be used here instead of fix subject, body text.
632 message = self.pool.get('mail.message')
633 email_to = lead.user_id and lead.user_id.user_email
637 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
638 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
639 subject = "lead %s converted into opportunity" % lead.name
640 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
641 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
644 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
649 value['section_id'] = team_id
650 if index < len(user_ids):
651 value['user_id'] = user_ids[index]
654 self.write(cr, uid, [lead_id], value, context=context)
657 def schedule_phonecall(self, cr, uid, ids, schedule_time, call_summary, user_id=False, section_id=False, categ_id=False, action='schedule', context=None):
659 action :('schedule','Schedule a call'), ('log','Log a call')
661 phonecall = self.pool.get('crm.phonecall')
662 model_data = self.pool.get('ir.model.data')
665 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
667 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
668 for lead in self.browse(cr, uid, ids, context=context):
670 section_id = lead.section_id and lead.section_id.id or False
672 user_id = lead.user_id and lead.user_id.id or False
674 'name' : call_summary,
675 'opportunity_id' : lead.id,
676 'user_id' : user_id or False,
677 'categ_id' : categ_id or False,
678 'description' : lead.description or False,
679 'date' : schedule_time,
680 'section_id' : section_id or False,
681 'partner_id': lead.partner_id and lead.partner_id.id or False,
682 'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
683 'partner_phone' : lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
684 'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
685 'priority': lead.priority,
688 new_id = phonecall.create(cr, uid, vals, context=context)
689 phonecall.case_open(cr, uid, [new_id])
691 phonecall.case_close(cr, uid, [new_id])
692 phonecall_dict[lead.id] = new_id
693 return phonecall_dict
696 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
697 models_data = self.pool.get('ir.model.data')
699 # Get Opportunity views
700 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
701 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
703 'name': _('Opportunity'),
705 'view_mode': 'tree, form',
706 'res_model': 'crm.lead',
707 'domain': [('type', '=', 'opportunity')],
708 'res_id': int(opportunity_id),
710 'views': [(form_view and form_view[1] or False, 'form'),
711 (tree_view and tree_view[1] or False, 'tree'),
712 (False, 'calendar'), (False, 'graph')],
713 'type': 'ir.actions.act_window',
717 def message_new(self, cr, uid, msg, custom_values=None, context=None):
718 """Automatically calls when new email message arrives"""
719 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
720 subject = msg.get('subject') or _("No Subject")
721 body = msg.get('body_text')
723 msg_from = msg.get('from')
724 priority = msg.get('priority')
727 'email_from': msg_from,
728 'email_cc': msg.get('cc'),
733 vals['priority'] = priority
734 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
735 self.write(cr, uid, [res_id], vals, context)
738 def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
739 if isinstance(ids, (str, int, long)):
742 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
744 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
745 vals['priority'] = msg.get('priority')
747 'cost':'planned_cost',
748 'revenue': 'planned_revenue',
749 'probability':'probability'
752 for line in msg['body_text'].split('\n'):
754 res = tools.misc.command_re.match(line)
755 if res and maps.get(res.group(1).lower()):
756 key = maps.get(res.group(1).lower())
757 vls[key] = res.group(2).lower()
760 # Unfortunately the API is based on lists
761 # but we want to update the state based on the
762 # previous state, so we have to loop:
763 for case in self.browse(cr, uid, ids, context=context):
765 if case.state in CRM_LEAD_PENDING_STATES:
766 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
767 res = self.write(cr, uid, [case.id], values, context=context)
770 def action_makeMeeting(self, cr, uid, ids, context=None):
772 This opens Meeting's calendar view to schedule meeting on current Opportunity
773 @return : Dictionary value for created Meeting view
776 for opp in self.browse(cr, uid, ids, context=context):
777 data_obj = self.pool.get('ir.model.data')
780 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
781 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
782 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
783 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
786 'default_opportunity_id': opp.id,
787 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
788 'default_user_id': uid,
789 'default_section_id': opp.section_id and opp.section_id.id or False,
790 'default_email_from': opp.email_from,
791 'default_state': 'open',
792 'default_name': opp.name
795 'name': _('Meetings'),
798 'view_mode': 'calendar,form,tree',
799 'res_model': 'crm.meeting',
801 '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')],
802 'type': 'ir.actions.act_window',
803 'search_view_id': search_view and search_view[1] or False,
809 def unlink(self, cr, uid, ids, context=None):
810 for lead in self.browse(cr, uid, ids, context):
811 if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
812 raise osv.except_osv(_('Warning !'),
813 _('You can not delete this lead. You should better cancel it.'))
814 return super(crm_lead, self).unlink(cr, uid, ids, context)
817 def write(self, cr, uid, ids, vals, context=None):
821 if 'date_closed' in vals:
822 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
824 if 'stage_id' in vals and vals['stage_id']:
825 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
826 text = _("Changed Stage to: %s") % stage_obj.name
827 self.message_append(cr, uid, ids, text, body_text=text, context=context)
829 for case in self.browse(cr, uid, ids, context=context):
830 if case.type == 'lead' or context.get('stage_type',False)=='lead':
831 message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
832 elif case.type == 'opportunity':
833 message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
834 self.log(cr, uid, case.id, message)
835 return super(crm_lead,self).write(cr, uid, ids, vals, context)
839 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: