Browse Source
Merge pull request #119 from osiell/8.0
Merge pull request #119 from osiell/8.0
[ADD] Module 'auditlog' - A substitute to the deprecated 'audittrail' module for Odoo 8pull/93/merge
Stefan Rijnhart (Therp)
10 years ago
13 changed files with 1567 additions and 0 deletions
-
49auditlog/README.rst
-
44auditlog/__init__.py
-
38auditlog/__openerp__.py
-
279auditlog/i18n/auditlog.pot
-
295auditlog/i18n/fr.po
-
26auditlog/migrations/8.0.1.0/pre-migration.py
-
23auditlog/models/__init__.py
-
55auditlog/models/log.py
-
451auditlog/models/rule.py
-
8auditlog/security/ir.model.access.csv
-
21auditlog/tests/__init__.py
-
77auditlog/tests/test_auditlog.py
-
201auditlog/views/auditlog_view.xml
@ -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 <sebastien.alix@osiell.com> |
||||
|
* Holger Brunn <hbrunn@therp.nl> |
||||
|
|
||||
|
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. |
@ -0,0 +1,44 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# OpenERP, Open Source Management Solution |
||||
|
# Copyright (C) 2013 ABF OSIELL (<http://osiell.com>). |
||||
|
# |
||||
|
# 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 <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
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'") |
@ -0,0 +1,38 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# OpenERP, Open Source Management Solution |
||||
|
# Copyright (C) 2013 ABF OSIELL (<http://osiell.com>). |
||||
|
# |
||||
|
# 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 <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
{ |
||||
|
'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', |
||||
|
} |
@ -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 "" |
||||
|
|
@ -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" |
@ -0,0 +1,26 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# OpenERP, Open Source Management Solution |
||||
|
# This module copyright (C) 2015 Therp BV (<http://therp.nl>). |
||||
|
# |
||||
|
# 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 <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
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) |
@ -0,0 +1,23 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# OpenERP, Open Source Management Solution |
||||
|
# Copyright (C) 2013 ABF OSIELL (<http://osiell.com>). |
||||
|
# |
||||
|
# 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 <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
from . import rule |
||||
|
from . import log |
@ -0,0 +1,55 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# OpenERP, Open Source Management Solution |
||||
|
# Copyright (C) 2013 ABF OSIELL (<http://osiell.com>). |
||||
|
# |
||||
|
# 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 <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
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') |
@ -0,0 +1,451 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# OpenERP, Open Source Management Solution |
||||
|
# Copyright (C) 2013 ABF OSIELL (<http://osiell.com>). |
||||
|
# |
||||
|
# 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 <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
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 |
@ -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 |
@ -0,0 +1,21 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# OpenERP, Open Source Management Solution |
||||
|
# This module copyright (C) 2015 Therp BV (<http://therp.nl>). |
||||
|
# |
||||
|
# 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 <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
from . import test_auditlog |
@ -0,0 +1,77 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# OpenERP, Open Source Management Solution |
||||
|
# This module copyright (C) 2015 Therp BV (<http://therp.nl>). |
||||
|
# |
||||
|
# 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 <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
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)]}) |
@ -0,0 +1,201 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
|
||||
|
<menuitem id="menu_audit" name="Audit" |
||||
|
parent="base.menu_reporting" sequence="50" |
||||
|
groups="base.group_system"/> |
||||
|
|
||||
|
|
||||
|
<!-- auditlog.rule --> |
||||
|
|
||||
|
<record model="ir.ui.view" id="view_auditlog_rule_form"> |
||||
|
<field name="name">auditlog.rule.form</field> |
||||
|
<field name="model">auditlog.rule</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form string="Rule"> |
||||
|
<header> |
||||
|
<button string="Subscribe" name="subscribe" |
||||
|
type="object" states="draft" class="oe_highlight"/> |
||||
|
<button string="Unsubscribe" name="unsubscribe" |
||||
|
type="object" states="subscribed"/> |
||||
|
<field name="state" widget="statusbar"/> |
||||
|
</header> |
||||
|
<sheet> |
||||
|
<group string="Rule"> |
||||
|
<group colspan="1"> |
||||
|
<field name="name" required="1"/> |
||||
|
<field name="model_id"/> |
||||
|
<field name="action_id" readonly="1" groups="base.group_no_one"/> |
||||
|
</group> |
||||
|
<group colspan="1"> |
||||
|
<field name="log_read" invisible="1"/> |
||||
|
<field name="log_write"/> |
||||
|
<field name="log_unlink"/> |
||||
|
<field name="log_create"/> |
||||
|
<!--<field name="log_action"/>--> |
||||
|
<!--<field name="log_workflow"/>--> |
||||
|
</group> |
||||
|
</group> |
||||
|
<!-- |
||||
|
<group string="Users"> |
||||
|
<field name="user_ids" nolabel="1"/> |
||||
|
</group> |
||||
|
--> |
||||
|
</sheet> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record model="ir.ui.view" id="view_auditlog_rule_tree"> |
||||
|
<field name="name">auditlog.rule.tree</field> |
||||
|
<field name="model">auditlog.rule</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<tree colors="blue:state == 'draft';black:state == 'subscribed'" string="Rules"> |
||||
|
<field name="name"/> |
||||
|
<field name="model_id"/> |
||||
|
<field name="log_read"/> |
||||
|
<field name="log_write"/> |
||||
|
<field name="log_unlink"/> |
||||
|
<field name="log_create"/> |
||||
|
<!--<field name="log_action"/>--> |
||||
|
<!--<field name="log_workflow"/>--> |
||||
|
<field name="state"/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record id="view_auditlog_rule_search" model="ir.ui.view"> |
||||
|
<field name="name">auditlog.rule.search</field> |
||||
|
<field name="model">auditlog.rule</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<search string="Rules"> |
||||
|
<field name="name"/> |
||||
|
<filter name="state_draft" |
||||
|
domain="[('state','=','draft')]" string="Draft"/> |
||||
|
<filter name="state_subscribed" |
||||
|
domain="[('state','=','subscribed')]" string="Subscribed"/> |
||||
|
<field name="model_id"/> |
||||
|
<group expand="0" string="Group By..."> |
||||
|
<filter name="group_by_state" string="State" |
||||
|
domain="[]" context="{'group_by':'state'}"/> |
||||
|
</group> |
||||
|
</search> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record model="ir.actions.act_window" id="action_auditlog_rule_tree"> |
||||
|
<field name="name">Rules</field> |
||||
|
<field name="res_model">auditlog.rule</field> |
||||
|
<field name="type">ir.actions.act_window</field> |
||||
|
<field name="view_type">form</field> |
||||
|
<field name="view_mode">tree,form</field> |
||||
|
<field name="context">{}</field> |
||||
|
<field name="search_view_id" ref="view_auditlog_rule_search"/> |
||||
|
</record> |
||||
|
|
||||
|
<menuitem id="menu_action_auditlog_rule_tree" parent="menu_audit" action="action_auditlog_rule_tree"/> |
||||
|
|
||||
|
|
||||
|
<!-- auditlog.log --> |
||||
|
|
||||
|
<record model="ir.ui.view" id="view_auditlog_log_form"> |
||||
|
<field name="name">auditlog.log.form</field> |
||||
|
<field name="model">auditlog.log</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form string="Log"> |
||||
|
<sheet> |
||||
|
<group string="Log"> |
||||
|
<group colspan="1"> |
||||
|
<field name="create_date" readonly="1"/> |
||||
|
<field name="user_id" readonly="1"/> |
||||
|
<field name="method" readonly="1"/> |
||||
|
</group> |
||||
|
<group colspan="1"> |
||||
|
<field name="model_id" readonly="1"/> |
||||
|
<field name="res_id" readonly="1"/> |
||||
|
<field name="name" readonly="1"/> |
||||
|
</group> |
||||
|
</group> |
||||
|
<group string="Fields updated"> |
||||
|
<field name="line_ids" readonly="1" nolabel="1"> |
||||
|
<form string="Log - Field updated"> |
||||
|
<group> |
||||
|
<field name="field_id" readonly="1"/> |
||||
|
</group> |
||||
|
<group string="Values" col="4"> |
||||
|
<field name="old_value" readonly="1"/> |
||||
|
<field name="new_value" readonly="1"/> |
||||
|
<field name="old_value_text" readonly="1"/> |
||||
|
<field name="new_value_text" readonly="1"/> |
||||
|
</group> |
||||
|
</form> |
||||
|
<tree> |
||||
|
<field name="field_description"/> |
||||
|
<field name="field_name"/> |
||||
|
<!--<field name="old_value"/>--> |
||||
|
<field name="old_value_text"/> |
||||
|
<!--<field name="new_value"/>--> |
||||
|
<field name="new_value_text"/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
</group> |
||||
|
</sheet> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record model="ir.ui.view" id="view_auditlog_log_tree"> |
||||
|
<field name="name">auditlog.log.tree</field> |
||||
|
<field name="model">auditlog.log</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<tree string="Logs" create="false"> |
||||
|
<field name="create_date"/> |
||||
|
<field name="name"/> |
||||
|
<field name="model_id"/> |
||||
|
<field name="res_id"/> |
||||
|
<field name="method"/> |
||||
|
<field name="user_id"/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record id="view_auditlog_log_search" model="ir.ui.view"> |
||||
|
<field name="name">auditlog.log.search</field> |
||||
|
<field name="model">auditlog.log</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<search string="Logs"> |
||||
|
<field name="name"/> |
||||
|
<field name="model_id"/> |
||||
|
<field name="res_id"/> |
||||
|
<field name="user_id"/> |
||||
|
<group expand="0" string="Group By..."> |
||||
|
<filter name="group_by_user_id" |
||||
|
string="User" |
||||
|
domain="[]" context="{'group_by':'user_id'}"/> |
||||
|
<filter name="group_by_model_id" |
||||
|
string="Model" |
||||
|
domain="[]" context="{'group_by':'model_id'}"/> |
||||
|
<filter name="group_by_res_id" |
||||
|
string="Resource ID" |
||||
|
domain="[]" context="{'group_by':'res_id'}"/> |
||||
|
<filter name="group_by_create_date" |
||||
|
string="Date" |
||||
|
domain="[]" context="{'group_by':'create_date'}"/> |
||||
|
</group> |
||||
|
</search> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record model="ir.actions.act_window" id="action_auditlog_log_tree"> |
||||
|
<field name="name">Logs</field> |
||||
|
<field name="res_model">auditlog.log</field> |
||||
|
<field name="view_type">form</field> |
||||
|
<field name="search_view_id" ref="view_auditlog_log_search"/> |
||||
|
</record> |
||||
|
|
||||
|
<menuitem id="menu_audit_logs" name="Logs" |
||||
|
parent="menu_audit" action="action_auditlog_log_tree"/> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue