[IMP] crm,crm_claim,crm_helpdesk,hr_recruitment,project_issue,project_mailgate: impro...
[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     }
174
175
176     _defaults = {
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,
186     }
187
188
189
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
198         """
199         if not add:
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}}
203
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
211         """
212         leads = self.browse(cr, uid, ids)
213
214
215
216         for i in xrange(0, len(ids)):
217             if leads[i].state == 'draft':
218                 value = {}
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)
226         return res
227
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
233         else:
234             message = _("The case '%s' has been opened.") % case.name
235         self.log(cr, uid, case.id, message)
236
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
244         """
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
252             else:
253                 message = _("The case '%s' has been closed.") % case.name
254             self.log(cr, uid, case.id, message)
255         return res
256
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
264         """
265         if context is None:
266             context = {}
267         context.update({'active_ids': ids})
268
269         data_obj = self.pool.get('ir.model.data')
270         value = {}
271
272         view_id = False
273
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')
277             view_id1 = False
278             if data_id:
279                 view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
280             value = {
281                     'name': _('Create Partner'),
282                     'view_type': 'form',
283                     'view_mode': 'form,tree',
284                     'res_model': 'crm.lead2opportunity.partner',
285                     'view_id': False,
286                     'context': context,
287                     'views': [(view_id1, 'form')],
288                     'type': 'ir.actions.act_window',
289                     'target': 'new',
290                     'nodestroy': True
291             }
292         return value
293
294     def write(self, cr, uid, ids, vals, context=None):
295         if not context:
296             context = {}
297
298         if 'date_closed' in vals:
299             return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
300
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)
304             message=''
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)
312
313     def stage_next(self, cr, uid, ids, context=None):
314         stage = super(crm_lead, self).stage_next(cr, uid, ids, context=context)
315         if stage:
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)
320         return stage
321
322     def stage_previous(self, cr, uid, ids, context=None):
323         stage = super(crm_lead, self).stage_previous(cr, uid, ids, context=context)
324         if stage:
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)
329         return stage
330
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)
337
338     def message_new(self, cr, uid, msg, context=None):
339         """
340         Automatically calls when new email message arrives
341
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
346         """
347         thread_pool = self.pool.get('email.thread')
348
349         subject = msg.get('subject')
350         body = msg.get('body')
351         msg_from = msg.get('from')
352         priority = msg.get('priority')
353
354         vals = {
355             'name': subject,
356             'email_from': msg_from,
357             'email_cc': msg.get('cc'),
358             'description': body,
359             'user_id': False,
360         }
361         if msg.get('priority', False):
362             vals['priority'] = priority
363
364         res = thread_pool.get_partner(cr, uid, msg.get('from', False))
365         if res:
366             vals.update(res)
367
368         res_id = self.create(cr, uid, vals, context)
369
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),
385                             context = context)
386
387         return res_id
388
389     def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
390         """
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
395         """
396         if isinstance(ids, (str, int, long)):
397             ids = [ids]
398
399         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
400             vals['priority'] = msg.get('priority')
401
402         maps = {
403             'cost':'planned_cost',
404             'revenue': 'planned_revenue',
405             'probability':'probability'
406         }
407         vls = {}
408         for line in msg['body'].split('\n'):
409             line = line.strip()
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()
414         vals.update(vls)
415
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):
420             values = dict(vals)
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)
424
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'),
436                             context = context)
437         return res
438
439     def on_change_optin(self, cr, uid, ids, optin):
440         return {'value':{'optin':optin,'optout':False}}
441
442     def on_change_optout(self, cr, uid, ids, optout):
443         return {'value':{'optout':optout,'optin':False}}
444
445 crm_lead()
446
447 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: