diff --git a/auditlog/README.rst b/auditlog/README.rst new file mode 100644 index 000000000..fa7a86de9 --- /dev/null +++ b/auditlog/README.rst @@ -0,0 +1,49 @@ +Track user operation on data models +=================================== + +This module allows the administrator to log user operations performed on data +models such as ``create``, ``read``, ``write`` and ``delete``. + +Usage +===== + +Go to `Reporting / Audit / Rules` to subscribe rules. A rule defines which +operations to log for a given data model. +Then, check logs in the `Reporting / Audit / Logs` menu. + +During installation, it will migrate any existing data from the `audittrail` +module (rules and logs). + +For further information, please visit: + + * https://www.odoo.com/forum/help-1 + +Known issues / Roadmap +====================== + + * log ``read`` operations + * log only operations triggered by some users (currently it logs all users) + * group logs by HTTP query (thanks to werzeug)? + * group HTTP query by user session? + +Credits +======= + +Contributors +------------ + +* Sebastien Alix +* Holger Brunn + +Maintainer +---------- + +.. image:: http://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: http://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/auditlog/__init__.py b/auditlog/__init__.py new file mode 100644 index 000000000..ae4e80c04 --- /dev/null +++ b/auditlog/__init__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013 ABF OSIELL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from . import models + + +def pre_init_hook(cr): + cr.execute("SELECT 1 FROM pg_class WHERE relname = 'audittrail_rule'") + if cr.fetchall(): + migrate_from_audittrail(cr) + + +def migrate_from_audittrail(cr): + cr.execute('ALTER TABLE audittrail_rule RENAME TO auditlog_rule') + cr.execute('ALTER TABLE audittrail_rule_id_seq ' + 'RENAME TO auditlog_rule_id_seq') + cr.execute('ALTER TABLE auditlog_rule RENAME COLUMN object_id TO model_id') + cr.execute('ALTER TABLE audittrail_log RENAME TO auditlog_log') + cr.execute('ALTER TABLE audittrail_log_id_seq ' + 'RENAME TO auditlog_log_id_seq') + cr.execute('ALTER TABLE auditlog_log RENAME COLUMN object_id TO model_id') + cr.execute('ALTER TABLE audittrail_log_line RENAME TO auditlog_log_line') + cr.execute('ALTER TABLE audittrail_log_line_id_seq ' + 'RENAME TO auditlog_log_line_id_seq') + cr.execute("UPDATE ir_model_data SET model='auditlog.rule' " + "WHERE model='audittrail.rule'") diff --git a/auditlog/__openerp__.py b/auditlog/__openerp__.py new file mode 100644 index 000000000..8705ea51e --- /dev/null +++ b/auditlog/__openerp__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013 ABF OSIELL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{ + 'name': "Audit Log", + 'version': "1.0", + 'author': "ABF OSIELL", + 'website': "http://www.osiell.com", + 'category': "Tools", + 'depends': [ + 'base', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/auditlog_view.xml', + ], + 'application': True, + 'installable': True, + 'pre_init_hook': 'pre_init_hook', +} diff --git a/auditlog/i18n/auditlog.pot b/auditlog/i18n/auditlog.pot new file mode 100644 index 000000000..7efad6304 --- /dev/null +++ b/auditlog/i18n/auditlog.pot @@ -0,0 +1,279 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auditlog +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-22 13:33+0000\n" +"PO-Revision-Date: 2015-01-22 13:33+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: auditlog +#: field:auditlog.rule,action_id:0 +msgid "Action" +msgstr "" + +#. module: auditlog +#: model:ir.ui.menu,name:auditlog.menu_audit +msgid "Audit" +msgstr "" + +#. module: auditlog +#: model:ir.model,name:auditlog.model_auditlog_log +msgid "Auditlog - Log" +msgstr "" + +#. module: auditlog +#: model:ir.model,name:auditlog.model_auditlog_log_line +msgid "Auditlog - Log details (fields updated)" +msgstr "" + +#. module: auditlog +#: model:ir.model,name:auditlog.model_auditlog_rule +msgid "Auditlog - Rule" +msgstr "" + +#. module: auditlog +#: field:auditlog.log,create_uid:0 +#: field:auditlog.log.line,create_uid:0 +#: field:auditlog.rule,create_uid:0 +msgid "Created by" +msgstr "" + +#. module: auditlog +#: field:auditlog.log,create_date:0 +#: field:auditlog.log.line,create_date:0 +#: field:auditlog.rule,create_date:0 +msgid "Created on" +msgstr "" + +#. module: auditlog +#: field:auditlog.log.line,field_description:0 +msgid "Description" +msgstr "" + +#. module: auditlog +#: view:auditlog.rule:auditlog.view_auditlog_rule_search +#: selection:auditlog.rule,state:0 +msgid "Draft" +msgstr "" + +#. module: auditlog +#: field:auditlog.log.line,field_id:0 +msgid "Field" +msgstr "" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_form +#: field:auditlog.log,line_ids:0 +msgid "Fields updated" +msgstr "" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_search +#: view:auditlog.rule:auditlog.view_auditlog_rule_search +msgid "Group By..." +msgstr "" + +#. module: auditlog +#: field:auditlog.log,id:0 +#: field:auditlog.log.line,id:0 +#: field:auditlog.rule,id:0 +msgid "ID" +msgstr "" + +#. module: auditlog +#: field:auditlog.log,write_uid:0 +#: field:auditlog.log.line,write_uid:0 +#: field:auditlog.rule,write_uid:0 +msgid "Last Updated by" +msgstr "" + +#. module: auditlog +#: field:auditlog.log,write_date:0 +#: field:auditlog.log.line,write_date:0 +#: field:auditlog.rule,write_date:0 +msgid "Last Updated on" +msgstr "" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_form +#: field:auditlog.log.line,log_id:0 +msgid "Log" +msgstr "" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_form +msgid "Log - Field updated" +msgstr "" + +#. module: auditlog +#: field:auditlog.rule,log_create:0 +msgid "Log Creates" +msgstr "" + +#. module: auditlog +#: field:auditlog.rule,log_unlink:0 +msgid "Log Deletes" +msgstr "" + +#. module: auditlog +#: field:auditlog.rule,log_read:0 +msgid "Log Reads" +msgstr "" + +#. module: auditlog +#: field:auditlog.rule,log_write:0 +msgid "Log Writes" +msgstr "" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_search +#: view:auditlog.log:auditlog.view_auditlog_log_tree +#: model:ir.actions.act_window,name:auditlog.action_auditlog_log_tree +#: model:ir.ui.menu,name:auditlog.menu_audit_logs +msgid "Logs" +msgstr "" + +#. module: auditlog +#: field:auditlog.log,method:0 +msgid "Method" +msgstr "" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_search +#: field:auditlog.log,model_id:0 +#: field:auditlog.rule,model_id:0 +msgid "Model" +msgstr "" + +#. module: auditlog +#: field:auditlog.rule,name:0 +msgid "Name" +msgstr "" + +#. module: auditlog +#: field:auditlog.log.line,new_value:0 +msgid "New Value" +msgstr "" + +#. module: auditlog +#: field:auditlog.log.line,new_value_text:0 +msgid "New value Text" +msgstr "" + +#. module: auditlog +#: field:auditlog.log.line,old_value:0 +msgid "Old Value" +msgstr "" + +#. module: auditlog +#: field:auditlog.log.line,old_value_text:0 +msgid "Old value Text" +msgstr "" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_search +#: field:auditlog.log,res_id:0 +msgid "Resource ID" +msgstr "" + +#. module: auditlog +#: field:auditlog.log,name:0 +msgid "Resource Name" +msgstr "" + +#. module: auditlog +#: view:auditlog.rule:auditlog.view_auditlog_rule_form +msgid "Rule" +msgstr "" + +#. module: auditlog +#: view:auditlog.rule:auditlog.view_auditlog_rule_search +#: view:auditlog.rule:auditlog.view_auditlog_rule_tree +#: model:ir.actions.act_window,name:auditlog.action_auditlog_rule_tree +#: model:ir.ui.menu,name:auditlog.menu_action_auditlog_rule_tree +msgid "Rules" +msgstr "" + +#. module: auditlog +#: help:auditlog.rule,model_id:0 +msgid "Select model for which you want to generate log." +msgstr "" + +#. module: auditlog +#: help:auditlog.rule,log_create:0 +msgid "Select this if you want to keep track of creation on any record of the model of this rule" +msgstr "" + +#. module: auditlog +#: help:auditlog.rule,log_unlink:0 +msgid "Select this if you want to keep track of deletion on any record of the model of this rule" +msgstr "" + +#. module: auditlog +#: help:auditlog.rule,log_write:0 +msgid "Select this if you want to keep track of modification on any record of the model of this rule" +msgstr "" + +#. module: auditlog +#: help:auditlog.rule,log_read:0 +msgid "Select this if you want to keep track of read/open on any record of the model of this rule" +msgstr "" + +#. module: auditlog +#: view:auditlog.rule:auditlog.view_auditlog_rule_search +#: field:auditlog.rule,state:0 +msgid "State" +msgstr "" + +#. module: auditlog +#: view:auditlog.rule:auditlog.view_auditlog_rule_form +msgid "Subscribe" +msgstr "" + +#. module: auditlog +#: view:auditlog.rule:auditlog.view_auditlog_rule_search +#: selection:auditlog.rule,state:0 +msgid "Subscribed" +msgstr "" + +#. module: auditlog +#: field:auditlog.log.line,field_name:0 +msgid "Technical name" +msgstr "" + +#. module: auditlog +#: sql_constraint:auditlog.rule:0 +msgid "There is already a rule defined on this model\n" +"You cannot define another: please edit the existing one." +msgstr "" + +#. module: auditlog +#: view:auditlog.rule:auditlog.view_auditlog_rule_form +msgid "Unsubscribe" +msgstr "" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_search +#: field:auditlog.log,user_id:0 +msgid "User" +msgstr "" + +#. module: auditlog +#: field:auditlog.rule,user_ids:0 +msgid "Users" +msgstr "" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_form +msgid "Values" +msgstr "" + diff --git a/auditlog/i18n/fr.po b/auditlog/i18n/fr.po new file mode 100644 index 000000000..25f23f556 --- /dev/null +++ b/auditlog/i18n/fr.po @@ -0,0 +1,295 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auditlog +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-22 09:51+0000\n" +"PO-Revision-Date: 2015-01-22 09:51+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: auditlog +#: field:auditlog.rule,action_id:0 +msgid "Action" +msgstr "Action" + +#. module: auditlog +#: model:ir.ui.menu,name:auditlog.menu_audit +msgid "Audit" +msgstr "Audit" + +#. module: auditlog +#: model:ir.model,name:auditlog.model_auditlog_log +msgid "Auditlog - Log" +msgstr "Auditlog - Log" + +#. module: auditlog +#: model:ir.model,name:auditlog.model_auditlog_log_line +msgid "Auditlog - Log details (fields updated)" +msgstr "Auditlog - Détails (champs modifiés)" + +#. module: auditlog +#: model:ir.model,name:auditlog.model_auditlog_rule +msgid "Auditlog - Rule" +msgstr "Auditlog - Règle" + +#. module: auditlog +#: field:auditlog.log,create_uid:0 +#: field:auditlog.log.line,create_uid:0 +#: field:auditlog.rule,create_uid:0 +msgid "Created by" +msgstr "" + +#. module: auditlog +#: field:auditlog.log,create_date:0 +#: field:auditlog.log.line,create_date:0 +#: field:auditlog.rule,create_date:0 +msgid "Created on" +msgstr "Date" + +#. module: auditlog +#: field:auditlog.log.line,field_description:0 +msgid "Description" +msgstr "Description" + +#. module: auditlog +#: view:auditlog.rule:auditlog.view_auditlog_rule_search +#: selection:auditlog.rule,state:0 +msgid "Draft" +msgstr "Brouillon" + +#. module: auditlog +#: field:auditlog.log.line,field_id:0 +msgid "Field" +msgstr "Champ" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_form +#: field:auditlog.log,line_ids:0 +msgid "Fields updated" +msgstr "Champs modifiés" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_search +#: view:auditlog.rule:auditlog.view_auditlog_rule_search +msgid "Group By..." +msgstr "Grouper par..." + +#. module: auditlog +#: field:auditlog.log,id:0 +#: field:auditlog.log.line,id:0 +#: field:auditlog.rule,id:0 +msgid "ID" +msgstr "ID" + +#. module: auditlog +#: field:auditlog.log,write_uid:0 +#: field:auditlog.log.line,write_uid:0 +#: field:auditlog.rule,write_uid:0 +msgid "Last Updated by" +msgstr "" + +#. module: auditlog +#: field:auditlog.log,write_date:0 +#: field:auditlog.log.line,write_date:0 +#: field:auditlog.rule,write_date:0 +msgid "Last Updated on" +msgstr "" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_form +#: field:auditlog.log.line,log_id:0 +msgid "Log" +msgstr "Log" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_form +msgid "Log - Field updated" +msgstr "Log - Champs modifiés" + +#. module: auditlog +#: field:auditlog.rule,log_create:0 +msgid "Log Creates" +msgstr "Enregistrer les créations" + +#. module: auditlog +#: field:auditlog.rule,log_unlink:0 +msgid "Log Deletes" +msgstr "Enregistrer les suppressions" + +#. module: auditlog +#: field:auditlog.rule,log_read:0 +msgid "Log Reads" +msgstr "Enregistrer les lectures" + +#. module: auditlog +#: field:auditlog.rule,log_write:0 +msgid "Log Writes" +msgstr "Enregistrer les écritures" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_search +#: view:auditlog.log:auditlog.view_auditlog_log_tree +#: model:ir.actions.act_window,name:auditlog.action_auditlog_log_tree +#: model:ir.ui.menu,name:auditlog.menu_audit_logs +msgid "Logs" +msgstr "Journaux" + +#. module: auditlog +#: field:auditlog.log,method:0 +msgid "Method" +msgstr "Méthode" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_search +#: field:auditlog.log,model_id:0 +#: field:auditlog.rule,model_id:0 +msgid "Model" +msgstr "Modèle" + +#. module: auditlog +#: field:auditlog.rule,name:0 +msgid "Name" +msgstr "Nom" + +#. module: auditlog +#: field:auditlog.log.line,new_value:0 +msgid "New Value" +msgstr "Nouvelle valeur" + +#. module: auditlog +#: field:auditlog.log.line,new_value_text:0 +msgid "New value Text" +msgstr "Nouvelle valeur texte" + +#. module: auditlog +#: field:auditlog.log.line,old_value:0 +msgid "Old Value" +msgstr "Ancienne valeur" + +#. module: auditlog +#: field:auditlog.log.line,old_value_text:0 +msgid "Old value Text" +msgstr "Ancienne valeur texte" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_search +#: field:auditlog.log,res_id:0 +msgid "Resource ID" +msgstr "ID de l'enregistrement" + +#. module: auditlog +#: field:auditlog.log,name:0 +msgid "Resource Name" +msgstr "Nom de l'enregistrement" + +#. module: auditlog +#: view:auditlog.rule:auditlog.view_auditlog_rule_form +msgid "Rule" +msgstr "Règle" + +#. module: auditlog +#: view:auditlog.rule:auditlog.view_auditlog_rule_search +#: view:auditlog.rule:auditlog.view_auditlog_rule_tree +#: model:ir.actions.act_window,name:auditlog.action_auditlog_rule_tree +#: model:ir.ui.menu,name:auditlog.menu_action_auditlog_rule_tree +msgid "Rules" +msgstr "Règles" + +#. module: auditlog +#: help:auditlog.rule,model_id:0 +msgid "Select model for which you want to generate log." +msgstr "Sélectionnez le modèle pour lequel vous voulez générer un historique." + +#. module: auditlog +#: help:auditlog.rule,log_create:0 +msgid "Select this if you want to keep track of creation on any record of the model of this rule" +msgstr "" +"Cochez cette case si vous voulez garder une trace de la création d'un nouvel " +"enregistrement concernant le modèle défini dans cette règle." + +#. module: auditlog +#: help:auditlog.rule,log_unlink:0 +msgid "Select this if you want to keep track of deletion on any record of the model of this rule" +msgstr "" +"Cochez cette case si vous voulez garder une trace des suppressions des " +"enregistrements du modèle défini dans cette règle." + +#. module: auditlog +#: help:auditlog.rule,log_write:0 +msgid "Select this if you want to keep track of modification on any record of the model of this rule" +msgstr "" +"Cochez cette case si vous voulez garder une trace des modifications sur " +"chaque enregistrement du modèle défini dans cette règle." + +#. module: auditlog +#: help:auditlog.rule,log_read:0 +msgid "Select this if you want to keep track of read/open on any record of the model of this rule" +msgstr "" +"Cochez cette case si vous voulez garder une trace de la lecture/ouverture de " +"chaque enregistrement du modèle défini dans cette règle." + +#. module: auditlog +#: field:auditlog.rule,state:0 +#: view:auditlog.rule:auditlog.view_auditlog_rule_search +msgid "State" +msgstr "État" + +#. module: auditlog +#: view:auditlog.rule:auditlog.view_auditlog_rule_form +msgid "Subscribe" +msgstr "Abonner" + +#. module: auditlog +#: view:auditlog.rule:auditlog.view_auditlog_rule_search +#: selection:auditlog.rule,state:0 +msgid "Subscribed" +msgstr "Abonné" + +#. module: auditlog +#: field:auditlog.log.line,field_name:0 +msgid "Technical name" +msgstr "Nom technique" + +#. module: auditlog +#: sql_constraint:auditlog.rule:0 +msgid "There is already a rule defined on this model\n" +"You cannot define another: please edit the existing one." +msgstr "" +"Il existe déjà une règle définie sur ce modèle\n" +"Vous ne pouvez pas en définir une nouvelle, vous devez modifier celle " +"existante." + +#. module: auditlog +#: view:auditlog.rule:auditlog.view_auditlog_rule_form +msgid "Unsubscribe" +msgstr "Désabonner" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_search +#: field:auditlog.log,user_id:0 +msgid "User" +msgstr "Utilisateur" + +#. module: auditlog +#: field:auditlog.rule,user_id:0 +msgid "Users" +msgstr "Utilisateurs" + +#. module: auditlog +#: view:auditlog.log:auditlog.view_auditlog_log_form +msgid "Values" +msgstr "Valeurs" + +#. module: auditlog +#: code:addons/auditlog/models/rule.py:0 +#, python-format +msgid "View logs" +msgstr "Consulter les journaux" diff --git a/auditlog/migrations/8.0.1.0/pre-migration.py b/auditlog/migrations/8.0.1.0/pre-migration.py new file mode 100644 index 000000000..3fe51e77c --- /dev/null +++ b/auditlog/migrations/8.0.1.0/pre-migration.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2015 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from openerp.addons.auditlog import migrate_from_audittrail + + +def migrate(cr, version): + """if we migrate from an older version, it's a migration from audittrail""" + migrate_from_audittrail(cr) diff --git a/auditlog/models/__init__.py b/auditlog/models/__init__.py new file mode 100644 index 000000000..eb562a4c0 --- /dev/null +++ b/auditlog/models/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013 ABF OSIELL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from . import rule +from . import log diff --git a/auditlog/models/log.py b/auditlog/models/log.py new file mode 100644 index 000000000..b222c470e --- /dev/null +++ b/auditlog/models/log.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013 ABF OSIELL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields + + +class auditlog_log(models.Model): + _name = 'auditlog.log' + _description = "Auditlog - Log" + _order = "create_date desc" + + name = fields.Char("Resource Name", size=64) + model_id = fields.Many2one( + 'ir.model', string=u"Model") + res_id = fields.Integer(u"Resource ID") + user_id = fields.Many2one( + 'res.users', string=u"User") + method = fields.Char(u"Method", size=64) + line_ids = fields.One2many( + 'auditlog.log.line', 'log_id', string=u"Fields updated") + + +class auditlog_log_line(models.Model): + _name = 'auditlog.log.line' + _description = "Auditlog - Log details (fields updated)" + + field_id = fields.Many2one( + 'ir.model.fields', ondelete='cascade', string=u"Field", required=True) + log_id = fields.Many2one( + 'auditlog.log', string=u"Log", ondelete='cascade') + old_value = fields.Text(u"Old Value") + new_value = fields.Text(u"New Value") + old_value_text = fields.Text(u"Old value Text") + new_value_text = fields.Text(u"New value Text") + field_name = fields.Char(u"Technical name", related='field_id.name') + field_description = fields.Char( + u"Description", related='field_id.field_description') diff --git a/auditlog/models/rule.py b/auditlog/models/rule.py new file mode 100644 index 000000000..9cd9007c7 --- /dev/null +++ b/auditlog/models/rule.py @@ -0,0 +1,451 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013 ABF OSIELL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields, api, modules, _, SUPERUSER_ID + +FIELDS_BLACKLIST = [ + 'id', 'create_uid', 'create_date', 'write_uid', 'write_date', + 'display_name', '__last_update', +] +# Used for performance, to avoid a dictionary instanciation when we need an +# empty dict to simplify algorithms +EMPTY_DICT = {} + + +class DictDiffer(object): + """Calculate the difference between two dictionaries as: + (1) items added + (2) items removed + (3) keys same in both but changed values + (4) keys same in both and unchanged values + """ + def __init__(self, current_dict, past_dict): + self.current_dict, self.past_dict = current_dict, past_dict + self.set_current = set(current_dict) + self.set_past = set(past_dict) + self.intersect = self.set_current.intersection(self.set_past) + + def added(self): + return self.set_current - self.intersect + + def removed(self): + return self.set_past - self.intersect + + def changed(self): + return set(o for o in self.intersect + if self.past_dict[o] != self.current_dict[o]) + + def unchanged(self): + return set(o for o in self.intersect + if self.past_dict[o] == self.current_dict[o]) + + +class auditlog_rule(models.Model): + _name = 'auditlog.rule' + _description = "Auditlog - Rule" + + name = fields.Char(u"Name", size=32, required=True) + model_id = fields.Many2one( + 'ir.model', u"Model", required=True, + help=u"Select model for which you want to generate log.") + user_ids = fields.Many2many( + 'res.users', + 'audittail_rules_users', + 'user_id', 'rule_id', + string=u"Users", + help=u"if User is not added then it will applicable for all users") + log_read = fields.Boolean( + u"Log Reads", + help=(u"Select this if you want to keep track of read/open on any " + u"record of the model of this rule")) + log_write = fields.Boolean( + u"Log Writes", default=True, + help=(u"Select this if you want to keep track of modification on any " + u"record of the model of this rule")) + log_unlink = fields.Boolean( + u"Log Deletes", default=True, + help=(u"Select this if you want to keep track of deletion on any " + u"record of the model of this rule")) + log_create = fields.Boolean( + u"Log Creates", default=True, + help=(u"Select this if you want to keep track of creation on any " + u"record of the model of this rule")) + # log_action = fields.Boolean( + # "Log Action", + # help=("Select this if you want to keep track of actions on the " + # "model of this rule")) + # log_workflow = fields.Boolean( + # "Log Workflow", + # help=("Select this if you want to keep track of workflow on any " + # "record of the model of this rule")) + state = fields.Selection( + [('draft', "Draft"), ('subscribed', "Subscribed")], + string=u"State", required=True, default='draft') + action_id = fields.Many2one( + 'ir.actions.act_window', string="Action") + + _sql_constraints = [ + ('model_uniq', 'unique(model_id)', + ("There is already a rule defined on this model\n" + "You cannot define another: please edit the existing one.")) + ] + + def _register_hook(self, cr, ids=None): + """Get all rules and apply them to log method calls.""" + super(auditlog_rule, self)._register_hook(cr) + if not hasattr(self.pool, '_auditlog_field_cache'): + self.pool._auditlog_field_cache = {} + if not hasattr(self.pool, '_auditlog_model_cache'): + self.pool._auditlog_model_cache = {} + if ids is None: + ids = self.search(cr, SUPERUSER_ID, [('state', '=', 'subscribed')]) + return self._patch_methods(cr, SUPERUSER_ID, ids) + + @api.multi + def _patch_methods(self): + """Patch ORM methods of models defined in rules to log their calls.""" + updated = False + model_cache = self.pool._auditlog_model_cache + for rule in self: + if rule.state != 'subscribed': + continue + if not self.pool.get(rule.model_id.model): + # ignore rules for models not loadable currently + continue + model_cache[rule.model_id.model] = rule.model_id.id + model_model = self.env[rule.model_id.model] + # CRUD + # -> create + check_attr = 'auditlog_ruled_create' + if getattr(rule, 'log_create') \ + and not hasattr(model_model, check_attr): + model_model._patch_method('create', self._make_create()) + setattr(model_model, check_attr, True) + updated = True + # -> read + check_attr = 'auditlog_ruled_read' + if getattr(rule, 'log_read') \ + and not hasattr(model_model, check_attr): + model_model._patch_method('read', self._make_read()) + setattr(model_model, check_attr, True) + updated = True + # -> write + check_attr = 'auditlog_ruled_write' + if getattr(rule, 'log_write') \ + and not hasattr(model_model, check_attr): + model_model._patch_method('write', self._make_write()) + setattr(model_model, check_attr, True) + updated = True + # -> unlink + check_attr = 'auditlog_ruled_unlink' + if getattr(rule, 'log_unlink') \ + and not hasattr(model_model, check_attr): + model_model._patch_method('unlink', self._make_unlink()) + setattr(model_model, check_attr, True) + updated = True + return updated + + @api.multi + def _revert_methods(self): + """Restore original ORM methods of models defined in rules.""" + updated = False + for rule in self: + model_model = self.env[rule.model_id.model] + for method in ['create', 'read', 'write', 'unlink']: + if getattr(rule, 'log_%s' % method): + model_model._revert_method(method) + updated = True + if updated: + modules.registry.RegistryManager.signal_registry_change( + self.env.cr.dbname) + + # Unable to find a way to declare the `create` method with the new API, + # errors occurs with the `_register_hook()` BaseModel method. + def create(self, cr, uid, vals, context=None): + """Update the registry when a new rule is created.""" + res_id = super(auditlog_rule, self).create( + cr, uid, vals, context=context) + if self._register_hook(cr, [res_id]): + modules.registry.RegistryManager.signal_registry_change(cr.dbname) + return res_id + + # Unable to find a way to declare the `write` method with the new API, + # errors occurs with the `_register_hook()` BaseModel method. + def write(self, cr, uid, ids, vals, context=None): + """Update the registry when existing rules are updated.""" + if isinstance(ids, (int, long)): + ids = [ids] + super(auditlog_rule, self).write(cr, uid, ids, vals, context=context) + if self._register_hook(cr, ids): + modules.registry.RegistryManager.signal_registry_change(cr.dbname) + return True + + def _make_create(self): + """Instanciate a create method that log its calls.""" + @api.model + def create(self, vals, **kwargs): + rule_model = self.env['auditlog.rule'] + new_record = create.origin(self, vals, **kwargs) + new_values = dict( + (d['id'], d) for d in new_record.sudo().read( + list(self._columns))) + rule_model.sudo().create_logs( + self.env.uid, self._name, new_record.ids, + 'create', None, new_values) + return new_record + return create + + def _make_read(self): + """Instanciate a read method that log its calls.""" + # FIXME: read() seems a bit tricky, improve to handle old/new api + + # @api.v7 + # def read(self, cr, user, ids, fields=None, context=None, + # load='_classic_read', **kwargs): + # print "LOG READ", fields, load, kwargs + # # avoid loops + # if self.env.context.get('auditlog_method_intercepted'): + # return read.origin( + # self, cr, user, ids, fields, context, load, **kwargs) + # # call original method with a modified context + # context = dict( + # self.env.context, auditlog_method_intercepted=True) + # result = read.origin( + # self.with_context(context), + # cr, user, ids, fields, context, load, **kwargs) + # print "RESULT", result + # return result + + # @api.v8 + # def read(self, fields=None, load='_classic_read', **kwargs): + # print "LOG READ", fields, load, kwargs + # # avoid loops + # if self.env.context.get('auditlog_method_intercepted'): + # return read.origin(self, fields, load, **kwargs) + # # call original method with a modified context + # context = dict( + # self.env.context, auditlog_method_intercepted=True) + # result = read.origin( + # self.with_context(context), fields, load, **kwargs) + # print "RESULT", result + # return result + + def read(self, *args, **kwargs): + result = read.origin(self, *args, **kwargs) + return result + return read + + def _make_write(self): + """Instanciate a write method that log its calls.""" + @api.multi + def write(self, vals, **kwargs): + rule_model = self.env['auditlog.rule'] + old_values = dict( + (d['id'], d) for d in self.sudo().read(list(self._columns))) + result = write.origin(self, vals, **kwargs) + new_values = dict( + (d['id'], d) for d in self.sudo().read(list(self._columns))) + rule_model.sudo().create_logs( + self.env.uid, self._name, self.ids, + 'write', old_values, new_values) + return result + return write + + def _make_unlink(self): + """Instanciate an unlink method that log its calls.""" + @api.multi + def unlink(self, **kwargs): + rule_model = self.env['auditlog.rule'] + old_values = dict( + (d['id'], d) for d in self.sudo().read(list(self._columns))) + rule_model.sudo().create_logs( + self.env.uid, self._name, self.ids, 'unlink', old_values) + return unlink.origin(self, **kwargs) + return unlink + + def create_logs(self, uid, res_model, res_ids, method, + old_values=None, new_values=None, + additional_log_values=None): + """Create logs. `old_values` and `new_values` are dictionnaries, e.g: + {RES_ID: {'FIELD': VALUE, ...}} + """ + if old_values is None: + old_values = EMPTY_DICT + if new_values is None: + new_values = EMPTY_DICT + log_model = self.env['auditlog.log'] + for res_id in res_ids: + model_model = self.env[res_model] + res_name = model_model.browse(res_id).name_get() + vals = { + 'name': res_name and res_name[0] and res_name[0][1] or False, + 'model_id': self.pool._auditlog_model_cache[res_model], + 'res_id': res_id, + 'method': method, + 'user_id': uid, + } + vals.update(additional_log_values or {}) + log = log_model.create(vals) + diff = DictDiffer( + new_values.get(res_id, EMPTY_DICT), + old_values.get(res_id, EMPTY_DICT)) + self._create_log_line_on_write( + log, diff.changed(), old_values, new_values) + self._create_log_line_on_create(log, diff.added(), new_values) + + def _get_field(self, model, field_name): + cache = self.pool._auditlog_field_cache + if field_name not in cache.get(model.model, {}): + cache.setdefault(model.model, {}) + # We use 'search()' then 'read()' instead of the 'search_read()' + # to take advantage of the 'classic_write' loading + field_model = self.env['ir.model.fields'] + field = field_model.search( + [('model_id', '=', model.id), ('name', '=', field_name)]) + field_data = field.read(load='_classic_write')[0] + cache[model.model][field_name] = field_data + return cache[model.model][field_name] + + def _create_log_line_on_write( + self, log, fields_list, old_values, new_values): + """Log field updated on a 'write' operation.""" + log_line_model = self.env['auditlog.log.line'] + for field_name in fields_list: + if field_name in FIELDS_BLACKLIST: + continue + field = self._get_field(log.model_id, field_name) + log_vals = self._prepare_log_line_vals_on_write( + log, field, old_values, new_values) + log_line_model.create(log_vals) + + def _prepare_log_line_vals_on_write( + self, log, field, old_values, new_values): + """Prepare the dictionary of values used to create a log line on a + 'write' operation. + """ + vals = { + 'field_id': field['id'], + 'log_id': log.id, + 'old_value': old_values[log.res_id][field['name']], + 'old_value_text': old_values[log.res_id][field['name']], + 'new_value': new_values[log.res_id][field['name']], + 'new_value_text': new_values[log.res_id][field['name']], + } + # for *2many fields, log the name_get + if field['relation'] and '2many' in field['ttype']: + # Filter IDs to prevent a 'name_get()' call on deleted resources + existing_ids = self.env[field['relation']]._search( + [('id', 'in', vals['old_value'])]) + old_value_text = [] + if existing_ids: + existing_values = self.env[field['relation']].browse( + existing_ids).name_get() + old_value_text.extend(existing_values) + # Deleted resources will have a 'DELETED' text representation + deleted_ids = set(vals['old_value']) - set(existing_ids) + for deleted_id in deleted_ids: + old_value_text.append((deleted_id, 'DELETED')) + vals['old_value_text'] = old_value_text + new_value_text = self.env[field['relation']].browse( + vals['new_value']).name_get() + vals['new_value_text'] = new_value_text + return vals + + def _create_log_line_on_create( + self, log, fields_list, new_values): + """Log field filled on a 'create' operation.""" + log_line_model = self.env['auditlog.log.line'] + for field_name in fields_list: + if field_name in FIELDS_BLACKLIST: + continue + field = self._get_field(log.model_id, field_name) + log_vals = self._prepare_log_line_vals_on_create( + log, field, new_values) + log_line_model.create(log_vals) + + def _prepare_log_line_vals_on_create(self, log, field, new_values): + """Prepare the dictionary of values used to create a log line on a + 'create' operation. + """ + vals = { + 'field_id': field['id'], + 'log_id': log.id, + 'old_value': False, + 'old_value_text': False, + 'new_value': new_values[log.res_id][field['name']], + 'new_value_text': new_values[log.res_id][field['name']], + } + if field['relation'] and '2many' in field['ttype']: + new_value_text = self.env[field['relation']].browse( + vals['new_value']).name_get() + vals['new_value_text'] = new_value_text + return vals + + @api.multi + def subscribe(self): + """Subscribe Rule for auditing changes on model and apply shortcut + to view logs on that model. + """ + act_window_model = self.env['ir.actions.act_window'] + model_data_model = self.env['ir.model.data'] + for rule in self: + # Create a shortcut to view logs + domain = "[('model_id', '=', %s), ('res_id', '=', active_id)]" % ( + rule.model_id.id) + vals = { + 'name': _(u"View logs"), + 'res_model': 'auditlog.log', + 'src_model': rule.model_id.model, + 'domain': domain, + } + act_window = act_window_model.sudo().create(vals) + rule.write({'state': 'subscribed', 'action_id': act_window.id}) + keyword = 'client_action_relate' + value = 'ir.actions.act_window,%s' % act_window.id + model_data_model.sudo().ir_set( + 'action', keyword, 'View_log_' + rule.model_id.model, + [rule.model_id.model], value, replace=True, + isobject=True, xml_id=False) + return True + + @api.multi + def unsubscribe(self): + """Unsubscribe Auditing Rule on model.""" + act_window_model = self.env['ir.actions.act_window'] + ir_values_model = self.env['ir.values'] + # Revert patched methods + self._revert_methods() + for rule in self: + # Remove the shortcut to view logs + act_window = act_window_model.search( + [('name', '=', 'View Log'), + ('res_model', '=', 'auditlog.log'), + ('src_model', '=', rule.model_id.model)]) + if act_window: + value = 'ir.actions.act_window,%s' % act_window.id + act_window.unlink() + ir_value = ir_values_model.search( + [('model', '=', rule.model_id.model), + ('value', '=', value)]) + if ir_value: + ir_value.unlink() + self.write({'state': 'draft'}) + return True diff --git a/auditlog/security/ir.model.access.csv b/auditlog/security/ir.model.access.csv new file mode 100644 index 000000000..1bb8381d0 --- /dev/null +++ b/auditlog/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_auditlog_rule_user,auditlog_rule_user,model_auditlog_rule,base.group_user,0,0,0,0 +access_auditlog_log_user,auditlog_log_user,model_auditlog_log,base.group_user,0,0,0,0 +access_auditlog_log_line_user,auditlog_log_line_user,model_auditlog_log_line,base.group_user,0,0,0,0 + +access_auditlog_rule_manager,auditlog_rule_manager,model_auditlog_rule,base.group_erp_manager,1,1,1,1 +access_auditlog_log_manager,auditlog_log_manager,model_auditlog_log,base.group_erp_manager,1,1,1,1 +access_auditlog_log_line_manager,auditlog_log_line_manager,model_auditlog_log_line,base.group_erp_manager,1,1,1,1 diff --git a/auditlog/tests/__init__.py b/auditlog/tests/__init__.py new file mode 100644 index 000000000..a688b88a7 --- /dev/null +++ b/auditlog/tests/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2015 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from . import test_auditlog diff --git a/auditlog/tests/test_auditlog.py b/auditlog/tests/test_auditlog.py new file mode 100644 index 000000000..42c4b2ccc --- /dev/null +++ b/auditlog/tests/test_auditlog.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2015 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from openerp.tests.common import TransactionCase + + +class TestAuditlog(TransactionCase): + def test_LogCreation(self): + """First test, caching some data.""" + auditlog_log = self.env['auditlog.log'] + groups_model_id = self.env.ref('base.model_res_groups').id + self.env['auditlog.rule'].create({ + 'name': 'testrule for groups', + 'model_id': groups_model_id, + 'log_create': True, + 'log_write': True, + 'log_unlink': True, + 'state': 'subscribed', + }) + group = self.env['res.groups'].create({ + 'name': 'testgroup1', + }) + self.assertTrue(auditlog_log.search([ + ('model_id', '=', groups_model_id), + ('method', '=', 'create'), + ('res_id', '=', group.id), + ])) + group.write({'name': 'Testgroup1'}) + self.assertTrue(auditlog_log.search([ + ('model_id', '=', groups_model_id), + ('method', '=', 'write'), + ('res_id', '=', group.id), + ])) + group.unlink() + self.assertTrue(auditlog_log.search([ + ('model_id', '=', groups_model_id), + ('method', '=', 'unlink'), + ('res_id', '=', group.id), + ])) + + def test_LogCreation2(self): + """Second test, using cached data of the first one.""" + self.env['res.groups'].create({ + 'name': 'testgroup2', + }) + + def test_LogCreation3(self): + """Third test, two groups, the latter being the parent of the former. + Then we remove it right after (with (2, X) tuple) to test the creation + of a 'write' log with a deleted resource (so with no text + representation). + """ + testgroup3 = self.env['res.groups'].create({ + 'name': 'testgroup3', + }) + testgroup4 = self.env['res.groups'].create({ + 'name': 'testgroup4', + 'implied_ids': [(4, testgroup3.id)], + }) + testgroup4.write({'implied_ids': [(2, testgroup3.id)]}) diff --git a/auditlog/views/auditlog_view.xml b/auditlog/views/auditlog_view.xml new file mode 100644 index 000000000..e44c6cce9 --- /dev/null +++ b/auditlog/views/auditlog_view.xml @@ -0,0 +1,201 @@ + + + + + + + + + + + auditlog.rule.form + auditlog.rule + +
+
+
+ + + + + + + + + + + + + + + + + + +
+
+
+ + + auditlog.rule.tree + auditlog.rule + + + + + + + + + + + + + + + + + auditlog.rule.search + auditlog.rule + + + + + + + + + + + + + + + Rules + auditlog.rule + ir.actions.act_window + form + tree,form + {} + + + + + + + + + + auditlog.log.form + auditlog.log + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + auditlog.log.tree + auditlog.log + + + + + + + + + + + + + + auditlog.log.search + auditlog.log + + + + + + + + + + + + + + + + + + Logs + auditlog.log + form + + + + + +
+