Merge pull request #286 from jbq/bugfix
[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             if isinstance(res, dict):
275                 records = [res]
276             else:
277                 records = res
278             # build the res_ids and the old_values dict. Here we don't use get_data() to
279             # avoid performing an additional read()
280             res_ids = []
281             for record in records:
282                 res_ids.append(record['id'])
283                 old_values[(model.id, record['id'])] = {'value': record, 'text': record}
284             # log only the fields read
285             field_list = args[1]
286         elif method == 'unlink':
287             res_ids = args[0]
288             old_values = self.get_data(cr, uid_orig, pool, res_ids, model, method)
289             # process_data first as fct_src will unlink the record
290             self.process_data(cr, uid_orig, pool, res_ids, model, method, old_values, new_values, field_list)
291             return fct_src(cr, uid_orig, model.model, method, *args, **kw)
292         else: # method is write, action or workflow action
293             res_ids = []
294             if args:
295                 res_ids = args[0]
296                 if isinstance(res_ids, (long, int)):
297                     res_ids = [res_ids]
298             if res_ids:
299                 # store the old values into a dictionary
300                 old_values = self.get_data(cr, uid_orig, pool, res_ids, model, method)
301             # process the original function, workflow trigger...
302             res = fct_src(cr, uid_orig, model.model, method, *args, **kw)
303             if method == 'copy':
304                 res_ids = [res]
305             if res_ids:
306                 # check the new values and store them into a dictionary
307                 new_values = self.get_data(cr, uid_orig, pool, res_ids, model, method)
308         # compare the old and new values and create audittrail log if needed
309         self.process_data(cr, uid_orig, pool, res_ids, model, method, old_values, new_values, field_list)
310         return res
311
312     def get_data(self, cr, uid, pool, res_ids, model, method):
313         """
314         This function simply read all the fields of the given res_ids, and also recurisvely on
315         all records of a x2m fields read that need to be logged. Then it returns the result in
316         convenient structure that will be used as comparison basis.
317
318             :param cr: the current row, from the database cursor,
319             :param uid: the current user’s ID. This parameter is currently not used as every
320                 operation to get data is made as super admin. Though, it could be usefull later.
321             :param pool: current db's pooler object.
322             :param res_ids: Id's of resource to be logged/compared.
323             :param model: Object whose values are being changed
324             :param method: method to log: create, read, unlink, write, actions, workflow actions
325             :return: dict mapping a tuple (model_id, resource_id) with its value and textual value
326                 { (model_id, resource_id): { 'value': ...
327                                              'textual_value': ...
328                                            },
329                 }
330         """
331         data = {}
332         resource_pool = pool.get(model.model)
333         # read all the fields of the given resources in super admin mode
334         for resource in resource_pool.read(cr, SUPERUSER_ID, res_ids, resource_pool._all_columns):
335             values = {}
336             values_text = {}
337             resource_id = resource['id']
338             # loop on each field on the res_ids we just have read
339             for field in resource:
340                 if field in ('__last_update', 'id'):
341                     continue
342                 values[field] = resource[field]
343                 # get the textual value of that field for this record
344                 values_text[field] = self.get_value_text(cr, SUPERUSER_ID, pool, resource_pool, method, field, resource[field])
345
346                 field_obj = resource_pool._all_columns.get(field).column
347                 if field_obj._type in ('one2many','many2many'):
348                     # check if an audittrail rule apply in super admin mode
349                     if self.check_rules(cr, SUPERUSER_ID, field_obj._obj, method):
350                         # check if the model associated to a *2m field exists, in super admin mode
351                         x2m_model_ids = pool.get('ir.model').search(cr, SUPERUSER_ID, [('model', '=', field_obj._obj)])
352                         x2m_model_id = x2m_model_ids and x2m_model_ids[0] or False
353                         assert x2m_model_id, _("'%s' Model does not exist..." %(field_obj._obj))
354                         x2m_model = pool.get('ir.model').browse(cr, SUPERUSER_ID, x2m_model_id)
355                         field_resource_ids = list(set(resource[field]))
356                         if model.model == x2m_model.model:
357                             # we need to remove current resource_id from the many2many to prevent an infinit loop
358                             if resource_id in field_resource_ids:
359                                 field_resource_ids.remove(resource_id)
360                         data.update(self.get_data(cr, SUPERUSER_ID, pool, field_resource_ids, x2m_model, method))
361     
362             data[(model.id, resource_id)] = {'text':values_text, 'value': values}
363         return data
364
365     def prepare_audittrail_log_line(self, cr, uid, pool, model, resource_id, method, old_values, new_values, field_list=None):
366         """
367         This function compares the old data (i.e before the method was executed) and the new data
368         (after the method was executed) and returns a structure with all the needed information to
369         log those differences.
370
371         :param cr: the current row, from the database cursor,
372         :param uid: the current user’s ID. This parameter is currently not used as every
373             operation to get data is made as super admin. Though, it could be usefull later.
374         :param pool: current db's pooler object.
375         :param model: model object which values are being changed
376         :param resource_id: ID of record to which values are being changed
377         :param method: method to log: create, read, unlink, write, actions, workflow actions
378         :param old_values: dict of values read before execution of the method
379         :param new_values: dict of values read after execution of the method
380         :param field_list: optional argument containing the list of fields to log. Currently only
381             used when performing a read, it could be usefull later on if we want to log the write
382             on specific fields only.
383
384         :return: dictionary with
385             * keys: tuples build as ID of model object to log and ID of resource to log
386             * values: list of all the changes in field values for this couple (model, resource)
387               return {
388                 (model.id, resource_id): []
389               }
390
391         The reason why the structure returned is build as above is because when modifying an existing
392         record, we may have to log a change done in a x2many field of that object
393         """
394         if field_list is None:
395             field_list = []
396         key = (model.id, resource_id)
397         lines = {
398             key: []
399         }
400         # loop on all the fields
401         for field_name, field_definition in pool.get(model.model)._all_columns.items():
402             if field_name in ('__last_update', 'id'):
403                 continue
404             #if the field_list param is given, skip all the fields not in that list
405             if field_list and field_name not in field_list:
406                 continue
407             field_obj = field_definition.column
408             if field_obj._type in ('one2many','many2many'):
409                 # checking if an audittrail rule apply in super admin mode
410                 if self.check_rules(cr, SUPERUSER_ID, field_obj._obj, method):
411                     # checking if the model associated to a *2m field exists, in super admin mode
412                     x2m_model_ids = pool.get('ir.model').search(cr, SUPERUSER_ID, [('model', '=', field_obj._obj)])
413                     x2m_model_id = x2m_model_ids and x2m_model_ids[0] or False
414                     assert x2m_model_id, _("'%s' Model does not exist..." %(field_obj._obj))
415                     x2m_model = pool.get('ir.model').browse(cr, SUPERUSER_ID, x2m_model_id)
416                     # the resource_ids that need to be checked are the sum of both old and previous values (because we
417                     # need to log also creation or deletion in those lists).
418                     x2m_old_values_ids = old_values.get(key, {'value': {}})['value'].get(field_name, [])
419                     x2m_new_values_ids = new_values.get(key, {'value': {}})['value'].get(field_name, [])
420                     # We use list(set(...)) to remove duplicates.
421                     res_ids = list(set(x2m_old_values_ids + x2m_new_values_ids))
422                     if model.model == x2m_model.model:
423                         # we need to remove current resource_id from the many2many to prevent an infinit loop
424                         if resource_id in res_ids:
425                             res_ids.remove(resource_id)
426                     for res_id in res_ids:
427                         lines.update(self.prepare_audittrail_log_line(cr, SUPERUSER_ID, pool, x2m_model, res_id, method, old_values, new_values, field_list))
428             # if the value value is different than the old value: record the change
429             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]:
430                 data = {
431                       'name': field_name,
432                       'new_value': key in new_values and new_values[key]['value'].get(field_name),
433                       'old_value': key in old_values and old_values[key]['value'].get(field_name),
434                       'new_value_text': key in new_values and new_values[key]['text'].get(field_name),
435                       'old_value_text': key in old_values and old_values[key]['text'].get(field_name)
436                 }
437                 lines[key].append(data)
438             # On read log add current values for fields.
439             if method == 'read':
440                 data={
441                     'name': field_name,
442                     'old_value': key in old_values and old_values[key]['value'].get(field_name),
443                     'old_value_text': key in old_values and old_values[key]['text'].get(field_name)
444                 }
445                 lines[key].append(data)
446         return lines
447
448     def process_data(self, cr, uid, pool, res_ids, model, method, old_values=None, new_values=None, field_list=None):
449         """
450         This function processes and iterates recursively to log the difference between the old
451         data (i.e before the method was executed) and the new data and creates audittrail log
452         accordingly.
453
454         :param cr: the current row, from the database cursor,
455         :param uid: the current user’s ID,
456         :param pool: current db's pooler object.
457         :param res_ids: Id's of resource to be logged/compared.
458         :param model: model object which values are being changed
459         :param method: method to log: create, read, unlink, write, actions, workflow actions
460         :param old_values: dict of values read before execution of the method
461         :param new_values: dict of values read after execution of the method
462         :param field_list: optional argument containing the list of fields to log. Currently only
463             used when performing a read, it could be usefull later on if we want to log the write
464             on specific fields only.
465         :return: True
466         """
467         if field_list is None:
468             field_list = []
469         # loop on all the given ids
470         for res_id in res_ids:
471             # compare old and new values and get audittrail log lines accordingly
472             lines = self.prepare_audittrail_log_line(cr, uid, pool, model, res_id, method, old_values, new_values, field_list)
473
474             # if at least one modification has been found
475             for model_id, resource_id in lines:
476                 line_model = pool.get('ir.model').browse(cr, SUPERUSER_ID, model_id).model
477
478                 vals = {
479                     'method': method,
480                     'object_id': model_id,
481                     'user_id': uid,
482                     'res_id': resource_id,
483                 }
484                 if (model_id, resource_id) not in old_values and method not in ('copy', 'read'):
485                     # the resource was not existing so we are forcing the method to 'create'
486                     # (because it could also come with the value 'write' if we are creating
487                     #  new record through a one2many field)
488                     vals.update({'method': 'create'})
489                 if (model_id, resource_id) not in new_values and method not in ('copy', 'read'):
490                     # the resource is not existing anymore so we are forcing the method to 'unlink'
491                     # (because it could also come with the value 'write' if we are deleting the
492                     #  record through a one2many field)
493                     name = old_values[(model_id, resource_id)]['value'].get('name',False)
494                     vals.update({'method': 'unlink'})
495                 else :
496                     name = pool[line_model].name_get(cr, uid, [resource_id])[0][1]
497                 vals.update({'name': name})
498                 # create the audittrail log in super admin mode, only if a change has been detected
499                 if lines[(model_id, resource_id)]:
500                     log_id = pool.get('audittrail.log').create(cr, SUPERUSER_ID, vals)
501                     model = pool.get('ir.model').browse(cr, uid, model_id)
502                     self.create_log_line(cr, SUPERUSER_ID, log_id, model, lines[(model_id, resource_id)])
503         return True
504
505     def check_rules(self, cr, uid, model, method):
506         """
507         Checks if auditrails is installed for that db and then if one rule match
508         @param cr: the current row, from the database cursor,
509         @param uid: the current user’s ID,
510         @param model: value of _name of the object which values are being changed
511         @param method: method to log: create, read, unlink,write,actions,workflow actions
512         @return: True or False
513         """
514         pool = pooler.get_pool(cr.dbname)
515         if 'audittrail.rule' in pool.models:
516             model_ids = pool.get('ir.model').search(cr, SUPERUSER_ID, [('model', '=', model)])
517             model_id = model_ids and model_ids[0] or False
518             if model_id:
519                 rule_ids = pool.get('audittrail.rule').search(cr, SUPERUSER_ID, [('object_id', '=', model_id), ('state', '=', 'subscribed')])
520                 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']):
521                     if len(rule['user_id']) == 0 or uid in rule['user_id']:
522                         if rule.get('log_'+method,0):
523                             return True
524                         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'):
525                             if rule['log_action']:
526                                 return True
527
528     def execute_cr(self, cr, uid, model, method, *args, **kw):
529         fct_src = super(audittrail_objects_proxy, self).execute_cr
530         if self.check_rules(cr,uid,model,method):
531             return self.log_fct(cr, uid, model, method, fct_src, *args, **kw)
532         return fct_src(cr, uid, model, method, *args, **kw)
533
534     def exec_workflow_cr(self, cr, uid, model, method, *args, **kw):
535         fct_src = super(audittrail_objects_proxy, self).exec_workflow_cr
536         if self.check_rules(cr,uid,model,'workflow'):
537             return self.log_fct(cr, uid, model, method, fct_src, *args, **kw)
538         return fct_src(cr, uid, model, method, *args, **kw)
539
540 audittrail_objects_proxy()
541
542 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
543