fb3a8f5e2233e145fbc0cc3bebfe33f89a150dae
[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 = ['mailgate.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('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)
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.name
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('mailgate.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 message_new(self, cr, uid, msg, context=None):
333         """
334         Automatically calls when new email message arrives
335
336         @param self: The object pointer
337         @param cr: the current row, from the database cursor,
338         @param uid: the current user’s ID for security checks
339         """
340         mailgate_pool = self.pool.get('email.server.tools')
341
342         subject = msg.get('subject')
343         body = msg.get('body')
344         msg_from = msg.get('from')
345         priority = msg.get('priority')
346
347         vals = {
348             'name': subject,
349             'email_from': msg_from,
350             'email_cc': msg.get('cc'),
351             'description': body,
352             'user_id': False,
353         }
354         if msg.get('priority', False):
355             vals['priority'] = priority
356
357         res = mailgate_pool.get_partner(cr, uid, msg.get('from') or msg.get_unixfrom())
358         if res:
359             vals.update(res)
360
361         res = self.create(cr, uid, vals, context)
362         attachents = msg.get('attachments', [])
363         for attactment in attachents or []:
364             data_attach = {
365                 'name': attactment,
366                 'datas':binascii.b2a_base64(str(attachents.get(attactment))),
367                 'datas_fname': attactment,
368                 'description': 'Mail attachment',
369                 'res_model': self._name,
370                 'res_id': res,
371             }
372             self.pool.get('ir.attachment').create(cr, uid, data_attach)
373
374         return res
375
376     def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context=None):
377         """
378         @param self: The object pointer
379         @param cr: the current row, from the database cursor,
380         @param uid: the current user’s ID for security checks,
381         @param ids: List of update mail’s IDs
382         """
383         if isinstance(ids, (str, int, long)):
384             ids = [ids]
385
386         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
387             vals['priority'] = msg.get('priority')
388
389         maps = {
390             'cost':'planned_cost',
391             'revenue': 'planned_revenue',
392             'probability':'probability'
393         }
394         vls = {}
395         for line in msg['body'].split('\n'):
396             line = line.strip()
397             res = tools.misc.command_re.match(line)
398             if res and maps.get(res.group(1).lower()):
399                 key = maps.get(res.group(1).lower())
400                 vls[key] = res.group(2).lower()
401         vals.update(vls)
402
403         # Unfortunately the API is based on lists
404         # but we want to update the state based on the
405         # previous state, so we have to loop:
406         for case in self.browse(cr, uid, ids, context=context):
407             values = dict(vals)
408             if case.state in CRM_LEAD_PENDING_STATES:
409                 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
410             res = self.write(cr, uid, [case.id], values, context=context)
411         return res
412
413     def msg_send(self, cr, uid, id, *args, **argv):
414
415         """ Send The Message
416             @param self: The object pointer
417             @param cr: the current row, from the database cursor,
418             @param uid: the current user’s ID for security checks,
419             @param ids: List of email’s IDs
420             @param *args: Return Tuple Value
421             @param **args: Return Dictionary of Keyword Value
422         """
423         return True
424
425     def on_change_optin(self, cr, uid, ids, optin):
426         return {'value':{'optin':optin,'optout':False}}
427
428     def on_change_optout(self, cr, uid, ids, optout):
429         return {'value':{'optout':optout,'optin':False}}
430
431 crm_lead()
432
433 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: