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 = "date_action, priority, id desc"
43 _inherit = ['email.thread','res.partner.address']
44 def _compute_day(self, cr, uid, ids, fields, args, context=None):
46 @param cr: the current row, from the database cursor,
47 @param uid: the current user’s ID for security checks,
48 @param ids: List of Openday’s IDs
49 @return: difference between current date and log date
50 @param context: A standard dictionary for contextual values
52 cal_obj = self.pool.get('resource.calendar')
53 res_obj = self.pool.get('resource.resource')
56 for lead in self.browse(cr, uid, ids, context=context):
61 if field == 'day_open':
63 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
64 date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
65 ans = date_open - date_create
66 date_until = lead.date_open
67 elif field == 'day_close':
69 date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
70 date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
71 date_until = lead.date_closed
72 ans = date_close - date_create
76 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
78 resource_id = resource_ids[0]
80 duration = float(ans.days)
81 if lead.section_id and lead.section_id.resource_calendar_id:
82 duration = float(ans.days) * 24
83 new_dates = cal_obj.interval_get(cr,
85 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
86 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
91 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
92 for in_time, out_time in new_dates:
93 if in_time.date not in no_days:
94 no_days.append(in_time.date)
95 if out_time > date_until:
97 duration = len(no_days)
98 res[lead.id][field] = abs(int(duration))
101 def _history_search(self, cr, uid, obj, name, args, context=None):
103 msg_obj = self.pool.get('email.message')
104 message_ids = msg_obj.search(cr, uid, [('history','=',True), ('subject', args[0][1], args[0][2])], context=context)
105 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
108 return [('id', 'in', lead_ids)]
110 return [('id', '=', '0')]
112 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
114 for obj in self.browse(cr, uid, ids, context=context):
116 for msg in obj.message_ids:
118 res[obj.id] = msg.subject
123 # Overridden from res.partner.address:
124 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
125 select=True, help="Optional linked partner, usually after conversion of the lead"),
128 'id': fields.integer('ID'),
129 'name': fields.char('Name', size=64),
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"),
134 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
135 select=True, help='Sales team to which this case belongs to. Defines responsible user and e-mail address for the mail gateway.'),
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),
142 'categ_id': fields.many2one('crm.case.categ', 'Category', \
143 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
144 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
145 domain="['|',('section_id','=',section_id),('section_id','=',False)]"),
146 'channel_id': fields.many2one('res.partner.canal', 'Channel'),
148 'contact_name': fields.char('Contact Name', size=64),
149 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner that will be created while converting the into opportunity'),
150 'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
151 'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
152 'type':fields.selection([
154 ('opportunity','Opportunity'),
156 ],'Type', help="Type is used to separate Leads and Opportunities"),
157 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
158 'date_closed': fields.datetime('Closed', readonly=True),
159 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('type','=','lead')]"),
160 'user_id': fields.many2one('res.users', 'Salesman'),
161 'referred': fields.char('Referred By', size=64),
162 'date_open': fields.datetime('Opened', readonly=True),
163 'day_open': fields.function(_compute_day, string='Days to Open', \
164 method=True, multi='day_open', type="float", store=True),
165 'day_close': fields.function(_compute_day, string='Days to Close', \
166 method=True, multi='day_close', type="float", store=True),
167 'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
168 help='The state is set to \'Draft\', when a case is created.\
169 \nIf the case is in progress the state is set to \'Open\'.\
170 \nWhen the case is over, the state is set to \'Done\'.\
171 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
172 'message_ids': fields.one2many('email.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
173 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', method=True, type='char', size=64),
178 'active': lambda *a: 1,
179 'user_id': crm_case._get_default_user,
180 'email_from': crm_case._get_default_email,
181 'state': lambda *a: 'draft',
182 'type': lambda *a: 'lead',
183 'section_id': crm_case._get_section,
184 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
185 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
186 #'stage_id': _get_stage_id,
191 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
192 """This function returns value of partner email based on Partner Address
193 @param self: The object pointer
194 @param cr: the current row, from the database cursor,
195 @param uid: the current user’s ID for security checks,
196 @param ids: List of case IDs
197 @param add: Id of Partner's address
198 @email: Partner's email ID
201 return {'value': {'email_from': False, 'country_id': False}}
202 address = self.pool.get('res.partner.address').browse(cr, uid, add)
203 return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
205 def case_open(self, cr, uid, ids, *args):
206 """Overrides cancel for crm_case for setting Open Date
207 @param self: The object pointer
208 @param cr: the current row, from the database cursor,
209 @param uid: the current user’s ID for security checks,
210 @param ids: List of case's Ids
211 @param *args: Give Tuple Value
213 leads = self.browse(cr, uid, ids)
217 for i in xrange(0, len(ids)):
218 if leads[i].state == 'draft':
220 if not leads[i].stage_id :
221 stage_id = self._find_first_stage(cr, uid, leads[i].type, leads[i].section_id.id or False)
222 value.update({'stage_id' : stage_id})
223 value.update({'date_open': time.strftime('%Y-%m-%d %H:%M:%S')})
224 self.write(cr, uid, [ids[i]], value)
225 self.log_open( cr, uid, leads[i])
226 res = super(crm_lead, self).case_open(cr, uid, ids, *args)
229 def log_open(self, cr, uid, case):
230 if case.type == 'lead':
231 message = _("The lead '%s' has been opened.") % case.name
232 elif case.type == 'opportunity':
233 message = _("The opportunity '%s' has been opened.") % case.name
235 message = _("The case '%s' has been opened.") % case.name
236 self.log(cr, uid, case.id, message)
238 def case_close(self, cr, uid, ids, *args):
239 """Overrides close for crm_case for setting close date
240 @param self: The object pointer
241 @param cr: the current row, from the database cursor,
242 @param uid: the current user’s ID for security checks,
243 @param ids: List of case Ids
244 @param *args: Tuple Value for additional Params
246 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
247 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
248 for case in self.browse(cr, uid, ids):
249 if case.type == 'lead':
250 message = _("The lead '%s' has been closed.") % case.name
251 elif case.type == 'opportunity':
252 message = _("The opportunity '%s' has been closed.") % case.name
254 message = _("The case '%s' has been closed.") % case.name
255 self.log(cr, uid, case.id, message)
258 def convert_opportunity(self, cr, uid, ids, context=None):
259 """ Precomputation for converting lead to opportunity
260 @param cr: the current row, from the database cursor,
261 @param uid: the current user’s ID for security checks,
262 @param ids: List of closeday’s IDs
263 @param context: A standard dictionary for contextual values
264 @return: Value of action in dict
268 context.update({'active_ids': ids})
270 data_obj = self.pool.get('ir.model.data')
275 for case in self.browse(cr, uid, ids, context=context):
276 context.update({'active_id': case.id})
277 data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
280 view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
282 'name': _('Create Partner'),
284 'view_mode': 'form,tree',
285 'res_model': 'crm.lead2opportunity.partner',
288 'views': [(view_id1, 'form')],
289 'type': 'ir.actions.act_window',
295 def write(self, cr, uid, ids, vals, context=None):
299 if 'date_closed' in vals:
300 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
302 if 'stage_id' in vals and vals['stage_id']:
303 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
304 self.history(cr, uid, ids, _("Changed Stage to: %s") % stage_obj.name, details=_("Changed Stage to: %s") % stage_obj.name)
306 for case in self.browse(cr, uid, ids, context=context):
307 if case.type == 'lead' or context.get('stage_type',False)=='lead':
308 message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
309 elif case.type == 'opportunity':
310 message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
311 self.log(cr, uid, case.id, message)
312 return super(crm_lead,self).write(cr, uid, ids, vals, context)
314 def stage_next(self, cr, uid, ids, context=None):
315 stage = super(crm_lead, self).stage_next(cr, uid, ids, context=context)
317 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
318 if stage_obj.on_change:
319 data = {'probability': stage_obj.probability}
320 self.write(cr, uid, ids, data)
323 def stage_previous(self, cr, uid, ids, context=None):
324 stage = super(crm_lead, self).stage_previous(cr, uid, ids, context=context)
326 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
327 if stage_obj.on_change:
328 data = {'probability': stage_obj.probability}
329 self.write(cr, uid, ids, data)
332 def unlink(self, cr, uid, ids, context=None):
333 for lead in self.browse(cr, uid, ids, context):
334 if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
335 raise osv.except_osv(_('Warning !'),
336 _('You can not delete this lead. You should better cancel it.'))
337 return super(crm_lead, self).unlink(cr, uid, ids, context)
339 def message_new(self, cr, uid, msg, context=None):
341 Automatically calls when new email message arrives
343 @param self: The object pointer
344 @param cr: the current row, from the database cursor,
345 @param uid: the current user’s ID for security checks
346 @param msg: dictionary object to contain email message data
348 thread_pool = self.pool.get('email.thread')
350 subject = msg.get('subject')
351 body = msg.get('body')
352 msg_from = msg.get('from')
353 priority = msg.get('priority')
357 'email_from': msg_from,
358 'email_cc': msg.get('cc'),
362 if msg.get('priority', False):
363 vals['priority'] = priority
365 res = thread_pool.get_partner(cr, uid, msg.get('from', False))
369 res_id = self.create(cr, uid, vals, context)
371 attachments = msg.get('attachments', {})
372 self.history(cr, uid, [res_id], _('receive'), history=True,
373 subject = msg.get('subject'),
374 email = msg.get('to'),
375 details = msg.get('body'),
376 email_from = msg.get('from'),
377 email_cc = msg.get('cc'),
378 message_id = msg.get('message-id'),
379 references = msg.get('references', False) or msg.get('in-reply-to', False),
380 attach = attachments,
381 email_date = msg.get('date'),
382 body_html= msg.get('body_html'),
383 sub_type = msg.get('sub_type'),
384 headers = msg.get('headers'),
385 priority = msg.get('priority'),
390 def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
392 @param self: The object pointer
393 @param cr: the current row, from the database cursor,
394 @param uid: the current user’s ID for security checks,
395 @param ids: List of update mail’s IDs
397 if isinstance(ids, (str, int, long)):
400 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
401 vals['priority'] = msg.get('priority')
404 'cost':'planned_cost',
405 'revenue': 'planned_revenue',
406 'probability':'probability'
409 for line in msg['body'].split('\n'):
411 res = tools.misc.command_re.match(line)
412 if res and maps.get(res.group(1).lower()):
413 key = maps.get(res.group(1).lower())
414 vls[key] = res.group(2).lower()
417 # Unfortunately the API is based on lists
418 # but we want to update the state based on the
419 # previous state, so we have to loop:
420 for case in self.browse(cr, uid, ids, context=context):
422 if case.state in CRM_LEAD_PENDING_STATES:
423 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
424 res = self.write(cr, uid, [case.id], values, context=context)
426 attachments = msg.get('attachments', {})
427 self.history(cr, uid, ids, _('receive'), history=True,
428 subject = msg.get('subject'),
429 email = msg.get('to'),
430 details = msg.get('body'),
431 email_from = msg.get('from'),
432 email_cc = msg.get('cc'),
433 message_id = msg.get('message-id'),
434 references = msg.get('references', False) or msg.get('in-reply-to', False),
435 attach = attachments,
436 email_date = msg.get('date'),
437 body_html= msg.get('body_html'),
438 sub_type = msg.get('sub_type'),
439 headers = msg.get('headers'),
440 priority = msg.get('priority'),
444 def on_change_optin(self, cr, uid, ids, optin):
445 return {'value':{'optin':optin,'optout':False}}
447 def on_change_optout(self, cr, uid, ids, optout):
448 return {'value':{'optout':optout,'optin':False}}
452 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: