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 _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')
346 def convert_opportunity(self, cr, uid, ids, context=None):
347 """ Precomputation for converting lead to opportunity
351 context.update({'active_ids': ids})
353 data_obj = self.pool.get('ir.model.data')
357 for case in self.browse(cr, uid, ids, context=context):
358 context.update({'active_id': case.id})
359 data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
362 view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
364 'name': _('Create Partner'),
366 'view_mode': 'form,tree',
367 'res_model': 'crm.lead2opportunity.partner',
370 'views': [(view_id1, 'form')],
371 'type': 'ir.actions.act_window',
377 def message_new(self, cr, uid, msg, custom_values=None, context=None):
378 """Automatically calls when new email message arrives"""
379 res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
380 subject = msg.get('subject') or _("No Subject")
381 body = msg.get('body_text')
383 msg_from = msg.get('from')
384 priority = msg.get('priority')
387 'email_from': msg_from,
388 'email_cc': msg.get('cc'),
393 vals['priority'] = priority
394 vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
395 self.write(cr, uid, [res_id], vals, context)
398 def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
399 if isinstance(ids, (str, int, long)):
402 super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
404 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
405 vals['priority'] = msg.get('priority')
407 'cost':'planned_cost',
408 'revenue': 'planned_revenue',
409 'probability':'probability'
412 for line in msg['body_text'].split('\n'):
414 res = tools.misc.command_re.match(line)
415 if res and maps.get(res.group(1).lower()):
416 key = maps.get(res.group(1).lower())
417 vls[key] = res.group(2).lower()
420 # Unfortunately the API is based on lists
421 # but we want to update the state based on the
422 # previous state, so we have to loop:
423 for case in self.browse(cr, uid, ids, context=context):
425 if case.state in CRM_LEAD_PENDING_STATES:
426 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
427 res = self.write(cr, uid, [case.id], values, context=context)
430 def action_makeMeeting(self, cr, uid, ids, context=None):
432 This opens Meeting's calendar view to schedule meeting on current Opportunity
433 @return : Dictionary value for created Meeting view
436 for opp in self.browse(cr, uid, ids, context=context):
437 data_obj = self.pool.get('ir.model.data')
440 result = data_obj._get_id(cr, uid, 'crm', 'view_crm_case_meetings_filter')
441 res = data_obj.read(cr, uid, result, ['res_id'])
442 id1 = data_obj._get_id(cr, uid, 'crm', 'crm_case_calendar_view_meet')
443 id2 = data_obj._get_id(cr, uid, 'crm', 'crm_case_form_view_meet')
444 id3 = data_obj._get_id(cr, uid, 'crm', 'crm_case_tree_view_meet')
446 id1 = data_obj.browse(cr, uid, id1, context=context).res_id
448 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
450 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
453 'default_opportunity_id': opp.id,
454 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
455 'default_user_id': uid,
456 'default_section_id': opp.section_id and opp.section_id.id or False,
457 'default_email_from': opp.email_from,
458 'default_state': 'open',
459 'default_name': opp.name
462 'name': _('Meetings'),
465 'view_mode': 'calendar,form,tree',
466 'res_model': 'crm.meeting',
468 'views': [(id1, 'calendar'), (id2, 'form'), (id3, 'tree')],
469 'type': 'ir.actions.act_window',
470 'search_view_id': res['res_id'],
476 def unlink(self, cr, uid, ids, context=None):
477 for lead in self.browse(cr, uid, ids, context):
478 if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
479 raise osv.except_osv(_('Warning !'),
480 _('You can not delete this lead. You should better cancel it.'))
481 return super(crm_lead, self).unlink(cr, uid, ids, context)
484 def write(self, cr, uid, ids, vals, context=None):
488 if 'date_closed' in vals:
489 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
491 if 'stage_id' in vals and vals['stage_id']:
492 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
493 text = _("Changed Stage to: %s") % stage_obj.name
494 self.message_append(cr, uid, ids, text, body_text=text, context=context)
496 for case in self.browse(cr, uid, ids, context=context):
497 if case.type == 'lead' or context.get('stage_type',False)=='lead':
498 message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
499 elif case.type == 'opportunity':
500 message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
501 self.log(cr, uid, case.id, message)
502 return super(crm_lead,self).write(cr, uid, ids, vals, context)
506 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: