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