[FIX] web: when filtering out context, also strip *_view_ref keys
[odoo/odoo.git] / addons / audittrail / audittrail.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 openerp
23 from openerp.osv import fields, osv
24 import openerp.service.model
25 from openerp.tools.translate import _
26 import time
27 from openerp import tools
28 from openerp import SUPERUSER_ID
29
30 class audittrail_rule(osv.osv):
31     """
32     For Auddittrail Rule
33     """
34     _name = 'audittrail.rule'
35     _description = "Audittrail Rule"
36     _columns = {
37         "name": fields.char("Rule Name", size=32, required=True),
38         "object_id": fields.many2one('ir.model', 'Object', required=True, help="Select object for which you want to generate log."),
39         "user_id": fields.many2many('res.users', 'audittail_rules_users',
40                                             'user_id', 'rule_id', 'Users', help="if  User is not added then it will applicable for all users"),
41         "log_read": fields.boolean("Log Reads", help="Select this if you want to keep track of read/open on any record of the object of this rule"),
42         "log_write": fields.boolean("Log Writes", help="Select this if you want to keep track of modification on any record of the object of this rule"),
43         "log_unlink": fields.boolean("Log Deletes", help="Select this if you want to keep track of deletion on any record of the object of this rule"),
44         "log_create": fields.boolean("Log Creates",help="Select this if you want to keep track of creation on any record of the object of this rule"),
45         "log_action": fields.boolean("Log Action",help="Select this if you want to keep track of actions on the object of this rule"),
46         "log_workflow": fields.boolean("Log Workflow",help="Select this if you want to keep track of workflow on any record of the object of this rule"),
47         "state": fields.selection((("draft", "Draft"), ("subscribed", "Subscribed")), "Status", required=True),
48         "action_id": fields.many2one('ir.actions.act_window', "Action ID"),
49     }
50     _defaults = {
51         'state': 'draft',
52         'log_create': 1,
53         'log_unlink': 1,
54         'log_write': 1,
55     }
56     _sql_constraints = [
57         ('model_uniq', 'unique (object_id)', """There is already a rule defined on this object\n You cannot define another: please edit the existing one.""")
58     ]
59     __functions = {}
60
61     def subscribe(self, cr, uid, ids, *args):
62         """
63         Subscribe Rule for auditing changes on object and apply shortcut for logs on that object.
64         @param cr: the current row, from the database cursor,
65         @param uid: the current user’s ID for security checks,
66         @param ids: List of Auddittrail Rule’s IDs.
67         @return: True
68         """
69         obj_action = self.pool.get('ir.actions.act_window')
70         obj_model = self.pool.get('ir.model.data')
71         #start Loop
72         for thisrule in self.browse(cr, uid, ids):
73             if thisrule.object_id.model not in self.pool:
74                 raise osv.except_osv(
75                         _('WARNING: audittrail is not part of the pool'),
76                         _('Change audittrail depends -- Setting rule as DRAFT'))
77                 self.write(cr, uid, [thisrule.id], {"state": "draft"})
78             val = {
79                  "name": 'View Log',
80                  "res_model": 'audittrail.log',
81                  "src_model": thisrule.object_id.model,
82                  "domain": "[('object_id','=', " + str(thisrule.object_id.id) + "), ('res_id', '=', active_id)]"
83
84             }
85             action_id = obj_action.create(cr, SUPERUSER_ID, val)
86             self.write(cr, uid, [thisrule.id], {"state": "subscribed", "action_id": action_id})
87             keyword = 'client_action_relate'
88             value = 'ir.actions.act_window,' + str(action_id)
89             res = obj_model.ir_set(cr, SUPERUSER_ID, 'action', keyword, 'View_log_' + thisrule.object_id.model, [thisrule.object_id.model], value, replace=True, isobject=True, xml_id=False)
90             #End Loop
91         return True
92
93     def unsubscribe(self, cr, uid, ids, *args):
94         """
95         Unsubscribe Auditing Rule on object
96         @param cr: the current row, from the database cursor,
97         @param uid: the current user’s ID for security checks,
98         @param ids: List of Auddittrail Rule’s IDs.
99         @return: True
100         """
101         obj_action = self.pool.get('ir.actions.act_window')
102         ir_values_obj = self.pool.get('ir.values')
103         value=''
104         #start Loop
105         for thisrule in self.browse(cr, uid, ids):
106             if thisrule.id in self.__functions:
107                 for function in self.__functions[thisrule.id]:
108                     setattr(function[0], function[1], function[2])
109             w_id = obj_action.search(cr, uid, [('name', '=', 'View Log'), ('res_model', '=', 'audittrail.log'), ('src_model', '=', thisrule.object_id.model)])
110             if w_id:
111                 obj_action.unlink(cr, SUPERUSER_ID, w_id)
112                 value = "ir.actions.act_window" + ',' + str(w_id[0])
113             val_id = ir_values_obj.search(cr, uid, [('model', '=', thisrule.object_id.model), ('value', '=', value)])
114             if val_id:
115                 res = ir_values_obj.unlink(cr, uid, [val_id[0]])
116             self.write(cr, uid, [thisrule.id], {"state": "draft"})
117         #End Loop
118         return True
119
120 class audittrail_log(osv.osv):
121     """
122     For Audittrail Log
123     """
124     _name = 'audittrail.log'
125     _description = "Audittrail Log"
126
127     def _name_get_resname(self, cr, uid, ids, *args):
128         data = {}
129         for resname in self.browse(cr, uid, ids,[]):
130             model_object = resname.object_id
131             res_id = resname.res_id
132             if model_object and res_id:
133                 model_pool = self.pool[model_object.model]
134                 res = model_pool.read(cr, uid, res_id, ['name'])
135                 data[resname.id] = res['name']
136             else:
137                  data[resname.id] = False
138         return data
139
140     _columns = {
141         "name": fields.char("Resource Name",size=64),
142         "object_id": fields.many2one('ir.model', 'Object'),
143         "user_id": fields.many2one('res.users', 'User'),
144         "method": fields.char("Method", size=64),
145         "timestamp": fields.datetime("Date"),
146         "res_id": fields.integer('Resource Id'),
147         "line_ids": fields.one2many('audittrail.log.line', 'log_id', 'Log lines'),
148     }
149
150     _defaults = {
151         "timestamp": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S")
152     }
153     _order = "timestamp desc"
154
155 class audittrail_log_line(osv.osv):
156     """
157     Audittrail Log Line.
158     """
159     _name = 'audittrail.log.line'
160     _description = "Log Line"
161     _columns = {
162           'field_id': fields.many2one('ir.model.fields', 'Fields', required=True),
163           'log_id': fields.many2one('audittrail.log', 'Log'),
164           'log': fields.integer("Log ID"),
165           'old_value': fields.text("Old Value"),
166           'new_value': fields.text("New Value"),
167           'old_value_text': fields.text('Old value Text'),
168           'new_value_text': fields.text('New value Text'),
169           'field_description': fields.char('Field Description', size=64),
170         }
171
172 # Monkeypatch the model RPC endpoint for auditing changes.
173
174 def get_value_text(cr, uid, pool, resource_pool, method, field, value):
175     """
176     Gets textual values for the fields.
177         If the field is a many2one, it returns the name.
178         If it's a one2many or a many2many, it returns a list of name.
179         In other cases, it just returns the value.
180     :param cr: the current row, from the database cursor,
181     :param uid: the current user’s ID for security checks,
182     :param pool: current db's pooler object.
183     :param resource_pool: pooler object of the model which values are being changed.
184     :param field: for which the text value is to be returned.
185     :param value: value of the field.
186     :param recursive: True or False, True will repeat the process recursively
187     :return: string value or a list of values(for O2M/M2M)
188     """
189
190     field_obj = (resource_pool._all_columns.get(field)).column
191     if field_obj._type in ('one2many','many2many'):
192         data = pool[field_obj._obj].name_get(cr, uid, value)
193         #return the modifications on x2many fields as a list of names
194         res = map(lambda x:x[1], data)
195     elif field_obj._type == 'many2one':
196         #return the modifications on a many2one field as its value returned by name_get()
197         res = value and value[1] or value
198     else:
199         res = value
200     return res
201
202 def create_log_line(cr, uid, log_id, model, lines=None):
203     """
204     Creates lines for changed fields with its old and new values
205
206     @param cr: the current row, from the database cursor,
207     @param uid: the current user’s ID for security checks,
208     @param model: Object which values are being changed
209     @param lines: List of values for line is to be created
210     """
211     if lines is None:
212         lines = []
213     pool = openerp.registry(cr.dbname)
214     obj_pool = pool[model.model]
215     model_pool = pool.get('ir.model')
216     field_pool = pool.get('ir.model.fields')
217     log_line_pool = pool.get('audittrail.log.line')
218     for line in lines:
219         field_obj = obj_pool._all_columns.get(line['name'])
220         assert field_obj, _("'%s' field does not exist in '%s' model" %(line['name'], model.model))
221         field_obj = field_obj.column
222         old_value = line.get('old_value', '')
223         new_value = line.get('new_value', '')
224         search_models = [model.id]
225         if obj_pool._inherits:
226             search_models += model_pool.search(cr, uid, [('model', 'in', obj_pool._inherits.keys())])
227         field_id = field_pool.search(cr, uid, [('name', '=', line['name']), ('model_id', 'in', search_models)])
228         if field_obj._type == 'many2one':
229             old_value = old_value and old_value[0] or old_value
230             new_value = new_value and new_value[0] or new_value
231         vals = {
232                 "log_id": log_id,
233                 "field_id": field_id and field_id[0] or False,
234                 "old_value": old_value,
235                 "new_value": new_value,
236                 "old_value_text": line.get('old_value_text', ''),
237                 "new_value_text": line.get('new_value_text', ''),
238                 "field_description": field_obj.string
239                 }
240         line_id = log_line_pool.create(cr, uid, vals)
241     return True
242
243 def log_fct(cr, uid_orig, model, method, fct_src, *args, **kw):
244     """
245     Logging function: This function is performing the logging operation
246     @param model: Object whose values are being changed
247     @param method: method to log: create, read, write, unlink, action or workflow action
248     @param fct_src: execute method of Object proxy
249
250     @return: Returns result as per method of Object proxy
251     """
252     pool = openerp.registry(cr.dbname)
253     resource_pool = pool[model]
254     model_pool = pool.get('ir.model')
255     model_ids = model_pool.search(cr, SUPERUSER_ID, [('model', '=', model)])
256     model_id = model_ids and model_ids[0] or False
257     assert model_id, _("'%s' Model does not exist..." %(model))
258     model = model_pool.browse(cr, SUPERUSER_ID, model_id)
259
260     # fields to log. currently only used by log on read()
261     field_list = []
262     old_values = new_values = {}
263
264     if method == 'create':
265         res = fct_src(cr, uid_orig, model.model, method, *args, **kw)
266         if res:
267             res_ids = [res]
268             new_values = get_data(cr, uid_orig, pool, res_ids, model, method)
269     elif method == 'read':
270         res = fct_src(cr, uid_orig, model.model, method, *args, **kw)
271         if isinstance(res, dict):
272             records = [res]
273         else:
274             records = res
275         # build the res_ids and the old_values dict. Here we don't use get_data() to
276         # avoid performing an additional read()
277         res_ids = []
278         for record in records:
279             res_ids.append(record['id'])
280             old_values[(model.id, record['id'])] = {'value': record, 'text': record}
281         # log only the fields read
282         field_list = args[1]
283     elif method == 'unlink':
284         res_ids = args[0]
285         old_values = get_data(cr, uid_orig, pool, res_ids, model, method)
286         # process_data first as fct_src will unlink the record
287         self.process_data(cr, uid_orig, pool, res_ids, model, method, old_values, new_values, field_list)
288         return fct_src(cr, uid_orig, model.model, method, *args, **kw)
289     else: # method is write, action or workflow action
290         res_ids = []
291         if args:
292             res_ids = args[0]
293             if isinstance(res_ids, (long, int)):
294                 res_ids = [res_ids]
295         if res_ids:
296             # store the old values into a dictionary
297             old_values = get_data(cr, uid_orig, pool, res_ids, model, method)
298         # process the original function, workflow trigger...
299         res = fct_src(cr, uid_orig, model.model, method, *args, **kw)
300         if method == 'copy':
301             res_ids = [res]
302         if res_ids:
303             # check the new values and store them into a dictionary
304             new_values = get_data(cr, uid_orig, pool, res_ids, model, method)
305     # compare the old and new values and create audittrail log if needed
306     process_data(cr, uid_orig, pool, res_ids, model, method, old_values, new_values, field_list)
307     return res
308
309 def get_data(cr, uid, pool, res_ids, model, method):
310     """
311     This function simply read all the fields of the given res_ids, and also recurisvely on
312     all records of a x2m fields read that need to be logged. Then it returns the result in
313     convenient structure that will be used as comparison basis.
314
315         :param cr: the current row, from the database cursor,
316         :param uid: the current user’s ID. This parameter is currently not used as every
317             operation to get data is made as super admin. Though, it could be usefull later.
318         :param pool: current db's pooler object.
319         :param res_ids: Id's of resource to be logged/compared.
320         :param model: Object whose values are being changed
321         :param method: method to log: create, read, unlink, write, actions, workflow actions
322         :return: dict mapping a tuple (model_id, resource_id) with its value and textual value
323             { (model_id, resource_id): { 'value': ...
324                                          'textual_value': ...
325                                        },
326             }
327     """
328     data = {}
329     resource_pool = pool[model.model]
330     # read all the fields of the given resources in super admin mode
331     for resource in resource_pool.read(cr, SUPERUSER_ID, res_ids, resource_pool._all_columns):
332         values = {}
333         values_text = {}
334         resource_id = resource['id']
335         # loop on each field on the res_ids we just have read
336         for field in resource:
337             if field in ('__last_update', 'id'):
338                 continue
339             values[field] = resource[field]
340             # get the textual value of that field for this record
341             values_text[field] = get_value_text(cr, SUPERUSER_ID, pool, resource_pool, method, field, resource[field])
342
343             field_obj = resource_pool._all_columns.get(field).column
344             if field_obj._type in ('one2many','many2many'):
345                 # check if an audittrail rule apply in super admin mode
346                 if check_rules(cr, SUPERUSER_ID, field_obj._obj, method):
347                     # check if the model associated to a *2m field exists, in super admin mode
348                     x2m_model_ids = pool.get('ir.model').search(cr, SUPERUSER_ID, [('model', '=', field_obj._obj)])
349                     x2m_model_id = x2m_model_ids and x2m_model_ids[0] or False
350                     assert x2m_model_id, _("'%s' Model does not exist..." %(field_obj._obj))
351                     x2m_model = pool.get('ir.model').browse(cr, SUPERUSER_ID, x2m_model_id)
352                     field_resource_ids = list(set(resource[field]))
353                     if model.model == x2m_model.model:
354                         # we need to remove current resource_id from the many2many to prevent an infinit loop
355                         if resource_id in field_resource_ids:
356                             field_resource_ids.remove(resource_id)
357                     data.update(get_data(cr, SUPERUSER_ID, pool, field_resource_ids, x2m_model, method))
358
359         data[(model.id, resource_id)] = {'text':values_text, 'value': values}
360     return data
361
362 def prepare_audittrail_log_line(cr, uid, pool, model, resource_id, method, old_values, new_values, field_list=None):
363     """
364     This function compares the old data (i.e before the method was executed) and the new data
365     (after the method was executed) and returns a structure with all the needed information to
366     log those differences.
367
368     :param cr: the current row, from the database cursor,
369     :param uid: the current user’s ID. This parameter is currently not used as every
370         operation to get data is made as super admin. Though, it could be usefull later.
371     :param pool: current db's pooler object.
372     :param model: model object which values are being changed
373     :param resource_id: ID of record to which values are being changed
374     :param method: method to log: create, read, unlink, write, actions, workflow actions
375     :param old_values: dict of values read before execution of the method
376     :param new_values: dict of values read after execution of the method
377     :param field_list: optional argument containing the list of fields to log. Currently only
378         used when performing a read, it could be usefull later on if we want to log the write
379         on specific fields only.
380
381     :return: dictionary with
382         * keys: tuples build as ID of model object to log and ID of resource to log
383         * values: list of all the changes in field values for this couple (model, resource)
384           return {
385             (model.id, resource_id): []
386           }
387
388     The reason why the structure returned is build as above is because when modifying an existing
389     record, we may have to log a change done in a x2many field of that object
390     """
391     if field_list is None:
392         field_list = []
393     key = (model.id, resource_id)
394     lines = {
395         key: []
396     }
397     # loop on all the fields
398     for field_name, field_definition in pool[model.model]._all_columns.items():
399         if field_name in ('__last_update', 'id'):
400             continue
401         #if the field_list param is given, skip all the fields not in that list
402         if field_list and field_name not in field_list:
403             continue
404         field_obj = field_definition.column
405         if field_obj._type in ('one2many','many2many'):
406             # checking if an audittrail rule apply in super admin mode
407             if check_rules(cr, SUPERUSER_ID, field_obj._obj, method):
408                 # checking if the model associated to a *2m field exists, in super admin mode
409                 x2m_model_ids = pool.get('ir.model').search(cr, SUPERUSER_ID, [('model', '=', field_obj._obj)])
410                 x2m_model_id = x2m_model_ids and x2m_model_ids[0] or False
411                 assert x2m_model_id, _("'%s' Model does not exist..." %(field_obj._obj))
412                 x2m_model = pool.get('ir.model').browse(cr, SUPERUSER_ID, x2m_model_id)
413                 # the resource_ids that need to be checked are the sum of both old and previous values (because we
414                 # need to log also creation or deletion in those lists).
415                 x2m_old_values_ids = old_values.get(key, {'value': {}})['value'].get(field_name, [])
416                 x2m_new_values_ids = new_values.get(key, {'value': {}})['value'].get(field_name, [])
417                 # We use list(set(...)) to remove duplicates.
418                 res_ids = list(set(x2m_old_values_ids + x2m_new_values_ids))
419                 if model.model == x2m_model.model:
420                     # we need to remove current resource_id from the many2many to prevent an infinit loop
421                     if resource_id in res_ids:
422                         res_ids.remove(resource_id)
423                 for res_id in res_ids:
424                     lines.update(prepare_audittrail_log_line(cr, SUPERUSER_ID, pool, x2m_model, res_id, method, old_values, new_values, field_list))
425         # if the value value is different than the old value: record the change
426         if key not in old_values or key not in new_values or old_values[key]['value'][field_name] != new_values[key]['value'][field_name]:
427             data = {
428                   'name': field_name,
429                   'new_value': key in new_values and new_values[key]['value'].get(field_name),
430                   'old_value': key in old_values and old_values[key]['value'].get(field_name),
431                   'new_value_text': key in new_values and new_values[key]['text'].get(field_name),
432                   'old_value_text': key in old_values and old_values[key]['text'].get(field_name)
433             }
434             lines[key].append(data)
435     return lines
436
437 def process_data(cr, uid, pool, res_ids, model, method, old_values=None, new_values=None, field_list=None):
438     """
439     This function processes and iterates recursively to log the difference between the old
440     data (i.e before the method was executed) and the new data and creates audittrail log
441     accordingly.
442
443     :param cr: the current row, from the database cursor,
444     :param uid: the current user’s ID,
445     :param pool: current db's pooler object.
446     :param res_ids: Id's of resource to be logged/compared.
447     :param model: model object which values are being changed
448     :param method: method to log: create, read, unlink, write, actions, workflow actions
449     :param old_values: dict of values read before execution of the method
450     :param new_values: dict of values read after execution of the method
451     :param field_list: optional argument containing the list of fields to log. Currently only
452         used when performing a read, it could be usefull later on if we want to log the write
453         on specific fields only.
454     :return: True
455     """
456     if field_list is None:
457         field_list = []
458     # loop on all the given ids
459     for res_id in res_ids:
460         # compare old and new values and get audittrail log lines accordingly
461         lines = prepare_audittrail_log_line(cr, uid, pool, model, res_id, method, old_values, new_values, field_list)
462
463         # if at least one modification has been found
464         for model_id, resource_id in lines:
465             line_model = pool.get('ir.model').browse(cr, SUPERUSER_ID, model_id).model
466             name = pool.get(line_model).name_get(cr, uid, [resource_id])[0][1]
467
468             vals = {
469                 'method': method,
470                 'object_id': model_id,
471                 'user_id': uid,
472                 'res_id': resource_id,
473                 'name': name,
474             }
475             if (model_id, resource_id) not in old_values and method not in ('copy', 'read'):
476                 # the resource was not existing so we are forcing the method to 'create'
477                 # (because it could also come with the value 'write' if we are creating
478                 #  new record through a one2many field)
479                 vals.update({'method': 'create'})
480             if (model_id, resource_id) not in new_values and method not in ('copy', 'read'):
481                 # the resource is not existing anymore so we are forcing the method to 'unlink'
482                 # (because it could also come with the value 'write' if we are deleting the
483                 #  record through a one2many field)
484                 vals.update({'method': 'unlink'})
485             # create the audittrail log in super admin mode, only if a change has been detected
486             if lines[(model_id, resource_id)]:
487                 log_id = pool.get('audittrail.log').create(cr, SUPERUSER_ID, vals)
488                 model = pool.get('ir.model').browse(cr, uid, model_id)
489                 create_log_line(cr, SUPERUSER_ID, log_id, model, lines[(model_id, resource_id)])
490     return True
491
492 def check_rules(cr, uid, model, method):
493     """
494     Checks if auditrails is installed for that db and then if one rule match
495     @param cr: the current row, from the database cursor,
496     @param uid: the current user’s ID,
497     @param model: value of _name of the object which values are being changed
498     @param method: method to log: create, read, unlink,write,actions,workflow actions
499     @return: True or False
500     """
501     pool = openerp.registry(cr.dbname)
502     if 'audittrail.rule' in pool.models:
503         model_ids = pool.get('ir.model').search(cr, SUPERUSER_ID, [('model', '=', model)])
504         model_id = model_ids and model_ids[0] or False
505         if model_id:
506             rule_ids = pool.get('audittrail.rule').search(cr, SUPERUSER_ID, [('object_id', '=', model_id), ('state', '=', 'subscribed')])
507             for rule in pool.get('audittrail.rule').read(cr, SUPERUSER_ID, rule_ids, ['user_id','log_read','log_write','log_create','log_unlink','log_action','log_workflow']):
508                 if len(rule['user_id']) == 0 or uid in rule['user_id']:
509                     if rule.get('log_'+method,0):
510                         return True
511                     elif method not in ('default_get','read','fields_view_get','fields_get','search','search_count','name_search','name_get','get','request_get', 'get_sc', 'unlink', 'write', 'create', 'read_group', 'import_data'):
512                         if rule['log_action']:
513                             return True
514
515 # Replace the openerp.service.model functions.
516
517 original_execute_cr = openerp.service.model.execute_cr
518 original_exec_workflow_cr = openerp.service.model.exec_workflow_cr
519
520 def execute_cr(cr, uid, model, method, *args, **kw):
521     fct_src = original_execute_cr
522     if check_rules(cr,uid,model,method):
523         return log_fct(cr, uid, model, method, fct_src, *args, **kw)
524     return fct_src(cr, uid, model, method, *args, **kw)
525
526 def exec_workflow_cr(cr, uid, model, method, *args, **kw):
527     fct_src = original_exec_workflow_cr
528     if check_rules(cr,uid,model,'workflow'):
529         return log_fct(cr, uid, model, method, fct_src, *args, **kw)
530     return fct_src(cr, uid, model, method, *args, **kw)
531
532 openerp.service.model.execute_cr = execute_cr
533 openerp.service.model.exec_workflow_cr = exec_workflow_cr
534
535 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
536