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)]),
177 'active': lambda *a: 1,
178 'user_id': crm_case._get_default_user,
179 'email_from': crm_case._get_default_email,
180 'state': lambda *a: 'draft',
181 'type': lambda *a: 'lead',
182 'section_id': crm_case._get_section,
183 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
184 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
185 #'stage_id': _get_stage_id,
190 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
191 """This function returns value of partner email based on Partner Address
192 @param self: The object pointer
193 @param cr: the current row, from the database cursor,
194 @param uid: the current user’s ID for security checks,
195 @param ids: List of case IDs
196 @param add: Id of Partner's address
197 @email: Partner's email ID
200 return {'value': {'email_from': False, 'country_id': False}}
201 address = self.pool.get('res.partner.address').browse(cr, uid, add)
202 return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
204 def case_open(self, cr, uid, ids, *args):
205 """Overrides cancel for crm_case for setting Open Date
206 @param self: The object pointer
207 @param cr: the current row, from the database cursor,
208 @param uid: the current user’s ID for security checks,
209 @param ids: List of case's Ids
210 @param *args: Give Tuple Value
212 leads = self.browse(cr, uid, ids)
216 for i in xrange(0, len(ids)):
217 if leads[i].state == 'draft':
219 if not leads[i].stage_id :
220 stage_id = self._find_first_stage(cr, uid, leads[i].type, leads[i].section_id.id or False)
221 value.update({'stage_id' : stage_id})
222 value.update({'date_open': time.strftime('%Y-%m-%d %H:%M:%S')})
223 self.write(cr, uid, [ids[i]], value)
224 self.log_open( cr, uid, leads[i])
225 res = super(crm_lead, self).case_open(cr, uid, ids, *args)
228 def log_open(self, cr, uid, case):
229 if case.type == 'lead':
230 message = _("The lead '%s' has been opened.") % case.name
231 elif case.type == 'opportunity':
232 message = _("The opportunity '%s' has been opened.") % case.name
234 message = _("The case '%s' has been opened.") % case.name
235 self.log(cr, uid, case.id, message)
237 def case_close(self, cr, uid, ids, *args):
238 """Overrides close for crm_case for setting close date
239 @param self: The object pointer
240 @param cr: the current row, from the database cursor,
241 @param uid: the current user’s ID for security checks,
242 @param ids: List of case Ids
243 @param *args: Tuple Value for additional Params
245 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
246 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
247 for case in self.browse(cr, uid, ids):
248 if case.type == 'lead':
249 message = _("The lead '%s' has been closed.") % case.name
250 elif case.type == 'opportunity':
251 message = _("The opportunity '%s' has been closed.") % case.name
253 message = _("The case '%s' has been closed.") % case.name
254 self.log(cr, uid, case.id, message)
257 def convert_opportunity(self, cr, uid, ids, context=None):
258 """ Precomputation for converting lead to opportunity
259 @param cr: the current row, from the database cursor,
260 @param uid: the current user’s ID for security checks,
261 @param ids: List of closeday’s IDs
262 @param context: A standard dictionary for contextual values
263 @return: Value of action in dict
267 context.update({'active_ids': ids})
269 data_obj = self.pool.get('ir.model.data')
274 for case in self.browse(cr, uid, ids, context=context):
275 context.update({'active_id': case.id})
276 data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
279 view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
281 'name': _('Create Partner'),
283 'view_mode': 'form,tree',
284 'res_model': 'crm.lead2opportunity.partner',
287 'views': [(view_id1, 'form')],
288 'type': 'ir.actions.act_window',
294 def write(self, cr, uid, ids, vals, context=None):
298 if 'date_closed' in vals:
299 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
301 if 'stage_id' in vals and vals['stage_id']:
302 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
303 self.history(cr, uid, ids, _("Changed Stage to: %s") % stage_obj.name, details=_("Changed Stage to: %s") % stage_obj.name)
305 for case in self.browse(cr, uid, ids, context=context):
306 if case.type == 'lead' or context.get('stage_type',False)=='lead':
307 message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
308 elif case.type == 'opportunity':
309 message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
310 self.log(cr, uid, case.id, message)
311 return super(crm_lead,self).write(cr, uid, ids, vals, context)
313 def stage_next(self, cr, uid, ids, context=None):
314 stage = super(crm_lead, self).stage_next(cr, uid, ids, context=context)
316 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
317 if stage_obj.on_change:
318 data = {'probability': stage_obj.probability}
319 self.write(cr, uid, ids, data)
322 def stage_previous(self, cr, uid, ids, context=None):
323 stage = super(crm_lead, self).stage_previous(cr, uid, ids, context=context)
325 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
326 if stage_obj.on_change:
327 data = {'probability': stage_obj.probability}
328 self.write(cr, uid, ids, data)
331 def unlink(self, cr, uid, ids, context=None):
332 for lead in self.browse(cr, uid, ids, context):
333 if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
334 raise osv.except_osv(_('Warning !'),
335 _('You can not delete this lead. You should better cancel it.'))
336 return super(crm_lead, self).unlink(cr, uid, ids, context)
338 def message_new(self, cr, uid, msg, context=None):
340 Automatically calls when new email message arrives
342 @param self: The object pointer
343 @param cr: the current row, from the database cursor,
344 @param uid: the current user’s ID for security checks
345 @param msg: dictionary object to contain email message data
347 thread_pool = self.pool.get('email.thread')
349 subject = msg.get('subject')
350 body = msg.get('body')
351 msg_from = msg.get('from')
352 priority = msg.get('priority')
356 'email_from': msg_from,
357 'email_cc': msg.get('cc'),
361 if msg.get('priority', False):
362 vals['priority'] = priority
364 res = thread_pool.get_partner(cr, uid, msg.get('from', False))
368 res_id = self.create(cr, uid, vals, context)
370 attachments = msg.get('attachments', {})
371 self.history(cr, uid, [res_id], _('receive'), history=True,
372 subject = msg.get('subject'),
373 email = msg.get('to'),
374 details = msg.get('body'),
375 email_from = msg.get('from'),
376 email_cc = msg.get('cc'),
377 message_id = msg.get('message-id'),
378 references = msg.get('references', False) or msg.get('in-reply-to', False),
379 attach = attachments,
380 email_date = msg.get('date'),
381 body_html= msg.get('body_html'),
382 sub_type = msg.get('sub_type'),
383 headers = msg.get('headers'),
384 priority = msg.get('priority', False),
389 def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
391 @param self: The object pointer
392 @param cr: the current row, from the database cursor,
393 @param uid: the current user’s ID for security checks,
394 @param ids: List of update mail’s IDs
396 if isinstance(ids, (str, int, long)):
399 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
400 vals['priority'] = msg.get('priority')
403 'cost':'planned_cost',
404 'revenue': 'planned_revenue',
405 'probability':'probability'
408 for line in msg['body'].split('\n'):
410 res = tools.misc.command_re.match(line)
411 if res and maps.get(res.group(1).lower()):
412 key = maps.get(res.group(1).lower())
413 vls[key] = res.group(2).lower()
416 # Unfortunately the API is based on lists
417 # but we want to update the state based on the
418 # previous state, so we have to loop:
419 for case in self.browse(cr, uid, ids, context=context):
421 if case.state in CRM_LEAD_PENDING_STATES:
422 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
423 res = self.write(cr, uid, [case.id], values, context=context)
425 attachments = msg.get('attachments', {})
426 self.history(cr, uid, ids, _('receive'), history=True,
427 subject = msg.get('subject'),
428 email = msg.get('to'),
429 details = msg.get('body'),
430 email_from = msg.get('from'),
431 email_cc = msg.get('cc'),
432 message_id = msg.get('message-id'),
433 references = msg.get('references', False) or msg.get('in-reply-to', False),
434 attach = attachments,
435 email_date = msg.get('date'),
439 def on_change_optin(self, cr, uid, ids, optin):
440 return {'value':{'optin':optin,'optout':False}}
442 def on_change_optout(self, cr, uid, ids, optout):
443 return {'value':{'optout':optout,'optin':False}}
447 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: