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
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 _compute_day(self, cr, uid, ids, fields, args, context=None):
47 @param cr: the current row, from the database cursor,
48 @param uid: the current user’s ID for security checks,
49 @param ids: List of Openday’s IDs
50 @return: difference between current date and log date
51 @param context: A standard dictionary for contextual values
53 cal_obj = self.pool.get('resource.calendar')
54 res_obj = self.pool.get('resource.resource')
57 for lead in self.browse(cr, uid, ids, context=context):
62 if field == 'day_open':
64 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
65 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
66 ans = date_open - date_create
67 date_until = lead.date_open
68 elif field == 'day_close':
70 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
71 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
72 date_until = lead.date_closed
73 ans = date_close - date_create
77 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
79 resource_id = resource_ids[0]
81 duration = float(ans.days)
82 if lead.section_id and lead.section_id.resource_calendar_id:
83 duration = float(ans.days) * 24
84 new_dates = cal_obj.interval_get(cr,
86 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
87 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
92 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
93 for in_time, out_time in new_dates:
94 if in_time.date not in no_days:
95 no_days.append(in_time.date)
96 if out_time > date_until:
98 duration = len(no_days)
99 res[lead.id][field] = abs(int(duration))
102 def _history_search(self, cr, uid, obj, name, args, context=None):
104 msg_obj = self.pool.get('mail.message')
105 message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
106 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
109 return [('id', 'in', lead_ids)]
111 return [('id', '=', '0')]
113 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
115 for obj in self.browse(cr, uid, ids, context=context):
117 for msg in obj.message_ids:
119 res[obj.id] = msg.subject
124 # Overridden from res.partner.address:
125 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
126 select=True, help="Optional linked partner, usually after conversion of the lead"),
128 'id': fields.integer('ID'),
129 'name': fields.char('Name', size=64, select=1),
130 'active': fields.boolean('Active', required=False),
131 'date_action_last': fields.datetime('Last Action', readonly=1),
132 'date_action_next': fields.datetime('Next Action', readonly=1),
133 'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
134 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
135 select=True, help='When sending mails, the default email address is taken from the sales team.'),
136 'create_date': fields.datetime('Creation Date' , readonly=True),
137 '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"),
138 'description': fields.text('Notes'),
139 'write_date': fields.datetime('Update Date' , readonly=True),
141 'categ_id': fields.many2one('crm.case.categ', 'Category', \
142 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
143 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
144 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
145 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
146 'contact_name': fields.char('Contact Name', size=64),
147 '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),
148 'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
149 'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
150 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
151 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
152 'date_closed': fields.datetime('Closed', readonly=True),
153 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
154 'user_id': fields.many2one('res.users', 'Salesman'),
155 'referred': fields.char('Referred By', size=64),
156 'date_open': fields.datetime('Opened', readonly=True),
157 'day_open': fields.function(_compute_day, string='Days to Open', \
158 multi='day_open', type="float", store=True),
159 'day_close': fields.function(_compute_day, string='Days to Close', \
160 multi='day_close', type="float", store=True),
161 'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
162 help='The state is set to \'Draft\', when a case is created.\
163 \nIf the case is in progress the state is set to \'Open\'.\
164 \nWhen the case is over, the state is set to \'Done\'.\
165 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
166 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
167 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
170 # Only used for type opportunity
171 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"),
172 'probability': fields.float('Probability (%)',group_operator="avg"),
173 'planned_revenue': fields.float('Expected Revenue'),
174 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
175 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
176 'phone': fields.char("Phone", size=64),
177 'date_deadline': fields.date('Expected Closing'),
178 'date_action': fields.date('Next Action Date'),
179 'title_action': fields.char('Next Action', size=64),
180 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
184 'active': lambda *a: 1,
185 'user_id': crm_case._get_default_user,
186 'email_from': crm_case._get_default_email,
187 'state': lambda *a: 'draft',
188 'type': lambda *a: 'lead',
189 'section_id': crm_case._get_section,
190 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
191 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
192 #'stage_id': _get_stage_id,
195 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
196 """This function returns value of partner email based on Partner Address
199 return {'value': {'email_from': False, 'country_id': False}}
200 address = self.pool.get('res.partner.address').browse(cr, uid, add)
201 return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
203 def on_change_optin(self, cr, uid, ids, optin):
204 return {'value':{'optin':optin,'optout':False}}
206 def on_change_optout(self, cr, uid, ids, optout):
207 return {'value':{'optout':optout,'optin':False}}
209 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
212 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
213 if not stage.on_change:
215 return {'value':{'probability': stage.probability}}
217 def stage_find_percent(self, cr, uid, percent, section_id):
218 """ Return the first stage with a probability == percent
220 stage_pool = self.pool.get('crm.case.stage')
222 ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
224 ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
230 def stage_find_lost(self, cr, uid, section_id):
231 return self.stage_find_percent(cr, uid, 0.0, section_id)
233 def stage_find_won(self, cr, uid, section_id):
234 return self.stage_find_percent(cr, uid, 100.0, section_id)
236 def case_open(self, cr, uid, ids, *args):
237 for l in self.browse(cr, uid, ids):
238 # When coming from draft override date and stage otherwise just set state
239 if l.state == 'draft':
241 message = _("The lead '%s' has been opened.") % l.name
242 elif l.type == 'opportunity':
243 message = _("The opportunity '%s' has been opened.") % l.name
245 message = _("The case '%s' has been opened.") % l.name
246 self.log(cr, uid, l.id, message)
247 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
248 self.write(cr, uid, [l.id], value)
249 if l.type == 'opportunity' and not l.stage_id:
250 stage_id = self.stage_find(cr, uid, l.section_id.id or False, [('sequence','>',0)])
252 self.stage_set(cr, uid, [l.id], stage_id)
253 res = super(crm_lead, self).case_open(cr, uid, ids, *args)
256 def case_close(self, cr, uid, ids, *args):
257 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
258 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
259 for case in self.browse(cr, uid, ids):
260 if case.type == 'lead':
261 message = _("The lead '%s' has been closed.") % case.name
263 message = _("The case '%s' has been closed.") % case.name
264 self.log(cr, uid, case.id, message)
267 def case_cancel(self, cr, uid, ids, *args):
268 """Overrides cancel for crm_case for setting probability
270 res = super(crm_lead, self).case_cancel(cr, uid, ids, args)
271 self.write(cr, uid, ids, {'probability' : 0.0})
274 def case_reset(self, cr, uid, ids, *args):
275 """Overrides reset as draft in order to set the stage field as empty
277 res = super(crm_lead, self).case_reset(cr, uid, ids, *args)
278 self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
281 def case_mark_lost(self, cr, uid, ids, *args):
282 """Mark the case as lost: state = done and probability = 0%
284 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
285 self.write(cr, uid, ids, {'probability' : 0.0})
286 for l in self.browse(cr, uid, ids):
287 stage_id = self.stage_find_lost(cr, uid, l.section_id.id or False)
289 self.stage_set(cr, uid, [l.id], stage_id)
290 message = _("The opportunity '%s' has been marked as lost.") % l.name
291 self.log(cr, uid, l.id, message)
294 def case_mark_won(self, cr, uid, ids, *args):
295 """Mark the case as lost: state = done and probability = 0%
297 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
298 self.write(cr, uid, ids, {'probability' : 100.0})
299 for l in self.browse(cr, uid, ids):
300 stage_id = self.stage_find_won(cr, uid, l.section_id.id or False)
302 self.stage_set(cr, uid, [l.id], stage_id)
303 message = _("The opportunity '%s' has been been won.") % l.name
304 self.log(cr, uid, l.id, message)
307 def convert_opportunity(self, cr, uid, ids, context=None):
308 """ Precomputation for converting lead to opportunity
312 context.update({'active_ids': ids})
314 data_obj = self.pool.get('ir.model.data')
318 for case in self.browse(cr, uid, ids, context=context):
319 context.update({'active_id': case.id})
320 data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
323 view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
325 'name': _('Create Partner'),
327 'view_mode': 'form,tree',
328 'res_model': 'crm.lead2opportunity.partner',
331 'views': [(view_id1, 'form')],
332 'type': 'ir.actions.act_window',
338 def message_new(self, cr, uid, msg, custom_values=None, context=None):
339 """Automatically calls when new email message arrives"""
340 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
341 subject = msg.get('subject') or _("No Subject")
342 body = msg.get('body_text')
344 msg_from = msg.get('from')
345 priority = msg.get('priority')
348 'email_from': msg_from,
349 'email_cc': msg.get('cc'),
354 vals['priority'] = priority
355 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
356 res_id = self.write(cr, uid, [res_id], vals, context)
359 def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
360 if isinstance(ids, (str, int, long)):
363 super(crm_lead, self).message_update(cr, uid, msg,
364 custom_values=custom_values,
367 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
368 vals['priority'] = msg.get('priority')
370 'cost':'planned_cost',
371 'revenue': 'planned_revenue',
372 'probability':'probability'
375 for line in msg['body_text'].split('\n'):
377 res = tools.misc.command_re.match(line)
378 if res and maps.get(res.group(1).lower()):
379 key = maps.get(res.group(1).lower())
380 vls[key] = res.group(2).lower()
383 # Unfortunately the API is based on lists
384 # but we want to update the state based on the
385 # previous state, so we have to loop:
386 for case in self.browse(cr, uid, ids, context=context):
388 if case.state in CRM_LEAD_PENDING_STATES:
389 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
390 res = self.write(cr, uid, [case.id], values, context=context)
393 def msg_send(self, cr, uid, id, *args, **argv):
395 @param ids: List of email’s IDs
399 def action_makeMeeting(self, cr, uid, ids, context=None):
401 This opens Meeting's calendar view to schedule meeting on current Opportunity
402 @return : Dictionary value for created Meeting view
405 for opp in self.browse(cr, uid, ids, context=context):
406 data_obj = self.pool.get('ir.model.data')
409 result = data_obj._get_id(cr, uid, 'crm', 'view_crm_case_meetings_filter')
410 res = data_obj.read(cr, uid, result, ['res_id'])
411 id1 = data_obj._get_id(cr, uid, 'crm', 'crm_case_calendar_view_meet')
412 id2 = data_obj._get_id(cr, uid, 'crm', 'crm_case_form_view_meet')
413 id3 = data_obj._get_id(cr, uid, 'crm', 'crm_case_tree_view_meet')
415 id1 = data_obj.browse(cr, uid, id1, context=context).res_id
417 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
419 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
422 'default_opportunity_id': opp.id,
423 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
424 'default_user_id': uid,
425 'default_section_id': opp.section_id and opp.section_id.id or False,
426 'default_email_from': opp.email_from,
427 'default_state': 'open',
428 'default_name': opp.name
431 'name': _('Meetings'),
434 'view_mode': 'calendar,form,tree',
435 'res_model': 'crm.meeting',
437 'views': [(id1, 'calendar'), (id2, 'form'), (id3, 'tree')],
438 'type': 'ir.actions.act_window',
439 'search_view_id': res['res_id'],
445 def unlink(self, cr, uid, ids, context=None):
446 for lead in self.browse(cr, uid, ids, context):
447 if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
448 raise osv.except_osv(_('Warning !'),
449 _('You can not delete this lead. You should better cancel it.'))
450 return super(crm_lead, self).unlink(cr, uid, ids, context)
453 def write(self, cr, uid, ids, vals, context=None):
457 if 'date_closed' in vals:
458 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
460 if 'stage_id' in vals and vals['stage_id']:
461 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
462 text = _("Changed Stage to: %s") % stage_obj.name
463 self.message_append(cr, uid, ids, text, body_text=text, context=context)
465 for case in self.browse(cr, uid, ids, context=context):
466 if case.type == 'lead' or context.get('stage_type',False)=='lead':
467 message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
468 elif case.type == 'opportunity':
469 message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
470 self.log(cr, uid, case.id, message)
471 return super(crm_lead,self).write(cr, uid, ids, vals, context)
475 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: