[FIX] remove security rule of osv_memory object
[odoo/odoo.git] / addons / crm / crm.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 import time
23 from datetime import datetime
24 from datetime import timedelta
25 import base64
26 import tools
27 from osv import fields
28 from osv import osv
29 from tools.translate import _
30
31 MAX_LEVEL = 15
32 AVAILABLE_STATES = [
33     ('draft', 'Draft'),
34     ('open', 'Open'),
35     ('cancel', 'Cancelled'),
36     ('done', 'Closed'),
37     ('pending', 'Pending'),
38 ]
39
40 AVAILABLE_PRIORITIES = [
41     ('1', 'Highest'),
42     ('2', 'High'),
43     ('3', 'Normal'),
44     ('4', 'Low'),
45     ('5', 'Lowest'),
46 ]
47
48 class crm_case(object):
49     """A simple python class to be used for common functions """
50
51     def _get_default_partner_address(self, cr, uid, context):
52         """Gives id of default address for current user
53         @param self: The object pointer
54         @param cr: the current row, from the database cursor,
55         @param uid: the current user’s ID for security checks,
56         @param context: A standard dictionary for contextual values
57         """
58         if not context.get('portal', False):
59             return False
60         return self.pool.get('res.users').browse(cr, uid, uid, context).address_id.id
61
62     def _get_default_partner(self, cr, uid, context):
63         """Gives id of partner for current user
64         @param self: The object pointer
65         @param cr: the current row, from the database cursor,
66         @param uid: the current user’s ID for security checks,
67         @param context: A standard dictionary for contextual values
68         """
69         if not context.get('portal', False):
70             return False
71         user = self.pool.get('res.users').browse(cr, uid, uid, context)
72         if not user.address_id:
73             return False
74         return user.address_id.partner_id.id
75
76     def _get_default_email(self, cr, uid, context):
77         """Gives default email address for current user
78         @param self: The object pointer
79         @param cr: the current row, from the database cursor,
80         @param uid: the current user’s ID for security checks,
81         @param context: A standard dictionary for contextual values
82         """
83         if not context.get('portal', False):
84             return False
85         user = self.pool.get('res.users').browse(cr, uid, uid, context)
86         if not user.address_id:
87             return False
88         return user.address_id.email
89
90     def _get_default_user(self, cr, uid, context):
91         """Gives current user id
92         @param self: The object pointer
93         @param cr: the current row, from the database cursor,
94         @param uid: the current user’s ID for security checks,
95         @param context: A standard dictionary for contextual values
96         """
97         if context.get('portal', False):
98             return False
99         return uid
100
101     def _get_section(self, cr, uid, context):
102         """Gives section id for current User
103         @param self: The object pointer
104         @param cr: the current row, from the database cursor,
105         @param uid: the current user’s ID for security checks,
106         @param context: A standard dictionary for contextual values
107         """
108         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
109         return user.context_section_id.id or False
110
111     def stage_next(self, cr, uid, ids, context=None):
112         """This function computes next stage for case from its current stage
113              using available stage for that case type
114         @param self: The object pointer
115         @param cr: the current row, from the database cursor,
116         @param uid: the current user’s ID for security checks,
117         @param ids: List of case IDs
118         @param context: A standard dictionary for contextual values"""
119         if not context:
120             context = {}
121         s = self.get_stage_dict(cr, uid, ids, context=context)
122         section = self._name
123         stage = False
124         stage_pool = self.pool.get('crm.case.stage')
125         for case in self.browse(cr, uid, ids, context):
126             if section in s:
127                 st = case.stage_id.id  or False
128                 if st in s[section]:
129                     data = {'stage_id': s[section][st]}
130                     stage = s[section][st]
131                     self.write(cr, uid, [case.id], data)
132         return stage
133
134     def get_stage_dict(self, cr, uid, ids, context=None):
135         """This function gives dictionary for stage according to stage levels
136         @param self: The object pointer
137         @param cr: the current row, from the database cursor,
138         @param uid: the current user’s ID for security checks,
139         @param ids: List of case IDs
140         @param context: A standard dictionary for contextual values"""
141         if not context:
142             context = {}
143         stage_obj = self.pool.get('crm.case.stage')
144         sid = stage_obj.search(cr, uid, \
145                             [('object_id.model', '=', self._name)], context=context)
146         s = {}
147         previous = {}
148         section = self._name
149         for stage in stage_obj.browse(cr, uid, sid, context=context):
150             s.setdefault(section, {})
151             s[section][previous.get(section, False)] = stage.id
152             previous[section] = stage.id
153         return s
154
155     def stage_previous(self, cr, uid, ids, context=None):
156         """This function computes previous stage for case from its current stage
157              using available stage for that case type
158         @param self: The object pointer
159         @param cr: the current row, from the database cursor,
160         @param uid: the current user’s ID for security checks,
161         @param ids: List of case IDs
162         @param context: A standard dictionary for contextual values"""
163         if not context:
164             context = {}
165
166         s = self.get_stage_dict(cr, uid, ids, context=context)
167         section = self._name
168         stage_pool = self.pool.get('crm.case.stage')
169         for case in self.browse(cr, uid, ids, context):
170             if section in s:
171                 st = case.stage_id.id or False
172                 s[section] = dict([(v, k) for (k, v) in s[section].iteritems()])
173                 if st in s[section]:
174                     data = {'stage_id': s[section][st]}
175                     if s[section][st]:
176                         stage = stage_pool.browse(cr, uid, s[section][st], context=context)
177                         if stage.on_change:
178                             data.update({'probability': stage.probability})
179                     self.write(cr, uid, [case.id], data)
180         return True
181
182     def onchange_partner_id(self, cr, uid, ids, part, email=False):
183         """This function returns value of partner address based on partner
184         @param self: The object pointer
185         @param cr: the current row, from the database cursor,
186         @param uid: the current user’s ID for security checks,
187         @param ids: List of case IDs
188         @param part: Partner's id
189         @email: Partner's email ID
190         """
191         if not part:
192             return {'value': {'partner_address_id': False,
193                             'email_from': False, 
194                             'phone': False
195                             }}
196         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
197         data = {'partner_address_id': addr['contact']}
198         data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value'])
199         return {'value': data}
200
201     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
202         """This function returns value of partner email based on Partner Address
203         @param self: The object pointer
204         @param cr: the current row, from the database cursor,
205         @param uid: the current user’s ID for security checks,
206         @param ids: List of case IDs
207         @param add: Id of Partner's address
208         @email: Partner's email ID
209         """
210         if not add:
211             return {'value': {'email_from': False}}
212         address = self.pool.get('res.partner.address').browse(cr, uid, add)
213         return {'value': {'email_from': address.email, 'phone': address.phone}}
214
215     def _history(self, cr, uid, cases, keyword, history=False, subject=None, email=False, details=None, email_from=False, message_id=False, attach=[], context={}):
216         mailgate_pool = self.pool.get('mailgate.thread')
217         return mailgate_pool.history(cr, uid, cases, keyword, history=history,\
218                                        subject=subject, email=email, \
219                                        details=details, email_from=email_from,\
220                                        message_id=message_id, attach=attach, \
221                                        context=context)
222
223     def case_open(self, cr, uid, ids, *args):
224         """Opens Case
225         @param self: The object pointer
226         @param cr: the current row, from the database cursor,
227         @param uid: the current user’s ID for security checks,
228         @param ids: List of case Ids
229         @param *args: Tuple Value for additional Params
230         """
231         cases = self.browse(cr, uid, ids)
232         self._history(cr, uid, cases, _('Open'))
233         for case in cases:
234             data = {'state': 'open', 'active': True}
235             if not case.user_id:
236                 data['user_id'] = uid
237             self.write(cr, uid, case.id, data)
238         self._action(cr, uid, cases, 'open')
239         return True
240
241     def case_close(self, cr, uid, ids, *args):
242         """Closes Case
243         @param self: The object pointer
244         @param cr: the current row, from the database cursor,
245         @param uid: the current user’s ID for security checks,
246         @param ids: List of case Ids
247         @param *args: Tuple Value for additional Params
248         """
249         cases = self.browse(cr, uid, ids)
250         cases[0].state # to fill the browse record cache
251         self._history(cr, uid, cases, _('Close'))
252         self.write(cr, uid, ids, {'state': 'done',
253                                   'date_closed': time.strftime('%Y-%m-%d %H:%M:%S'),
254                                   })
255         #
256         # We use the cache of cases to keep the old case state
257         #
258         self._action(cr, uid, cases, 'done')
259         return True
260
261     def case_escalate(self, cr, uid, ids, *args):
262         """Escalates case to top level
263         @param self: The object pointer
264         @param cr: the current row, from the database cursor,
265         @param uid: the current user’s ID for security checks,
266         @param ids: List of case Ids
267         @param *args: Tuple Value for additional Params
268         """
269         cases = self.browse(cr, uid, ids)
270         for case in cases:
271             data = {'active': True, 'user_id': False}
272
273             if case.section_id.parent_id:
274                 data['section_id'] = case.section_id.parent_id.id
275                 if case.section_id.parent_id.user_id:
276                     data['user_id'] = case.section_id.parent_id.user_id.id
277             else:
278                 raise osv.except_osv(_('Error !'), _('You can not escalate, You are already at the top level regarding your sales-team category.'))
279             self.write(cr, uid, [case.id], data)
280         cases = self.browse(cr, uid, ids)
281         self._history(cr, uid, cases, _('Escalate'))
282         self._action(cr, uid, cases, 'escalate')
283         return True
284
285     def case_cancel(self, cr, uid, ids, *args):
286         """Cancels Case
287         @param self: The object pointer
288         @param cr: the current row, from the database cursor,
289         @param uid: the current user’s ID for security checks,
290         @param ids: List of case Ids
291         @param *args: Tuple Value for additional Params
292         """
293         cases = self.browse(cr, uid, ids)
294         cases[0].state # to fill the browse record cache
295         self._history(cr, uid, cases, _('Cancel'))
296         self.write(cr, uid, ids, {'state': 'cancel',
297                                   'active': True})
298         self._action(cr, uid, cases, 'cancel')
299         return True
300
301     def case_pending(self, cr, uid, ids, *args):
302         """Marks case as pending
303         @param self: The object pointer
304         @param cr: the current row, from the database cursor,
305         @param uid: the current user’s ID for security checks,
306         @param ids: List of case Ids
307         @param *args: Tuple Value for additional Params
308         """
309         cases = self.browse(cr, uid, ids)
310         cases[0].state # to fill the browse record cache
311         self._history(cr, uid, cases, _('Pending'))
312         self.write(cr, uid, ids, {'state': 'pending', 'active': True})
313         self._action(cr, uid, cases, 'pending')
314         return True
315
316     def case_reset(self, cr, uid, ids, *args):
317         """Resets case as draft
318         @param self: The object pointer
319         @param cr: the current row, from the database cursor,
320         @param uid: the current user’s ID for security checks,
321         @param ids: List of case Ids
322         @param *args: Tuple Value for additional Params
323         """
324         cases = self.browse(cr, uid, ids)
325         cases[0].state # to fill the browse record cache
326         self._history(cr, uid, cases, _('Draft'))
327         self.write(cr, uid, ids, {'state': 'draft', 'active': True})
328         self._action(cr, uid, cases, 'draft')
329         return True
330
331     def remind_partner(self, cr, uid, ids, context={}, attach=False):
332
333         """
334         @param self: The object pointer
335         @param cr: the current row, from the database cursor,
336         @param uid: the current user’s ID for security checks,
337         @param ids: List of Remind Partner's IDs
338         @param context: A standard dictionary for contextual values
339
340         """
341         return self.remind_user(cr, uid, ids, context, attach,
342                 destination=False)
343
344     def remind_user(self, cr, uid, ids, context={}, attach=False,destination=True):
345         """
346         @param self: The object pointer
347         @param cr: the current row, from the database cursor,
348         @param uid: the current user’s ID for security checks,
349         @param ids: List of Remind user's IDs
350         @param context: A standard dictionary for contextual values
351
352         """
353         for case in self.browse(cr, uid, ids):
354             if not case.section_id.reply_to:
355                 raise osv.except_osv(_('Error!'), ("Reply To is not specified in the sales team"))
356             if not case.email_from:
357                 raise osv.except_osv(_('Error!'), ("Partner Email is not specified in Case"))
358             if case.section_id.reply_to and case.email_from:
359                 src = case.email_from
360                 dest = case.section_id.reply_to
361                 body = case.description or ""
362                 if case.message_ids:
363                     body = case.message_ids[0].description or ""
364                 if not destination:
365                     src, dest = dest, src
366                     if body and case.user_id.signature:
367                         body += '\n\n%s' % (case.user_id.signature)
368
369                 body = self.format_body(body)
370
371                 attach_to_send = None
372
373                 if attach:
374                     attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', case.id)])
375                     attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname','datas'])
376                     attach_to_send = map(lambda x: (x['datas_fname'], base64.decodestring(x['datas'])), attach_to_send)
377
378                 # Send an email
379                 subject = "Reminder: [%s] %s" % (str(case.id), case.name, )
380                 flag = tools.email_send(
381                     src,
382                     [dest],
383                     subject, 
384                     body,
385                     reply_to=case.section_id.reply_to,
386                     openobject_id=str(case.id),
387                     attach=attach_to_send
388                 )
389                 self._history(cr, uid, [case], _('Send'), history=True, subject=subject, email=dest, details=body, email_from=src)
390         return True
391
392     def _check(self, cr, uid, ids=False, context={}):
393         """
394         Function called by the scheduler to process cases for date actions
395         Only works on not done and cancelled cases
396
397         @param self: The object pointer
398         @param cr: the current row, from the database cursor,
399         @param uid: the current user’s ID for security checks,
400         @param context: A standard dictionary for contextual values
401         """
402         cr.execute('select * from crm_case \
403                 where (date_action_last<%s or date_action_last is null) \
404                 and (date_action_next<=%s or date_action_next is null) \
405                 and state not in (\'cancel\',\'done\')',
406                 (time.strftime("%Y-%m-%d %H:%M:%S"),
407                     time.strftime('%Y-%m-%d %H:%M:%S')))
408
409         ids2 = map(lambda x: x[0], cr.fetchall() or [])
410         cases = self.browse(cr, uid, ids2, context)
411         return self._action(cr, uid, cases, False, context=context)
412
413     def _action(self, cr, uid, cases, state_to, scrit=None, context={}):
414         if not context:
415             context = {}
416         context['state_to'] = state_to
417         rule_obj = self.pool.get('base.action.rule')
418         model_obj = self.pool.get('ir.model')
419         model_ids = model_obj.search(cr, uid, [('model','=',self._name)])
420         rule_ids = rule_obj.search(cr, uid, [('name','=',model_ids[0])])
421         return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context)
422
423     def format_body(self, body):
424         return self.pool.get('base.action.rule').format_body(body)
425
426     def format_mail(self, obj, body):
427         return self.pool.get('base.action.rule').format_mail(obj, body)
428
429 class crm_case_section(osv.osv):
430     """Sales Team"""
431
432     _name = "crm.case.section"
433     _description = "Sales Teams"
434     _order = "name"
435
436     _columns = {
437         'name': fields.char('Sales Team', size=64, required=True, translate=True),
438         'code': fields.char('Code', size=8),
439         'active': fields.boolean('Active', help="If the active field is set to \
440                         true, it will allow you to hide the sales team without removing it."),
441         'allow_unlink': fields.boolean('Allow Delete', help="Allows to delete non draft cases"),
442         'user_id': fields.many2one('res.users', 'Responsible User'),
443         'member_ids':fields.many2many('res.users', 'sale_member_rel', 'section_id', 'member_id', 'Team Members'),
444         'reply_to': fields.char('Reply-To', size=64, help="The email address put \
445                         in the 'Reply-To' of all emails sent by Open ERP about cases in this sales team"),
446         'parent_id': fields.many2one('crm.case.section', 'Parent Team'),
447         'child_ids': fields.one2many('crm.case.section', 'parent_id', 'Child Teams'),
448         'resource_calendar_id': fields.many2one('resource.calendar', "Resource's Calendar"),
449         'note': fields.text('Description'),
450         'working_hours': fields.float('Working Hours', digits=(16,2 )),
451     }
452
453     _defaults = {
454         'active': lambda *a: 1,
455         'allow_unlink': lambda *a: 1,
456     }
457
458     _sql_constraints = [
459         ('code_uniq', 'unique (code)', 'The code of the sales team must be unique !')
460     ]
461
462     def _check_recursion(self, cr, uid, ids):
463
464         """
465         Checks for recursion level for sales team
466         @param self: The object pointer
467         @param cr: the current row, from the database cursor,
468         @param uid: the current user’s ID for security checks,
469         @param ids: List of Sales team ids
470         """
471         level = 100
472
473         while len(ids):
474             cr.execute('select distinct parent_id from crm_case_section where id IN %s', (tuple(ids),))
475             ids = filter(None, map(lambda x: x[0], cr.fetchall()))
476             if not level:
477                 return False
478             level -= 1
479
480         return True
481
482     _constraints = [
483         (_check_recursion, 'Error ! You cannot create recursive Sales team.', ['parent_id'])
484     ]
485
486     def name_get(self, cr, uid, ids, context=None):
487         """Overrides orm name_get method
488         @param self: The object pointer
489         @param cr: the current row, from the database cursor,
490         @param uid: the current user’s ID for security checks,
491         @param ids: List of sales team ids
492         """
493         if not context:
494             context = {}
495
496         res = []
497         if not ids:
498             return res
499         reads = self.read(cr, uid, ids, ['name', 'parent_id'], context)
500
501         for record in reads:
502             name = record['name']
503             if record['parent_id']:
504                 name = record['parent_id'][1] + ' / ' + name
505             res.append((record['id'], name))
506         return res
507
508 crm_case_section()
509
510
511 class crm_case_categ(osv.osv):
512     """ Category of Case """
513
514     _name = "crm.case.categ"
515     _description = "Category of case"
516
517     _columns = {
518         'name': fields.char('Case Category Name', size=64, required=True, translate=True),
519         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
520         'object_id': fields.many2one('ir.model', 'Object Name'),
521     }
522
523     def _find_object_id(self, cr, uid, context=None):
524         """Finds id for case object
525         @param self: The object pointer
526         @param cr: the current row, from the database cursor,
527         @param uid: the current user’s ID for security checks,
528         @param context: A standard dictionary for contextual values
529         """
530
531         object_id = context and context.get('object_id', False) or False
532         ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
533         return ids and ids[0]
534
535     _defaults = {
536         'object_id' : _find_object_id
537
538     }
539 crm_case_categ()
540
541
542 class crm_case_resource_type(osv.osv):
543     """ Resource Type of case """
544
545     _name = "crm.case.resource.type"
546     _description = "Resource Type of case"
547     _rec_name = "name"
548
549     _columns = {
550         'name': fields.char('Resource Type', size=64, required=True, translate=True),
551         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
552         'object_id': fields.many2one('ir.model', 'Object Name'),
553     }
554     def _find_object_id(self, cr, uid, context=None):
555         """Finds id for case object
556         @param self: The object pointer
557         @param cr: the current row, from the database cursor,
558         @param uid: the current user’s ID for security checks,
559         @param context: A standard dictionary for contextual values
560         """
561         object_id = context and context.get('object_id', False) or False
562         ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
563         return ids and ids[0]
564
565     _defaults = {
566         'object_id' : _find_object_id
567     }
568
569 crm_case_resource_type()
570
571
572 class crm_case_stage(osv.osv):
573     """ Stage of case """
574
575     _name = "crm.case.stage"
576     _description = "Stage of case"
577     _rec_name = 'name'
578     _order = "sequence"
579
580     _columns = {
581         'name': fields.char('Stage Name', size=64, required=True, translate=True),
582         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
583         'sequence': fields.integer('Sequence', help="Gives the sequence order \
584                         when displaying a list of case stages."),
585         'object_id': fields.many2one('ir.model', 'Object Name'),
586         'probability': fields.float('Probability (%)', required=True),
587         'on_change': fields.boolean('Change Probability Automatically', \
588                          help="Change Probability on next and previous stages."),
589         'requirements': fields.text('Requirements')
590     }
591     def _find_object_id(self, cr, uid, context=None):
592         """Finds id for case object
593         @param self: The object pointer
594         @param cr: the current row, from the database cursor,
595         @param uid: the current user’s ID for security checks,
596         @param context: A standard dictionary for contextual values
597         """
598         object_id = context and context.get('object_id', False) or False
599         ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
600         return ids and ids[0]
601
602     _defaults = {
603         'sequence': lambda *args: 1,
604         'probability': lambda *args: 0.0,
605         'object_id' : _find_object_id
606     }
607
608 crm_case_stage()
609
610 def _links_get(self, cr, uid, context=None):
611     """Gets links value for reference field
612     @param self: The object pointer
613     @param cr: the current row, from the database cursor,
614     @param uid: the current user’s ID for security checks,
615     @param context: A standard dictionary for contextual values
616     """
617     if not context:
618         context = {}
619     obj = self.pool.get('res.request.link')
620     ids = obj.search(cr, uid, [])
621     res = obj.read(cr, uid, ids, ['object', 'name'], context)
622     return [(r['object'], r['name']) for r in res]
623
624 class users(osv.osv):
625     _inherit = 'res.users'
626     _description = "Users"
627     _columns = {
628         'context_section_id': fields.many2one('crm.case.section', 'Sales Team'),
629     }
630 users()
631
632
633 class res_partner(osv.osv):
634     _inherit = 'res.partner'
635     _columns = {
636         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
637     }
638 res_partner()
639
640 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: