[IMP/MERGE]:improved code
[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 = "priority,date_action,id desc"
43     _inherit = ['mail.thread','res.partner.address']
44
45     # overridden because res.partner.address has an inconvenient name_get,
46     # especially if base_contact is installed.
47     def name_get(self, cr, user, ids, context=None):
48         if isinstance(ids, (int, long)):
49             ids = [ids]
50         return [(r['id'], tools.ustr(r[self._rec_name]))
51                     for r in self.read(cr, user, ids, [self._rec_name], context)]
52
53     def _compute_day(self, cr, uid, ids, fields, args, context=None):
54         """
55         @param cr: the current row, from the database cursor,
56         @param uid: the current user’s ID for security checks,
57         @param ids: List of Openday’s IDs
58         @return: difference between current date and log date
59         @param context: A standard dictionary for contextual values
60         """
61         cal_obj = self.pool.get('resource.calendar')
62         res_obj = self.pool.get('resource.resource')
63
64         res = {}
65         for lead in self.browse(cr, uid, ids, context=context):
66             for field in fields:
67                 res[lead.id] = {}
68                 duration = 0
69                 ans = False
70                 if field == 'day_open':
71                     if lead.date_open:
72                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
73                         date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
74                         ans = date_open - date_create
75                         date_until = lead.date_open
76                 elif field == 'day_close':
77                     if lead.date_closed:
78                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
79                         date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
80                         date_until = lead.date_closed
81                         ans = date_close - date_create
82                 if ans:
83                     resource_id = False
84                     if lead.user_id:
85                         resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
86                         if len(resource_ids):
87                             resource_id = resource_ids[0]
88
89                     duration = float(ans.days)
90                     if lead.section_id and lead.section_id.resource_calendar_id:
91                         duration =  float(ans.days) * 24
92                         new_dates = cal_obj.interval_get(cr,
93                             uid,
94                             lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
95                             datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
96                             duration,
97                             resource=resource_id
98                         )
99                         no_days = []
100                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
101                         for in_time, out_time in new_dates:
102                             if in_time.date not in no_days:
103                                 no_days.append(in_time.date)
104                             if out_time > date_until:
105                                 break
106                         duration =  len(no_days)
107                 res[lead.id][field] = abs(int(duration))
108         return res
109
110     def _history_search(self, cr, uid, obj, name, args, context=None):
111         res = []
112         msg_obj = self.pool.get('mail.message')
113         message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
114         lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
115
116         if lead_ids:
117             return [('id', 'in', lead_ids)]
118         else:
119             return [('id', '=', '0')]
120
121     def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
122         res = {}
123         for obj in self.browse(cr, uid, ids, context=context):
124             res[obj.id] = ''
125             for msg in obj.message_ids:
126                 if msg.email_from:
127                     res[obj.id] = msg.subject
128                     break
129         return res
130     
131     def _get_stage_id(self, cr, uid, *args):
132         model_data = self.pool.get('ir.model.data')
133         res = model_data.search(cr,uid, [('name', '=', 'stage_lead1')])
134         if res:return model_data.browse(cr,uid,res[0]).res_id
135         return False
136
137     _columns = {
138         # Overridden from res.partner.address:
139         'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
140             select=True, help="Optional linked partner, usually after conversion of the lead"),
141
142         'id': fields.integer('ID', readonly=True),
143         'name': fields.char('Name', size=64, select=1),
144         'active': fields.boolean('Active', required=False),
145         'date_action_last': fields.datetime('Last Action', readonly=1),
146         'date_action_next': fields.datetime('Next Action', readonly=1),
147         'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
148         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
149                         select=True, help='When sending mails, the default email address is taken from the sales team.'),
150         'create_date': fields.datetime('Creation Date' , readonly=True),
151         '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"),
152         'description': fields.text('Notes'),
153         'write_date': fields.datetime('Update Date' , readonly=True),
154
155         'categ_id': fields.many2one('crm.case.categ', 'Category', \
156             domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
157         'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
158             domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
159         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
160         'contact_name': fields.char('Contact Name', size=64),
161         'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner that will be created while converting the into opportunity', select=1),
162         'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
163         'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
164         'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
165         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
166         'date_closed': fields.datetime('Closed', readonly=True),
167         'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
168         'user_id': fields.many2one('res.users', 'Salesman'),
169         'referred': fields.char('Referred By', size=64),
170         'date_open': fields.datetime('Opened', readonly=True),
171         'day_open': fields.function(_compute_day, string='Days to Open', \
172                                 multi='day_open', type="float", store=True),
173         'day_close': fields.function(_compute_day, string='Days to Close', \
174                                 multi='day_close', type="float", store=True),
175         'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
176                                   help='The state is set to \'Draft\', when a case is created.\
177                                   \nIf the case is in progress the state is set to \'Open\'.\
178                                   \nWhen the case is over, the state is set to \'Done\'.\
179                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
180         'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
181         'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
182
183
184         # Only used for type opportunity
185         'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"), 
186         'probability': fields.float('Probability (%)',group_operator="avg"),
187         'planned_revenue': fields.float('Expected Revenue'),
188         'ref': fields.reference('Reference', selection=crm._links_get, size=128),
189         'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
190         'phone': fields.char("Phone", size=64),
191         'date_deadline': fields.date('Expected Closing'),
192         'date_action': fields.date('Next Action Date'),
193         'title_action': fields.char('Next Action', size=64),
194         'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"),
195         'color': fields.integer('Color Index'),
196         'partner_address_name': fields.related('partner_address_id', 'name', type='char', string='Partner Contact Name', readonly=True),
197         'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
198         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
199         'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
200
201     }
202
203     _defaults = {
204         'active': lambda *a: 1,
205         'user_id': crm_case._get_default_user,
206         'email_from': crm_case._get_default_email,
207         'state': lambda *a: 'draft',
208         'type': lambda *a: 'lead',
209         'section_id': crm_case._get_section,
210         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
211         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
212         'color': 0,
213         'stage_id': _get_stage_id,
214     }
215
216     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
217         """This function returns value of partner email based on Partner Address
218         """
219         if not add:
220             return {'value': {'email_from': False, 'country_id': False}}
221         address = self.pool.get('res.partner.address').browse(cr, uid, add)
222         return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
223
224     def on_change_optin(self, cr, uid, ids, optin):
225         return {'value':{'optin':optin,'optout':False}}
226
227     def on_change_optout(self, cr, uid, ids, optout):
228         return {'value':{'optout':optout,'optin':False}}
229
230     def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
231         if not stage_id:
232             return {'value':{}}
233         stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
234         if not stage.on_change:
235             return {'value':{}}
236         return {'value':{'probability': stage.probability}}
237
238     def stage_find_percent(self, cr, uid, percent, section_id):
239         """ Return the first stage with a probability == percent
240         """
241         stage_pool = self.pool.get('crm.case.stage')
242         if section_id :
243             ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
244         else :
245             ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
246
247         if ids:
248             return ids[0]
249         return False
250
251     def stage_find_lost(self, cr, uid, section_id):
252         return self.stage_find_percent(cr, uid, 0.0, section_id)
253
254     def stage_find_won(self, cr, uid, section_id):
255         return self.stage_find_percent(cr, uid, 100.0, section_id)
256
257     def case_open(self, cr, uid, ids, *args):
258         for l in self.browse(cr, uid, ids):
259             # When coming from draft override date and stage otherwise just set state
260             if l.state == 'draft':
261                 if l.type == 'lead':
262                     message = _("The lead '%s' has been opened.") % l.name
263                 elif l.type == 'opportunity':
264                     message = _("The opportunity '%s' has been opened.") % l.name
265                 else:
266                     message = _("The case '%s' has been opened.") % l.name
267                 self.log(cr, uid, l.id, message)
268                 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
269                 self.write(cr, uid, [l.id], value)
270                 if l.type == 'opportunity' and not l.stage_id:
271                     stage_id = self.stage_find(cr, uid, l.section_id.id or False, [('sequence','>',0)])
272                     if stage_id:
273                         self.stage_set(cr, uid, [l.id], stage_id)
274         res = super(crm_lead, self).case_open(cr, uid, ids, *args)
275         return res
276
277     def case_close(self, cr, uid, ids, *args):
278         res = super(crm_lead, self).case_close(cr, uid, ids, *args)
279         self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
280         for case in self.browse(cr, uid, ids):
281             if case.type == 'lead':
282                 message = _("The lead '%s' has been closed.") % case.name
283             else:
284                 message = _("The case '%s' has been closed.") % case.name
285             self.log(cr, uid, case.id, message)
286         return res
287
288     def case_cancel(self, cr, uid, ids, *args):
289         """Overrides cancel for crm_case for setting probability
290         """
291         res = super(crm_lead, self).case_cancel(cr, uid, ids, args)
292         self.write(cr, uid, ids, {'probability' : 0.0})
293         return res
294
295     def case_reset(self, cr, uid, ids, *args):
296         """Overrides reset as draft in order to set the stage field as empty
297         """
298         res = super(crm_lead, self).case_reset(cr, uid, ids, *args)
299         self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
300         return res
301
302     def case_mark_lost(self, cr, uid, ids, *args):
303         """Mark the case as lost: state = done and probability = 0%
304         """
305         res = super(crm_lead, self).case_close(cr, uid, ids, *args)
306         self.write(cr, uid, ids, {'probability' : 0.0})
307         for l in self.browse(cr, uid, ids):
308             stage_id = self.stage_find_lost(cr, uid, l.section_id.id or False)
309             if stage_id:
310                 self.stage_set(cr, uid, [l.id], stage_id)
311             message = _("The opportunity '%s' has been marked as lost.") % l.name
312             self.log(cr, uid, l.id, message)
313         return res
314
315     def case_mark_won(self, cr, uid, ids, *args):
316         """Mark the case as lost: state = done and probability = 0%
317         """
318         res = super(crm_lead, self).case_close(cr, uid, ids, *args)
319         self.write(cr, uid, ids, {'probability' : 100.0})
320         for l in self.browse(cr, uid, ids):
321             stage_id = self.stage_find_won(cr, uid, l.section_id.id or False)
322             if stage_id:
323                 self.stage_set(cr, uid, [l.id], stage_id)
324             message = _("The opportunity '%s' has been been won.") % l.name
325             self.log(cr, uid, l.id, message)
326         return res
327
328     def set_priority(self, cr, uid, ids, priority):
329         """Set lead priority
330         """
331         return self.write(cr, uid, ids, {'priority' : priority})
332
333     def set_high_priority(self, cr, uid, ids, *args):
334         """Set lead priority to high
335         """
336         return self.set_priority(cr, uid, ids, '1')
337
338     def set_normal_priority(self, cr, uid, ids, *args):
339         """Set lead priority to normal
340         """
341         return self.set_priority(cr, uid, ids, '3')
342
343     def convert_opportunity(self, cr, uid, ids, context=None):
344         """ Precomputation for converting lead to opportunity
345         """
346         if context is None:
347             context = {}
348         context.update({'active_ids': ids})
349
350         data_obj = self.pool.get('ir.model.data')
351         value = {}
352
353
354         for case in self.browse(cr, uid, ids, context=context):
355             context.update({'active_id': case.id})
356             data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
357             view_id1 = False
358             if data_id:
359                 view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
360             value = {
361                     'name': _('Create Partner'),
362                     'view_type': 'form',
363                     'view_mode': 'form,tree',
364                     'res_model': 'crm.lead2opportunity.partner',
365                     'view_id': False,
366                     'context': context,
367                     'views': [(view_id1, 'form')],
368                     'type': 'ir.actions.act_window',
369                     'target': 'new',
370                     'nodestroy': True
371             }
372         return value
373
374     def message_new(self, cr, uid, msg, custom_values=None, context=None):
375         """Automatically calls when new email message arrives"""
376         res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
377         subject = msg.get('subject')  or _("No Subject")
378         body = msg.get('body_text')
379
380         msg_from = msg.get('from')
381         priority = msg.get('priority')
382         vals = {
383             'name': subject,
384             'email_from': msg_from,
385             'email_cc': msg.get('cc'),
386             'description': body,
387             'user_id': False,
388         }
389         if priority:
390             vals['priority'] = priority
391         vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
392         self.write(cr, uid, [res_id], vals, context)
393         return res_id
394
395     def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
396         if isinstance(ids, (str, int, long)):
397             ids = [ids]
398
399         super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
400
401         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
402             vals['priority'] = msg.get('priority')
403         maps = {
404             'cost':'planned_cost',
405             'revenue': 'planned_revenue',
406             'probability':'probability'
407         }
408         vls = {}
409         for line in msg['body_text'].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         return res
426
427     def action_makeMeeting(self, cr, uid, ids, context=None):
428         """
429         This opens Meeting's calendar view to schedule meeting on current Opportunity
430         @return : Dictionary value for created Meeting view
431         """
432         value = {}
433         for opp in self.browse(cr, uid, ids, context=context):
434             data_obj = self.pool.get('ir.model.data')
435
436             # Get meeting views
437             result = data_obj._get_id(cr, uid, 'crm', 'view_crm_case_meetings_filter')
438             res = data_obj.read(cr, uid, result, ['res_id'])
439             id1 = data_obj._get_id(cr, uid, 'crm', 'crm_case_calendar_view_meet')
440             id2 = data_obj._get_id(cr, uid, 'crm', 'crm_case_form_view_meet')
441             id3 = data_obj._get_id(cr, uid, 'crm', 'crm_case_tree_view_meet')
442             if id1:
443                 id1 = data_obj.browse(cr, uid, id1, context=context).res_id
444             if id2:
445                 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
446             if id3:
447                 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
448
449             context = {
450                 'default_opportunity_id': opp.id,
451                 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
452                 'default_user_id': uid, 
453                 'default_section_id': opp.section_id and opp.section_id.id or False,
454                 'default_email_from': opp.email_from,
455                 'default_state': 'open',  
456                 'default_name': opp.name
457             }
458             value = {
459                 'name': _('Meetings'),
460                 'context': context,
461                 'view_type': 'form',
462                 'view_mode': 'calendar,form,tree',
463                 'res_model': 'crm.meeting',
464                 'view_id': False,
465                 'views': [(id1, 'calendar'), (id2, 'form'), (id3, 'tree')],
466                 'type': 'ir.actions.act_window',
467                 'search_view_id': res['res_id'],
468                 'nodestroy': True
469             }
470         return value
471
472
473     def unlink(self, cr, uid, ids, context=None):
474         for lead in self.browse(cr, uid, ids, context):
475             if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
476                 raise osv.except_osv(_('Warning !'),
477                     _('You can not delete this lead. You should better cancel it.'))
478         return super(crm_lead, self).unlink(cr, uid, ids, context)
479
480
481     def write(self, cr, uid, ids, vals, context=None):
482         if not context:
483             context = {}
484
485         if 'date_closed' in vals:
486             return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
487
488         if 'stage_id' in vals and vals['stage_id']:
489             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
490             text = _("Changed Stage to: %s") % stage_obj.name
491             self.message_append(cr, uid, ids, text, body_text=text, context=context)
492             message=''
493             for case in self.browse(cr, uid, ids, context=context):
494                 if case.type == 'lead' or  context.get('stage_type',False)=='lead':
495                     message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
496                 elif case.type == 'opportunity':
497                     message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
498                 self.log(cr, uid, case.id, message)
499         return super(crm_lead,self).write(cr, uid, ids, vals, context)
500
501 crm_lead()
502
503 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: