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 = ['mailgate.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('mailgate.message')
104 message_ids = msg_obj.search(cr, uid, [('history','=',True), ('name', 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.name
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)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
146 'channel_id': fields.many2one('res.partner.canal', 'Channel', help="From which channel (mail, direct, phone, ...) did this contact reach you?"),
147 'contact_name': fields.char('Contact Name', size=64),
148 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner that will be created while converting the into opportunity'),
149 'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
150 'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
151 'type':fields.selection([
153 ('opportunity','Opportunity'),
155 ],'Type', help="Type is used to separate Leads and Opportunities"),
156 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
157 'date_closed': fields.datetime('Closed', readonly=True),
158 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[(section_ids', '=', section_id)]"),
159 'user_id': fields.many2one('res.users', 'Salesman'),
160 'referred': fields.char('Referred By', size=64),
161 'date_open': fields.datetime('Opened', readonly=True),
162 'day_open': fields.function(_compute_day, string='Days to Open', \
163 multi='day_open', type="float", store=True),
164 'day_close': fields.function(_compute_day, string='Days to Close', \
165 multi='day_close', type="float", store=True),
166 'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
167 help='The state is set to \'Draft\', when a case is created.\
168 \nIf the case is in progress the state is set to \'Open\'.\
169 \nWhen the case is over, the state is set to \'Done\'.\
170 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
171 'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
172 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
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')
273 for case in self.browse(cr, uid, ids, context=context):
274 context.update({'active_id': case.id})
275 data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
278 view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
280 'name': _('Create Partner'),
282 'view_mode': 'form,tree',
283 'res_model': 'crm.lead2opportunity.partner',
286 'views': [(view_id1, 'form')],
287 'type': 'ir.actions.act_window',
293 def write(self, cr, uid, ids, vals, context=None):
297 if 'date_closed' in vals:
298 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
300 if 'stage_id' in vals and vals['stage_id']:
301 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
302 self.history(cr, uid, ids, _("Changed Stage to: %s") % stage_obj.name, details=_("Changed Stage to: %s") % stage_obj.name)
304 for case in self.browse(cr, uid, ids, context=context):
305 if case.type == 'lead' or context.get('stage_type',False)=='lead':
306 message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
307 elif case.type == 'opportunity':
308 message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
309 self.log(cr, uid, case.id, message)
310 return super(crm_lead,self).write(cr, uid, ids, vals, context)
312 def stage_next(self, cr, uid, ids, context=None):
313 stage = super(crm_lead, self).stage_next(cr, uid, ids, context=context)
315 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
316 if stage_obj.on_change:
317 data = {'probability': stage_obj.probability}
318 self.write(cr, uid, ids, data)
321 def stage_previous(self, cr, uid, ids, context=None):
322 stage = super(crm_lead, self).stage_previous(cr, uid, ids, context=context)
324 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
325 if stage_obj.on_change:
326 data = {'probability': stage_obj.probability}
327 self.write(cr, uid, ids, data)
330 def unlink(self, cr, uid, ids, context=None):
331 for lead in self.browse(cr, uid, ids, context):
332 if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
333 raise osv.except_osv(_('Warning !'),
334 _('You can not delete this lead. You should better cancel it.'))
335 return super(crm_lead, self).unlink(cr, uid, ids, context)
337 def message_new(self, cr, uid, msg, context=None):
339 Automatically calls when new email message arrives
341 @param self: The object pointer
342 @param cr: the current row, from the database cursor,
343 @param uid: the current user’s ID for security checks
345 mailgate_pool = self.pool.get('email.server.tools')
347 subject = msg.get('subject') or _("No Subject")
348 body = msg.get('body')
349 msg_from = msg.get('from')
350 priority = msg.get('priority')
354 'email_from': msg_from,
355 'email_cc': msg.get('cc'),
359 if msg.get('priority', False):
360 vals['priority'] = priority
362 res = mailgate_pool.get_partner(cr, uid, msg.get('from') or msg.get_unixfrom())
366 res = self.create(cr, uid, vals, context)
367 attachents = msg.get('attachments', [])
368 for attactment in attachents or []:
371 'datas':binascii.b2a_base64(str(attachents.get(attactment))),
372 'datas_fname': attactment,
373 'description': 'Mail attachment',
374 'res_model': self._name,
377 self.pool.get('ir.attachment').create(cr, uid, data_attach)
381 def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context=None):
383 @param self: The object pointer
384 @param cr: the current row, from the database cursor,
385 @param uid: the current user’s ID for security checks,
386 @param ids: List of update mail’s IDs
388 if isinstance(ids, (str, int, long)):
391 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
392 vals['priority'] = msg.get('priority')
395 'cost':'planned_cost',
396 'revenue': 'planned_revenue',
397 'probability':'probability'
400 for line in msg['body'].split('\n'):
402 res = tools.misc.command_re.match(line)
403 if res and maps.get(res.group(1).lower()):
404 key = maps.get(res.group(1).lower())
405 vls[key] = res.group(2).lower()
408 # Unfortunately the API is based on lists
409 # but we want to update the state based on the
410 # previous state, so we have to loop:
411 for case in self.browse(cr, uid, ids, context=context):
413 if case.state in CRM_LEAD_PENDING_STATES:
414 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
415 res = self.write(cr, uid, [case.id], values, context=context)
418 def msg_send(self, cr, uid, id, *args, **argv):
421 @param self: The object pointer
422 @param cr: the current row, from the database cursor,
423 @param uid: the current user’s ID for security checks,
424 @param ids: List of email’s IDs
425 @param *args: Return Tuple Value
426 @param **args: Return Dictionary of Keyword Value
430 def on_change_optin(self, cr, uid, ids, optin):
431 return {'value':{'optin':optin,'optout':False}}
433 def on_change_optout(self, cr, uid, ids, optout):
434 return {'value':{'optout':optout,'optin':False}}
438 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: