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 ##############################################################################
23 from openerp.osv import fields, osv
24 import openerp.service.model
25 from openerp.tools.translate import _
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 if thisrule.object_id.model not in self.pool:
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"})
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)]"
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)
93 def unsubscribe(self, cr, uid, ids, *args):
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.
101 obj_action = self.pool.get('ir.actions.act_window')
102 ir_values_obj = self.pool.get('ir.values')
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)])
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)])
115 res = ir_values_obj.unlink(cr, uid, [val_id[0]])
116 self.write(cr, uid, [thisrule.id], {"state": "draft"})
120 class audittrail_log(osv.osv):
124 _name = 'audittrail.log'
125 _description = "Audittrail Log"
127 def _name_get_resname(self, cr, uid, ids, *args):
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']
137 data[resname.id] = False
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'),
151 "timestamp": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S")
153 _order = "timestamp desc"
155 class audittrail_log_line(osv.osv):
159 _name = 'audittrail.log.line'
160 _description = "Log Line"
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),
172 # Monkeypatch the model RPC endpoint for auditing changes.
174 def get_value_text(cr, uid, pool, resource_pool, method, field, value):
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)
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
202 def create_log_line(cr, uid, log_id, model, lines=None):
204 Creates lines for changed fields with its old and new values
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
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')
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
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
240 line_id = log_line_pool.create(cr, uid, vals)
243 def log_fct(cr, uid_orig, model, method, fct_src, *args, **kw):
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
250 @return: Returns result as per method of Object proxy
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)
260 # fields to log. currently only used by log on read()
262 old_values = new_values = {}
264 if method == 'create':
265 res = fct_src(cr, uid_orig, model.model, method, *args, **kw)
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):
275 # build the res_ids and the old_values dict. Here we don't use get_data() to
276 # avoid performing an additional read()
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
283 elif method == 'unlink':
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
293 if isinstance(res_ids, (long, int)):
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)
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)
309 def get_data(cr, uid, pool, res_ids, model, method):
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.
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': ...
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):
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'):
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])
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))
359 data[(model.id, resource_id)] = {'text':values_text, 'value': values}
362 def prepare_audittrail_log_line(cr, uid, pool, model, resource_id, method, old_values, new_values, field_list=None):
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.
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.
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)
385 (model.id, resource_id): []
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
391 if field_list is None:
393 key = (model.id, resource_id)
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'):
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:
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]:
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)
434 lines[key].append(data)
437 def process_data(cr, uid, pool, res_ids, model, method, old_values=None, new_values=None, field_list=None):
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
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.
456 if field_list is None:
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)
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]
470 'object_id': model_id,
472 'res_id': resource_id,
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)])
492 def check_rules(cr, uid, model, method):
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
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
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):
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']:
515 # Replace the openerp.service.model functions.
517 original_execute_cr = openerp.service.model.execute_cr
518 original_exec_workflow_cr = openerp.service.model.exec_workflow_cr
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)
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)
532 openerp.service.model.execute_cr = execute_cr
533 openerp.service.model.exec_workflow_cr = exec_workflow_cr
535 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: