1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
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
27 from openerp import tools
28 from openerp import SUPERUSER_ID
30 class audittrail_rule(osv.osv):
34 _name = 'audittrail.rule'
35 _description = "Audittrail Rule"
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"),
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.""")
61 def subscribe(self, cr, uid, ids, *args):
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.
69 obj_action = self.pool.get('ir.actions.act_window')
70 obj_model = self.pool.get('ir.model.data')
72 for thisrule in self.browse(cr, uid, ids):
73 obj = self.pool.get(thisrule.object_id.model)
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"})
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)]"
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)
94 def unsubscribe(self, cr, uid, ids, *args):
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.
102 obj_action = self.pool.get('ir.actions.act_window')
103 ir_values_obj = self.pool.get('ir.values')
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)])
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)])
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"})
122 class audittrail_log(osv.osv):
126 _name = 'audittrail.log'
127 _description = "Audittrail Log"
129 def _name_get_resname(self, cr, uid, ids, *args):
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']
139 data[resname.id] = False
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'),
153 "timestamp": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S")
155 _order = "timestamp desc"
157 class audittrail_log_line(osv.osv):
161 _name = 'audittrail.log.line'
162 _description = "Log Line"
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),
174 class audittrail_objects_proxy(object_proxy):
175 """ Uses Object proxy for auditing changes on object of subscribed Rules"""
177 def get_value_text(self, cr, uid, pool, resource_pool, method, field, value):
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)
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
205 def create_log_line(self, cr, uid, log_id, model, lines=None):
207 Creates lines for changed fields with its old and new values
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
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')
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
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
243 line_id = log_line_pool.create(cr, uid, vals)
246 def log_fct(self, cr, uid_orig, model, method, fct_src, *args, **kw):
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
253 @return: Returns result as per method of Object proxy
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)
263 # fields to log. currently only used by log on read()
265 old_values = new_values = {}
267 if method == 'create':
268 res = fct_src(cr, uid_orig, model.model, method, *args, **kw)
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):
278 # build the res_ids and the old_values dict. Here we don't use get_data() to
279 # avoid performing an additional read()
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
286 elif method == 'unlink':
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
296 if isinstance(res_ids, (long, int)):
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)
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)
312 def get_data(self, cr, uid, pool, res_ids, model, method):
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.
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': ...
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):
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'):
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])
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))
362 data[(model.id, resource_id)] = {'text':values_text, 'value': values}
365 def prepare_audittrail_log_line(self, cr, uid, pool, model, resource_id, method, old_values, new_values, field_list=None):
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.
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.
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)
388 (model.id, resource_id): []
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
394 if field_list is None:
396 key = (model.id, resource_id)
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'):
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:
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]:
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)
437 lines[key].append(data)
438 # On read log add current values for fields.
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)
445 lines[key].append(data)
448 def process_data(self, cr, uid, pool, res_ids, model, method, old_values=None, new_values=None, field_list=None):
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
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.
467 if field_list is None:
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)
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
480 'object_id': model_id,
482 'res_id': resource_id,
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'})
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)])
505 def check_rules(self, cr, uid, model, method):
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
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
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):
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']:
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)
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)
540 audittrail_objects_proxy()
542 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: