b95d00a101c5eba973b82d45917800cdbf19a994
[odoo/odoo.git] / addons / crm / crm_lead.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 from osv import fields, osv
23 from datetime import datetime
24 import crm
25 import time
26 from tools.translate import _
27 from crm import crm_case
28 import binascii
29 import tools
30
31
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
36 )
37
38 class crm_lead(crm_case, osv.osv):
39     """ CRM Lead Case """
40     _name = "crm.lead"
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):
45         """
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
51         """
52         cal_obj = self.pool.get('resource.calendar')
53         res_obj = self.pool.get('resource.resource')
54
55         res = {}
56         for lead in self.browse(cr, uid, ids, context=context):
57             for field in fields:
58                 res[lead.id] = {}
59                 duration = 0
60                 ans = False
61                 if field == 'day_open':
62                     if lead.date_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':
68                     if lead.date_closed:
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
73                 if ans:
74                     resource_id = False
75                     if lead.user_id:
76                         resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
77                         if len(resource_ids):
78                             resource_id = resource_ids[0]
79
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,
84                             uid,
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'),
87                             duration,
88                             resource=resource_id
89                         )
90                         no_days = []
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:
96                                 break
97                         duration =  len(no_days)
98                 res[lead.id][field] = abs(int(duration))
99         return res
100
101     def _history_search(self, cr, uid, obj, name, args, context=None):
102         res = []
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)
106
107         if lead_ids:
108             return [('id', 'in', lead_ids)]
109         else:
110             return [('id', '=', '0')]
111
112     def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
113         res = {}
114         for obj in self.browse(cr, uid, ids, context=context):
115             res[obj.id] = ''
116             for msg in obj.message_ids:
117                 if msg.history:
118                     res[obj.id] = msg.subject
119                     break
120         return res
121
122     _columns = {
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"),
126
127         # From crm.case
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),
140
141         # Lead fields
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'),
147
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([
153             ('lead','Lead'),
154             ('opportunity','Opportunity'),
155
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),
174     }
175
176
177     _defaults = {
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,
187     }
188
189
190
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
199         """
200         if not add:
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}}
204
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
212         """
213         leads = self.browse(cr, uid, ids)
214
215
216
217         for i in xrange(0, len(ids)):
218             if leads[i].state == 'draft':
219                 value = {}
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)
227         return res
228
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
234         else:
235             message = _("The case '%s' has been opened.") % case.name
236         self.log(cr, uid, case.id, message)
237
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
245         """
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
253             else:
254                 message = _("The case '%s' has been closed.") % case.name
255             self.log(cr, uid, case.id, message)
256         return res
257
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
265         """
266         if context is None:
267             context = {}
268         context.update({'active_ids': ids})
269
270         data_obj = self.pool.get('ir.model.data')
271         value = {}
272
273         view_id = False
274
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')
278             view_id1 = False
279             if data_id:
280                 view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
281             value = {
282                     'name': _('Create Partner'),
283                     'view_type': 'form',
284                     'view_mode': 'form,tree',
285                     'res_model': 'crm.lead2opportunity.partner',
286                     'view_id': False,
287                     'context': context,
288                     'views': [(view_id1, 'form')],
289                     'type': 'ir.actions.act_window',
290                     'target': 'new',
291                     'nodestroy': True
292             }
293         return value
294
295     def write(self, cr, uid, ids, vals, context=None):
296         if not context:
297             context = {}
298
299         if 'date_closed' in vals:
300             return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
301
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)
305             message=''
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)
313
314     def stage_next(self, cr, uid, ids, context=None):
315         stage = super(crm_lead, self).stage_next(cr, uid, ids, context=context)
316         if stage:
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)
321         return stage
322
323     def stage_previous(self, cr, uid, ids, context=None):
324         stage = super(crm_lead, self).stage_previous(cr, uid, ids, context=context)
325         if stage:
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)
330         return stage
331
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)
338
339     def message_new(self, cr, uid, msg, context=None):
340         """
341         Automatically calls when new email message arrives
342
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
347         """
348         thread_pool = self.pool.get('email.thread')
349
350         subject = msg.get('subject')
351         body = msg.get('body')
352         msg_from = msg.get('from')
353         priority = msg.get('priority')
354
355         vals = {
356             'name': subject,
357             'email_from': msg_from,
358             'email_cc': msg.get('cc'),
359             'description': body,
360             'user_id': False,
361         }
362         if msg.get('priority', False):
363             vals['priority'] = priority
364
365         res = thread_pool.get_partner(cr, uid, msg.get('from', False))
366         if res:
367             vals.update(res)
368
369         res_id = self.create(cr, uid, vals, context)
370
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'),
386                             context = context)
387
388         return res_id
389
390     def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
391         """
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
396         """
397         if isinstance(ids, (str, int, long)):
398             ids = [ids]
399
400         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
401             vals['priority'] = msg.get('priority')
402
403         maps = {
404             'cost':'planned_cost',
405             'revenue': 'planned_revenue',
406             'probability':'probability'
407         }
408         vls = {}
409         for line in msg['body'].split('\n'):
410             line = line.strip()
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()
415         vals.update(vls)
416
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):
421             values = dict(vals)
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)
425
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'),
441                             context = context)
442         return res
443
444     def on_change_optin(self, cr, uid, ids, optin):
445         return {'value':{'optin':optin,'optout':False}}
446
447     def on_change_optout(self, cr, uid, ids, optout):
448         return {'value':{'optout':optout,'optin':False}}
449
450 crm_lead()
451
452 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: