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),
196 # Only used for type opportunity
197 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"),
198 'probability': fields.float('Probability (%)',group_operator="avg"),
199 'planned_revenue': fields.float('Expected Revenue'),
200 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
201 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
202 'phone': fields.char("Phone", size=64),
203 'date_deadline': fields.date('Expected Closing'),
204 'date_action': fields.date('Next Action Date', select=True),
205 'title_action': fields.char('Next Action', size=64),
206 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
207 'color': fields.integer('Color Index'),
208 'partner_address_name': fields.related('partner_address_id', 'name', type='char', string='Partner Contact Name', readonly=True),
209 'partner_address_email': fields.related('partner_address_id', 'email', type='char', string='Partner Contact Email', readonly=True),
210 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
211 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
212 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
217 'active': lambda *a: 1,
218 'user_id': crm_case._get_default_user,
219 'email_from': crm_case._get_default_email,
220 'state': lambda *a: 'draft',
221 'type': lambda *a: 'lead',
222 'section_id': crm_case._get_section,
223 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
224 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
228 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
229 """This function returns value of partner email based on Partner Address
232 return {'value': {'email_from': False, 'country_id': False}}
233 address = self.pool.get('res.partner.address').browse(cr, uid, add)
234 return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
236 def on_change_optin(self, cr, uid, ids, optin):
237 return {'value':{'optin':optin,'optout':False}}
239 def on_change_optout(self, cr, uid, ids, optout):
240 return {'value':{'optout':optout,'optin':False}}
242 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
245 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
246 if not stage.on_change:
248 return {'value':{'probability': stage.probability}}
250 def stage_find_percent(self, cr, uid, percent, section_id):
251 """ Return the first stage with a probability == percent
253 stage_pool = self.pool.get('crm.case.stage')
255 ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
257 ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
263 def stage_find_lost(self, cr, uid, section_id):
264 return self.stage_find_percent(cr, uid, 0.0, section_id)
266 def stage_find_won(self, cr, uid, section_id):
267 return self.stage_find_percent(cr, uid, 100.0, section_id)
269 def case_open(self, cr, uid, ids, *args):
270 for l in self.browse(cr, uid, ids):
271 # When coming from draft override date and stage otherwise just set state
272 if l.state == 'draft':
274 message = _("The lead '%s' has been opened.") % l.name
275 elif l.type == 'opportunity':
276 message = _("The opportunity '%s' has been opened.") % l.name
278 message = _("The case '%s' has been opened.") % l.name
279 self.log(cr, uid, l.id, message)
280 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
281 self.write(cr, uid, [l.id], value)
282 if l.type == 'opportunity' and not l.stage_id:
283 stage_id = self.stage_find(cr, uid, l.section_id.id or False, [('sequence','>',0)])
285 self.stage_set(cr, uid, [l.id], stage_id)
286 res = super(crm_lead, self).case_open(cr, uid, ids, *args)
289 def case_close(self, cr, uid, ids, *args):
290 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
291 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
292 for case in self.browse(cr, uid, ids):
293 if case.type == 'lead':
294 message = _("The lead '%s' has been closed.") % case.name
296 message = _("The case '%s' has been closed.") % case.name
297 self.log(cr, uid, case.id, message)
300 def case_cancel(self, cr, uid, ids, *args):
301 """Overrides cancel for crm_case for setting probability
303 res = super(crm_lead, self).case_cancel(cr, uid, ids, args)
304 self.write(cr, uid, ids, {'probability' : 0.0})
307 def case_reset(self, cr, uid, ids, *args):
308 """Overrides reset as draft in order to set the stage field as empty
310 res = super(crm_lead, self).case_reset(cr, uid, ids, *args)
311 self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
314 def case_mark_lost(self, cr, uid, ids, *args):
315 """Mark the case as lost: state = done and probability = 0%
317 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
318 self.write(cr, uid, ids, {'probability' : 0.0})
319 for l in self.browse(cr, uid, ids):
320 stage_id = self.stage_find_lost(cr, uid, l.section_id.id or False)
322 self.stage_set(cr, uid, [l.id], stage_id)
323 message = _("The opportunity '%s' has been marked as lost.") % l.name
324 self.log(cr, uid, l.id, message)
327 def case_mark_won(self, cr, uid, ids, *args):
328 """Mark the case as lost: state = done and probability = 0%
330 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
331 self.write(cr, uid, ids, {'probability' : 100.0})
332 for l in self.browse(cr, uid, ids):
333 stage_id = self.stage_find_won(cr, uid, l.section_id.id or False)
335 self.stage_set(cr, uid, [l.id], stage_id)
336 message = _("The opportunity '%s' has been been won.") % l.name
337 self.log(cr, uid, l.id, message)
340 def set_priority(self, cr, uid, ids, priority):
343 return self.write(cr, uid, ids, {'priority' : priority})
345 def set_high_priority(self, cr, uid, ids, *args):
346 """Set lead priority to high
348 return self.set_priority(cr, uid, ids, '1')
350 def set_normal_priority(self, cr, uid, ids, *args):
351 """Set lead priority to normal
353 return self.set_priority(cr, uid, ids, '3')
356 def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
357 # prepare opportunity data into dictionary for merging
358 opportunities = self.browse(cr, uid, ids, context=context)
359 def _get_first_not_null(attr):
360 if hasattr(oldest, attr):
361 return getattr(oldest, attr)
362 for opportunity in opportunities:
363 if hasattr(opportunity, attr):
364 return getattr(opportunity, attr)
367 def _get_first_not_null_id(attr):
368 res = _get_first_not_null(attr)
369 return res and res.id or False
371 def _concat_all(attr):
372 return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
375 for field_name in fields:
376 field_info = self._all_columns.get(field_name)
377 if field_info is None:
379 field = field_info.column
380 if field._type in ('many2many', 'one2many'):
382 elif field._type == 'many2one':
383 data[field_name] = _get_first_not_null_id(field_name) # !!
384 elif field._type == 'text':
385 data[field_name] = _concat_all(field_name) #not lost
387 data[field_name] = _get_first_not_null(field_name) #not lost
390 def _merge_find_oldest(self, cr, uid, ids, context=None):
393 #TOCHECK: where pass 'convert' in context ?
394 if context.get('convert'):
395 ids = list(set(ids) - set(context.get('lead_ids', False)) )
397 #search opportunities order by create date
398 opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
399 oldest_id = opportunity_ids[0]
400 return self.browse(cr, uid, oldest_id, context=context)
402 def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
405 body.append("%s\n" % (title))
406 for field_name in fields:
407 field_info = self._all_columns.get(field_name)
408 if field_info is None:
410 field = field_info.column
413 if field._type == 'selection':
414 if hasattr(field.selection, '__call__'):
415 key = field.selection(self, cr, uid, context=context)
417 key = field.selection
418 value = dict(key).get(lead[field_name], lead[field_name])
419 elif field._type == 'many2one':
421 value = lead[field_name].name_get()[0][1]
423 value = lead[field_name]
425 body.append("%s: %s" % (field.string, value or ''))
426 return "\n".join(body + ['---'])
428 def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
429 #TOFIX: mail template should be used instead of fix body, subject text
431 merge_message = _('Merged opportunities')
432 subject = [merge_message]
433 fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
434 'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
435 'country_id', 'city', 'street', 'street2', 'zip']
436 for opportunity in opportunities:
437 subject.append(opportunity.name)
438 title = "%s : %s" % (merge_message, opportunity.name)
439 details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
441 subject = subject[0] + ", ".join(subject[1:])
442 details = "\n\n".join(details)
443 return self.message_append(cr, uid, [opportunity_id], subject, body_text=details, context=context)
445 def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
446 message = self.pool.get('mail.message')
447 for opportunity in opportunities:
448 for history in opportunity.message_ids:
449 message.write(cr, uid, history.id, {
450 'res_id': opportunity_id,
451 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
456 def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
457 attachment = self.pool.get('ir.attachment')
459 # return attachments of opportunity
460 def _get_attachments(opportunity_id):
461 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
462 return attachment.browse(cr, uid, attachment_ids, context=context)
465 first_attachments = _get_attachments(opportunity_id)
466 for opportunity in opportunities:
467 attachments = _get_attachments(opportunity.id)
468 for first in first_attachments:
469 for attachment in attachments:
470 if attachment.name == first.name:
472 name = "%s (%s)" % (attachment.name, count,),
473 res_id = opportunity_id,
475 attachment.write(values)
480 def merge_opportunity(self, cr, uid, ids, context=None):
482 To merge opportunities
483 :param ids: list of opportunities ids to merge
485 if context is None: context = {}
487 #TOCHECK: where pass lead_ids in context?
488 lead_ids = context and context.get('lead_ids', []) or []
491 raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
493 ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
494 opportunities = self.browse(cr, uid, ids, context=context)
495 opportunities_list = list(set(opportunities) - set(ctx_opportunities))
496 oldest = self._merge_find_oldest(cr, uid, ids, context=context)
497 if ctx_opportunities :
498 first_opportunity = ctx_opportunities[0]
499 tail_opportunities = opportunities_list
501 first_opportunity = opportunities_list[0]
502 tail_opportunities = opportunities_list[1:]
504 fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id',
505 'partner_address_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
506 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
507 'date_action_next', 'email_from', 'email_cc', 'partner_name']
509 data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
511 # merge data into first opportunity
512 self.write(cr, uid, [first_opportunity.id], data, context=context)
514 #copy message and attachements into the first opportunity
515 self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
516 self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
518 #Notification about loss of information
519 self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
520 #delete tail opportunities
521 self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
523 #open first opportunity
524 self.case_open(cr, uid, [first_opportunity.id])
525 return first_opportunity.id
527 def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
528 crm_stage = self.pool.get('crm.case.stage')
531 contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
533 section_id = lead.section_id and lead.section_id.id or False
535 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
537 stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
538 stage_id = stage_ids and stage_ids[0] or False
540 'planned_revenue': lead.planned_revenue,
541 'probability': lead.probability,
543 'partner_id': customer and customer.id or False,
544 'user_id': (lead.user_id and lead.user_id.id),
545 'type': 'opportunity',
546 'stage_id': stage_id or False,
547 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
548 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
549 'partner_address_id': contact_id,
552 def _convert_opportunity_notification(self, cr, uid, lead, context=None):
553 success_message = _("Lead '%s' has been converted to an opportunity.") % lead.name
554 self.message_append(cr, uid, [lead.id], success_message, body_text=success_message, context=context)
555 self.log(cr, uid, lead.id, success_message)
556 self._send_mail_to_salesman(cr, uid, lead, context=context)
559 def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
560 partner = self.pool.get('res.partner')
561 mail_message = self.pool.get('mail.message')
564 customer = partner.browse(cr, uid, partner_id, context=context)
565 for lead in self.browse(cr, uid, ids, context=context):
566 if lead.state in ('done', 'cancel'):
568 if user_ids or section_id:
569 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
571 vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
572 self.write(cr, uid, [lead.id], vals, context=context)
574 self._convert_opportunity_notification(cr, uid, lead, context=context)
575 #TOCHECK: why need to change partner details in all messages of lead ?
577 msg_ids = [ x.id for x in lead.message_ids]
578 mail_message.write(cr, uid, msg_ids, {
579 'partner_id': lead.partner_id.id
583 def _lead_create_partner(self, cr, uid, lead, context=None):
584 partner = self.pool.get('res.partner')
585 partner_id = partner.create(cr, uid, {
586 'name': lead.partner_name or lead.contact_name or lead.name,
587 'user_id': lead.user_id.id,
588 'comment': lead.description,
589 'section_id': lead.section_id.id or False,
594 def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
596 res_partner = self.pool.get('res.partner')
598 res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
599 contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
600 res = lead.write({'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
604 def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
605 address = self.pool.get('res.partner.address')
606 return address.create(cr, uid, {
607 'partner_id': partner_id,
608 'name': lead.contact_name,
610 'mobile': lead.mobile,
611 'email': lead.email_from and to_email(lead.email_from)[0],
613 'title': lead.title and lead.title.id or False,
614 'function': lead.function,
615 'street': lead.street,
616 'street2': lead.street2,
619 'country_id': lead.country_id and lead.country_id.id or False,
620 'state_id': lead.state_id and lead.state_id.id or False,
623 def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
625 This function convert partner based on action.
626 if action is 'create', create new partner with contact and assign lead to new partner_id.
627 otherwise assign lead to specified partner_id
632 for lead in self.browse(cr, uid, ids, context=context):
633 if action == 'create':
635 partner_id = self._lead_create_partner(cr, uid, lead, context=context)
636 self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
637 self._lead_set_partner(cr, uid, lead, partner_id, context=context)
638 partner_ids[lead.id] = partner_id
641 def _send_mail_to_salesman(self, cr, uid, lead, context=None):
643 Send mail to salesman with updated Lead details.
644 @ lead: browse record of 'crm.lead' object.
646 #TOFIX: mail template should be used here instead of fix subject, body text.
647 message = self.pool.get('mail.message')
648 email_to = lead.user_id and lead.user_id.user_email
652 email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
653 partner = lead.partner_id and lead.partner_id.name or lead.partner_name
654 subject = "lead %s converted into opportunity" % lead.name
655 body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
656 return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
659 def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
664 value['section_id'] = team_id
665 if index < len(user_ids):
666 value['user_id'] = user_ids[index]
669 self.write(cr, uid, [lead_id], value, context=context)
672 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):
674 action :('schedule','Schedule a call'), ('log','Log a call')
676 phonecall = self.pool.get('crm.phonecall')
677 model_data = self.pool.get('ir.model.data')
680 res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
682 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
683 for lead in self.browse(cr, uid, ids, context=context):
685 section_id = lead.section_id and lead.section_id.id or False
687 user_id = lead.user_id and lead.user_id.id or False
689 'name' : call_summary,
690 'opportunity_id' : lead.id,
691 'user_id' : user_id or False,
692 'categ_id' : categ_id or False,
693 'description' : desc or '',
694 'date' : schedule_time,
695 'section_id' : section_id or False,
696 'partner_id': lead.partner_id and lead.partner_id.id or False,
697 'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
698 'partner_phone' : phone or lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
699 'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
700 'priority': lead.priority,
703 new_id = phonecall.create(cr, uid, vals, context=context)
704 phonecall.case_open(cr, uid, [new_id])
706 phonecall.case_close(cr, uid, [new_id])
707 phonecall_dict[lead.id] = new_id
708 return phonecall_dict
711 def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
712 models_data = self.pool.get('ir.model.data')
714 # Get Opportunity views
715 form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
716 tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
718 'name': _('Opportunity'),
720 'view_mode': 'tree, form',
721 'res_model': 'crm.lead',
722 'domain': [('type', '=', 'opportunity')],
723 'res_id': int(opportunity_id),
725 'views': [(form_view and form_view[1] or False, 'form'),
726 (tree_view and tree_view[1] or False, 'tree'),
727 (False, 'calendar'), (False, 'graph')],
728 'type': 'ir.actions.act_window',
732 def message_new(self, cr, uid, msg, custom_values=None, context=None):
733 """Automatically calls when new email message arrives"""
734 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
735 subject = msg.get('subject') or _("No Subject")
736 body = msg.get('body_text')
738 msg_from = msg.get('from')
739 priority = msg.get('priority')
742 'email_from': msg_from,
743 'email_cc': msg.get('cc'),
748 vals['priority'] = priority
749 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
750 self.write(cr, uid, [res_id], vals, context)
753 def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
754 if isinstance(ids, (str, int, long)):
758 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
760 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
761 vals['priority'] = msg.get('priority')
763 'cost':'planned_cost',
764 'revenue': 'planned_revenue',
765 'probability':'probability'
768 for line in msg['body_text'].split('\n'):
770 res = tools.misc.command_re.match(line)
771 if res and maps.get(res.group(1).lower()):
772 key = maps.get(res.group(1).lower())
773 vls[key] = res.group(2).lower()
776 # Unfortunately the API is based on lists
777 # but we want to update the state based on the
778 # previous state, so we have to loop:
779 for case in self.browse(cr, uid, ids, context=context):
781 if case.state in CRM_LEAD_PENDING_STATES:
783 values.update(state=crm.AVAILABLE_STATES[1][0])
784 if not case.date_open:
785 values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
786 res = self.write(cr, uid, [case.id], values, context=context)
789 def action_makeMeeting(self, cr, uid, ids, context=None):
791 This opens Meeting's calendar view to schedule meeting on current Opportunity
792 @return : Dictionary value for created Meeting view
797 data_obj = self.pool.get('ir.model.data')
798 for opp in self.browse(cr, uid, ids, context=context):
800 tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
801 form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
802 calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
803 search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
805 'default_opportunity_id': opp.id,
806 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
807 'default_user_id': uid,
808 'default_section_id': opp.section_id and opp.section_id.id or False,
809 'default_email_from': opp.email_from,
810 'default_state': 'open',
811 'default_name': opp.name
814 'name': _('Meetings'),
817 'view_mode': 'calendar,form,tree',
818 'res_model': 'crm.meeting',
820 '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')],
821 'type': 'ir.actions.act_window',
822 'search_view_id': search_view and search_view[1] or False,
828 def unlink(self, cr, uid, ids, context=None):
829 for lead in self.browse(cr, uid, ids, context):
830 if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
831 raise osv.except_osv(_('Warning !'),
832 _('You can not delete this lead. You should better cancel it.'))
833 return super(crm_lead, self).unlink(cr, uid, ids, context)
836 def write(self, cr, uid, ids, vals, context=None):
840 if 'date_closed' in vals:
841 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
843 if 'stage_id' in vals and vals['stage_id']:
844 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
845 text = _("Changed Stage to: %s") % stage_obj.name
846 self.message_append(cr, uid, ids, text, body_text=text, context=context)
848 for case in self.browse(cr, uid, ids, context=context):
849 if case.type == 'lead' or context.get('stage_type',False)=='lead':
850 message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
851 elif case.type == 'opportunity':
852 message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
853 self.log(cr, uid, case.id, message)
854 return super(crm_lead,self).write(cr, uid, ids, vals, context)
858 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: