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