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