diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b402b6c0..983d65ca7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,20 +36,19 @@ jobs: matrix: include: - container: ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest - include: "database_cleanup" + include: "database_cleanup,tracking_manager" makepot: "true" name: test with Odoo - container: ghcr.io/oca/oca-ci/py3.10-ocb16.0:latest - include: "database_cleanup" + include: "database_cleanup,tracking_manager" name: test with OCB - container: ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest - exclude: "database_cleanup" + exclude: "database_cleanup,tracking_manager" makepot: "true" name: test with Odoo - container: ghcr.io/oca/oca-ci/py3.10-ocb16.0:latest - exclude: "database_cleanup" + exclude: "database_cleanup,tracking_manager" name: test with OCB - makepot: "true" services: postgres: image: postgres:12.0 diff --git a/setup/tracking_manager/odoo/addons/tracking_manager b/setup/tracking_manager/odoo/addons/tracking_manager new file mode 120000 index 000000000..6be6653c3 --- /dev/null +++ b/setup/tracking_manager/odoo/addons/tracking_manager @@ -0,0 +1 @@ +../../../../tracking_manager \ No newline at end of file diff --git a/setup/tracking_manager/setup.py b/setup/tracking_manager/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/tracking_manager/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/tracking_manager/README.rst b/tracking_manager/README.rst new file mode 100644 index 000000000..c7842b7d3 --- /dev/null +++ b/tracking_manager/README.rst @@ -0,0 +1,107 @@ +================ +Tracking Manager +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:53b3c1bfe2031ada04d6f0a2fc177a5a5b39e8aa9a5e150a5b01f7b83b6af0c5 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/16.0/tracking_manager + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-tracking_manager + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to track all fields on every model that has a chatter, including one2many and many2many ones. This excludes the computed, readonly, related fields by default. +In addition, line changes of a one2many field can be tracked (e.g. product_uom_qty of an order_line in a sale order). + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +- In setting > models: select a model +- Check "Active" under Custom Tracking. +- You have two options - 1) manually configure tracked fields one by one, or 2) determine tracked fields based on a specific domain. +- For 1) manually configure tracked fields one by one + - Click on Tracked Fields smart button, and select/unselect Custom Tracking. +- For 2) determine tracked fields based on a specific domain + - Select "Automatic configuration", and then set the domain accordingly. + - Click "Update" for the domain to take effect. + +.. image:: https://raw.githubusercontent.com/OCA/server-tools/16.0/tracking_manager/static/description/model_view.png + +- Then select the fields to track + +.. image:: https://raw.githubusercontent.com/OCA/server-tools/16.0/tracking_manager/static/description/fields.png + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Kévin Roche +* Sébastien BEAU + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +.. |maintainer-Kev-Roche| image:: https://github.com/Kev-Roche.png?size=40px + :target: https://github.com/Kev-Roche + :alt: Kev-Roche +.. |maintainer-sebastienbeau| image:: https://github.com/sebastienbeau.png?size=40px + :target: https://github.com/sebastienbeau + :alt: sebastienbeau + +Current `maintainers `__: + +|maintainer-Kev-Roche| |maintainer-sebastienbeau| + +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/tracking_manager/__init__.py b/tracking_manager/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/tracking_manager/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/tracking_manager/__manifest__.py b/tracking_manager/__manifest__.py new file mode 100644 index 000000000..cf1d8fe32 --- /dev/null +++ b/tracking_manager/__manifest__.py @@ -0,0 +1,29 @@ +# Copyright 2022 Akretion (https://www.akretion.com). +# @author Kévin Roche +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Tracking Manager", + "summary": """This module tracks all fields of a model, + including one2many and many2many ones.""", + "version": "16.0.1.0.0", + "category": "Tools", + "website": "https://github.com/OCA/server-tools", + "author": "Akretion, Odoo Community Association (OCA)", + "maintainers": ["Kev-Roche", "sebastienbeau"], + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "base", + "mail", + ], + "external_dependencies": { + "python": ["odoo_test_helper"], + }, + "data": [ + "views/ir_model_fields.xml", + "views/ir_model.xml", + "views/message_template.xml", + ], +} diff --git a/tracking_manager/i18n/fr.po b/tracking_manager/i18n/fr.po new file mode 100644 index 000000000..67d34003c --- /dev/null +++ b/tracking_manager/i18n/fr.po @@ -0,0 +1,281 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * tracking_manager +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-10-20 16:02+0000\n" +"PO-Revision-Date: 2022-10-20 18:03+0200\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: \n" +"X-Generator: Poedit 3.1.1\n" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "Change :" +msgstr "Modifié :" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "Delete :" +msgstr "Supprimé :" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "New :" +msgstr "Nouveau :" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Active" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__active_custom_tracking +#, fuzzy +msgid "Active Custom Tracking" +msgstr "Suivi personnalisé" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__assigned_attachment_ids +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__assigned_attachment_ids +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__assigned_attachment_ids +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__assigned_attachment_ids +msgid "Assigned Attachments" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__automatic_custom_tracking +#, fuzzy +msgid "Automatic Custom Tracking" +msgstr "Suivi personnalisé" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__automatic_custom_tracking_domain +#, fuzzy +msgid "Automatic Custom Tracking Domain" +msgstr "Suivi personnalisé" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Automatic configuration" +msgstr "" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_base +msgid "Base" +msgstr "" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "Changed" +msgstr "Modifié" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__changeset_change_ids +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__changeset_change_ids +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__changeset_change_ids +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__changeset_change_ids +msgid "Changeset Changes" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__changeset_ids +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__changeset_ids +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__changeset_ids +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__changeset_ids +#, fuzzy +msgid "Changesets" +msgstr "Modifié" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__count_pending_changeset_changes +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__count_pending_changeset_changes +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__count_pending_changeset_changes +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__count_pending_changeset_changes +msgid "Count Pending Changeset Changes" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__count_pending_changesets +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__count_pending_changesets +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__count_pending_changesets +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__count_pending_changesets +msgid "Count Pending Changesets" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__custom_tracking +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Custom Tracking" +msgstr "Suivi personnalisé" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_track_fields_search +#, fuzzy +msgid "Custom Tracking OFF" +msgstr "Suivi personnalisé" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_track_fields_search +#, fuzzy +msgid "Custom Tracking ON" +msgstr "Suivi personnalisé" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__display_name +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__display_name +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__display_name +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Domain" +msgstr "" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_mail_thread +msgid "Email Thread" +msgstr "Discussion par email" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_ir_model_fields +msgid "Fields" +msgstr "Champs" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__id +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__id +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__id +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__id +msgid "ID" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,help:tracking_manager.field_ir_model__automatic_custom_tracking +msgid "If tick new field will be automatically tracked if the domain match" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model____last_update +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields____last_update +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread____last_update +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_mail_tracking_value +#, fuzzy +msgid "Mail Tracking Value" +msgstr "Suivi natif" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_ir_model +msgid "Models" +msgstr "Modèles" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__native_tracking +msgid "Native Tracking" +msgstr "Suivi natif" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__smart_search +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__smart_search +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__smart_search +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__smart_search +msgid "Smart Search" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__trackable +msgid "Trackable" +msgstr "" + +#. module: tracking_manager +#: model:ir.actions.act_window,name:tracking_manager.ir_model_fields_action +#, fuzzy +msgid "Trackable Fields" +msgstr "Champs avec suivi personnalisé" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__tracked_field_count +#, fuzzy +msgid "Tracked Field Count" +msgstr "Champs avec suivi personnalisé" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Tracked Fields" +msgstr "Champs avec suivi personnalisé" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Update" +msgstr "" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Update fields configuration" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__user_can_see_changeset +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__user_can_see_changeset +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__user_can_see_changeset +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__user_can_see_changeset +msgid "User Can See Changeset" +msgstr "" + +#~ msgid "" +#~ "Add tracking on all this model fields if they are not readonly True, " +#~ "neither computed." +#~ msgstr "" +#~ "Active le suivi des champs de ce modèles qui ne sont pas en lecture " +#~ "seule, ni reliés, ni calculés." + +#~ msgid "Apply custom tracking on fields" +#~ msgstr "Active le suivi personnalisé des champs" + +#~ msgid "Created by" +#~ msgstr "Créé par" + +#~ msgid "Created on" +#~ msgstr "Créé le" + +#~ msgid "Custom Tracked fields" +#~ msgstr "Champs Suivis" + +#~ msgid "Field" +#~ msgstr "Champ" + +#~ msgid "Field Count" +#~ msgstr "Nb de champs" + +#~ msgid "Field Name" +#~ msgstr "Nom du champs" + +#~ msgid "Kind Field" +#~ msgstr "Type de champs" + +#~ msgid "One2many Models" +#~ msgstr "Modèles One2many" + +#~ msgid "One2many related models" +#~ msgstr "Modèles One2many présents" + +#~ msgid "Tracking" +#~ msgstr "Suivi" + +#~ msgid "Tracking Model Field" +#~ msgstr "Model suivi" diff --git a/tracking_manager/i18n/tracking_manager.pot b/tracking_manager/i18n/tracking_manager.pot new file mode 100644 index 000000000..a5aab58fa --- /dev/null +++ b/tracking_manager/i18n/tracking_manager.pot @@ -0,0 +1,225 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * tracking_manager +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \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: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "Change :" +msgstr "" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "Delete :" +msgstr "" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "New :" +msgstr "" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Active" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__active_custom_tracking +msgid "Active Custom Tracking" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__assigned_attachment_ids +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__assigned_attachment_ids +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__assigned_attachment_ids +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__assigned_attachment_ids +msgid "Assigned Attachments" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__automatic_custom_tracking +msgid "Automatic Custom Tracking" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__automatic_custom_tracking_domain +msgid "Automatic Custom Tracking Domain" +msgstr "" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Automatic configuration" +msgstr "" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_base +msgid "Base" +msgstr "" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "Changed" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__changeset_change_ids +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__changeset_change_ids +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__changeset_change_ids +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__changeset_change_ids +msgid "Changeset Changes" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__changeset_ids +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__changeset_ids +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__changeset_ids +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__changeset_ids +msgid "Changesets" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__count_pending_changeset_changes +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__count_pending_changeset_changes +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__count_pending_changeset_changes +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__count_pending_changeset_changes +msgid "Count Pending Changeset Changes" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__count_pending_changesets +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__count_pending_changesets +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__count_pending_changesets +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__count_pending_changesets +msgid "Count Pending Changesets" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__custom_tracking +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Custom Tracking" +msgstr "" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_track_fields_search +msgid "Custom Tracking OFF" +msgstr "" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_track_fields_search +msgid "Custom Tracking ON" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__display_name +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__display_name +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__display_name +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__display_name +msgid "Display Name" +msgstr "" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Domain" +msgstr "" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_mail_thread +msgid "Email Thread" +msgstr "" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_ir_model_fields +msgid "Fields" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__id +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__id +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__id +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__id +msgid "ID" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,help:tracking_manager.field_ir_model__automatic_custom_tracking +msgid "If tick new field will be automatically tracked if the domain match" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model____last_update +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields____last_update +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread____last_update +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value____last_update +msgid "Last Modified on" +msgstr "" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_mail_tracking_value +msgid "Mail Tracking Value" +msgstr "" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_ir_model +msgid "Models" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__native_tracking +msgid "Native Tracking" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__smart_search +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__smart_search +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__smart_search +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__smart_search +msgid "Smart Search" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__trackable +msgid "Trackable" +msgstr "" + +#. module: tracking_manager +#: model:ir.actions.act_window,name:tracking_manager.ir_model_fields_action +msgid "Trackable Fields" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__tracked_field_count +msgid "Tracked Field Count" +msgstr "" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Tracked Fields" +msgstr "" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Update" +msgstr "" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Update fields configuration" +msgstr "" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__user_can_see_changeset +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__user_can_see_changeset +#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__user_can_see_changeset +#: model:ir.model.fields,field_description:tracking_manager.field_mail_tracking_value__user_can_see_changeset +msgid "User Can See Changeset" +msgstr "" diff --git a/tracking_manager/models/__init__.py b/tracking_manager/models/__init__.py new file mode 100644 index 000000000..b6ac322dc --- /dev/null +++ b/tracking_manager/models/__init__.py @@ -0,0 +1,4 @@ +from . import mail_thread +from . import ir_model +from . import models +from . import mail_tracking_value diff --git a/tracking_manager/models/ir_model.py b/tracking_manager/models/ir_model.py new file mode 100644 index 000000000..07cd8a9b6 --- /dev/null +++ b/tracking_manager/models/ir_model.py @@ -0,0 +1,204 @@ +# Copyright (C) 2022 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from ast import literal_eval + +from odoo import api, fields, models, tools + + +class IrModel(models.Model): + _inherit = "ir.model" + + active_custom_tracking = fields.Boolean() + tracked_field_count = fields.Integer(compute="_compute_tracked_field_count") + automatic_custom_tracking = fields.Boolean( + compute="_compute_automatic_custom_tracking", + readonly=False, + store=True, + help=("If tick new field will be automatically tracked if the domain match"), + ) + automatic_custom_tracking_domain = fields.Char( + compute="_compute_automatic_custom_tracking_domain", + store=True, + readonly=False, + ) + + @tools.ormcache() + def _get_custom_tracked_fields_per_model(self): + models = self.sudo().search([("active_custom_tracking", "=", True)]) + return { + model.model: model.field_id.filtered( + lambda f: f.custom_tracking + and self.env[model.model]._fields.get(f.name) + ).mapped("name") + for model in models + if model.model in self.env + } + + @tools.ormcache() + def _get_model_tracked_by_o2m(self): + """For each model tracked due to a o2m relation + compute the information of + - the fields to track + - the 'notify" field to found the related record to post the message + return example + { + "res.partner.bank": { + "fields": ["acc_holder_name", "acc_number", ...], + "notify": [["partner_id", "bank_ids"]], + } + } + """ + self = self.sudo() + fields = self.env["ir.model.fields"].search( + [ + ("custom_tracking", "=", True), + ("model_id.active_custom_tracking", "=", True), + ("ttype", "=", "one2many"), + ] + ) + related_models = self.env["ir.model"].search( + [ + ("model", "in", fields.mapped("relation")), + ] + ) + custom_tracked_fields = self._get_custom_tracked_fields_per_model() + res = {} + for model in related_models: + if model.model not in self.env: + # If the model do not exist skip it (ex: during module update) + continue + if model.model in custom_tracked_fields: + tracked_fields = custom_tracked_fields[model.model] + else: + tracked_fields = model.field_id.filtered( + lambda s: not s.readonly + and not s.related + and not s.ttype == "one2many" + and s.name in self.env[model.model]._fields + ).mapped("name") + res[model.model] = {"fields": tracked_fields, "notify": []} + + for field in fields: + model_name = field.model_id.model + if ( + model_name in self.env + and self.env[model_name]._fields.get(field.name) + and field.relation in res + ): + res[field.relation]["notify"].append( + [self.env[model_name]._fields[field.name].inverse_name, field.name] + ) + return res + + @api.depends("active_custom_tracking") + def _compute_automatic_custom_tracking(self): + for record in self: + record.automatic_custom_tracking = False + + def _default_automatic_custom_tracking_domain_rules(self): + return { + "product.product": [ + "|", + ("ttype", "!=", "one2many"), + ("name", "in", ["barcode_ids"]), + ], + "sale.order": [ + "|", + ("ttype", "!=", "one2many"), + ("name", "in", ["order_line"]), + ], + "account.move": [ + "|", + ("ttype", "!=", "one2many"), + ("name", "in", ["invoice_line_ids"]), + ], + "default_automatic_rule": [("ttype", "!=", "one2many")], + } + + @api.depends("automatic_custom_tracking") + def _compute_automatic_custom_tracking_domain(self): + rules = self._default_automatic_custom_tracking_domain_rules() + for record in self: + record.automatic_custom_tracking_domain = str( + rules.get(record.model) or rules.get("default_automatic_rule") + ) + + def update_custom_tracking(self): + for record in self: + fields = record.field_id.filtered("trackable").filtered_domain( + literal_eval(record.automatic_custom_tracking_domain) + ) + fields.write({"custom_tracking": True}) + untrack_fields = record.field_id - fields + untrack_fields.write({"custom_tracking": False}) + + @api.depends("field_id.custom_tracking") + def _compute_tracked_field_count(self): + for rec in self: + rec.tracked_field_count = len(rec.field_id.filtered("custom_tracking")) + + def write(self, vals): + if "active_custom_tracking" in vals: + self.clear_caches() + return super().write(vals) + + +class IrModelFields(models.Model): + _inherit = "ir.model.fields" + + custom_tracking = fields.Boolean( + compute="_compute_custom_tracking", + store=True, + readonly=False, + ) + native_tracking = fields.Boolean( + compute="_compute_native_tracking", + store=True, + ) + trackable = fields.Boolean( + compute="_compute_trackable", + store=True, + ) + + @api.depends("native_tracking") + def _compute_custom_tracking(self): + for record in self: + if record.model_id.automatic_custom_tracking: + domain = literal_eval(record.model_id.automatic_custom_tracking_domain) + record.custom_tracking = bool(record.filtered_domain(domain)) + else: + record.custom_tracking = record.native_tracking + + @api.depends("tracking") + def _compute_native_tracking(self): + for record in self: + record.native_tracking = bool(record.tracking) + + @api.depends("readonly", "related", "store") + def _compute_trackable(self): + blacklists = [ + "activity_ids", + "message_ids", + "message_last_post", + "message_main_attachment", + "message_main_attachement_id", + ] + for rec in self: + rec.trackable = ( + rec.name not in blacklists + and rec.store + and not rec.readonly + and not rec.related + ) + + def write(self, vals): + custom_tracking = None + if "custom_tracking" in vals: + self.clear_caches() + self.check_access_rights("write") + custom_tracking = vals.pop("custom_tracking") + self._write({"custom_tracking": custom_tracking}) + self.invalidate_model(fnames=["custom_tracking"]) + return super().write(vals) diff --git a/tracking_manager/models/mail_thread.py b/tracking_manager/models/mail_thread.py new file mode 100644 index 000000000..bcb670198 --- /dev/null +++ b/tracking_manager/models/mail_thread.py @@ -0,0 +1,17 @@ +# Copyright 2022 Akretion (https://www.akretion.com). +# @author Kévin Roche +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models, tools + + +class MailThread(models.AbstractModel): + _inherit = "mail.thread" + + @tools.ormcache("self.env.uid", "self.env.su") + def _track_get_fields(self): + fields_per_models = self.env["ir.model"]._get_custom_tracked_fields_per_model() + if self._name in fields_per_models: + return set(self.fields_get(fields_per_models[self._name])) + else: + return super()._track_get_fields() diff --git a/tracking_manager/models/mail_tracking_value.py b/tracking_manager/models/mail_tracking_value.py new file mode 100644 index 000000000..34338f040 --- /dev/null +++ b/tracking_manager/models/mail_tracking_value.py @@ -0,0 +1,28 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + +from ..tools import format_m2m + + +class MailTrackingValue(models.Model): + _inherit = "mail.tracking.value" + + def create_tracking_values( + self, + initial_value, + new_value, + col_name, + col_info, + tracking_sequence, + model_name, + ): + if col_info["type"] == "many2many": + col_info["type"] = "text" + initial_value = format_m2m(initial_value) + new_value = format_m2m(new_value) + return super().create_tracking_values( + initial_value, new_value, col_name, col_info, tracking_sequence, model_name + ) diff --git a/tracking_manager/models/models.py b/tracking_manager/models/models.py new file mode 100644 index 000000000..ef523831f --- /dev/null +++ b/tracking_manager/models/models.py @@ -0,0 +1,156 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from collections import defaultdict + +from odoo import api, models, tools + +from ..tools import format_m2m + +# To avoid conflict with other module and avoid too long function name +# specific tracking_manager method are prefixed with _tm + + +class Base(models.AbstractModel): + _inherit = "base" + + @tools.ormcache() + def is_tracked_by_o2m(self): + return self._name in self.env["ir.model"]._get_model_tracked_by_o2m() + + def _tm_get_fields_to_notify(self): + return ( + self.env["ir.model"] + ._get_model_tracked_by_o2m() + .get(self._name, {}) + .get("notify", []) + ) + + def _tm_get_fields_to_track(self): + # We track manually + # all fields that belong to a model tracked via a one2many + # all the many2many fields + return ( + self.env["ir.model"] + ._get_model_tracked_by_o2m() + .get(self._name, {}) + .get("fields", []) + ) + + def _tm_notify_owner(self, mode, changes=None): + """Notify all model that have a one2many linked to the record changed""" + self.ensure_one() + data = self.env.cr.precommit.data.setdefault( + "tracking.manager.data", + defaultdict(lambda: defaultdict(lambda: defaultdict(list))), + ) + for field_name, owner_field_name in self._tm_get_fields_to_notify(): + owner = self[field_name] + data[owner._name][owner.id][owner_field_name].append( + { + "mode": mode, + "record": self.display_name, + "changes": changes, + } + ) + + def _tm_get_field_description(self, field_name): + return self._fields[field_name].get_description(self.env)["string"] + + def _tm_get_changes(self, values): + self.ensure_one() + changes = [] + for field_name, before in values.items(): + field = self._fields[field_name] + if before != self[field_name]: + if field.type == "many2many": + old = format_m2m(before) + new = format_m2m(self[field_name]) + elif field.type == "many2one": + old = before.display_name + new = self[field_name]["display_name"] + else: + old = before + new = self[field_name] + changes.append( + { + "name": self._tm_get_field_description(field_name), + "old": old, + "new": new, + } + ) + return changes + + def _tm_post_message(self, data): + for model_name, model_data in data.items(): + for record_id, messages_by_field in model_data.items(): + record = self.env[model_name].browse(record_id) + messages = [ + { + "name": record._tm_get_field_description(field_name), + "messages": messages, + } + for field_name, messages in messages_by_field.items() + ] + # use sudo as user may not have access to mail.message + record.sudo().message_post_with_view( + "tracking_manager.track_o2m_m2m_template", + values={"lines": messages}, + subtype_id=self.env.ref("mail.mt_note").id, + ) + + def _tm_prepare_o2m_tracking(self): + fnames = self._tm_get_fields_to_track() + if not fnames: + return + self.env.cr.precommit.add(self._tm_finalize_o2m_tracking) + initial_values = self.env.cr.precommit.data.setdefault( + f"tracking.manager.before.{self._name}", {} + ) + for record in self: + values = initial_values.setdefault(record.id, {}) + if values is not None: + for fname in fnames: + values.setdefault(fname, record[fname]) + + def _tm_finalize_o2m_tracking(self): + initial_values = self.env.cr.precommit.data.pop( + f"tracking.manager.before.{self._name}", {} + ) + for _id, values in initial_values.items(): + # Always use sudo in case that the record have been modified using sudo + record = self.sudo().browse(_id) + if not record.exists(): + # if a record have been modify and then deleted + # it's not need to track the change so skip it + continue + changes = record._tm_get_changes(values) + if changes: + record._tm_notify_owner("update", changes) + data = self.env.cr.precommit.data.pop("tracking.manager.data", {}) + self._tm_post_message(data) + self.flush_model() + + def _tm_track_create_unlink(self, mode): + self.env.cr.precommit.add(self._tm_finalize_o2m_tracking) + for record in self: + record._tm_notify_owner(mode) + + def write(self, vals): + if self.is_tracked_by_o2m(): + self._tm_prepare_o2m_tracking() + return super().write(vals) + + @api.model_create_multi + def create(self, list_vals): + records = super().create(list_vals) + if self.is_tracked_by_o2m(): + records._tm_track_create_unlink("create") + return records + + def unlink(self): + if self.is_tracked_by_o2m(): + self._tm_track_create_unlink("unlink") + return super().unlink() diff --git a/tracking_manager/readme/CONTRIBUTORS.rst b/tracking_manager/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..7ee0b5b5e --- /dev/null +++ b/tracking_manager/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Kévin Roche +* Sébastien BEAU diff --git a/tracking_manager/readme/DESCRIPTION.rst b/tracking_manager/readme/DESCRIPTION.rst new file mode 100644 index 000000000..e2624f5a4 --- /dev/null +++ b/tracking_manager/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module allows to track all fields on every model that has a chatter, including one2many and many2many ones. This excludes the computed, readonly, related fields by default. +In addition, line changes of a one2many field can be tracked (e.g. product_uom_qty of an order_line in a sale order). diff --git a/tracking_manager/readme/USAGE.rst b/tracking_manager/readme/USAGE.rst new file mode 100644 index 000000000..e36722a9e --- /dev/null +++ b/tracking_manager/readme/USAGE.rst @@ -0,0 +1,14 @@ +- In setting > models: select a model +- Check "Active" under Custom Tracking. +- You have two options - 1) manually configure tracked fields one by one, or 2) determine tracked fields based on a specific domain. +- For 1) manually configure tracked fields one by one + - Click on Tracked Fields smart button, and select/unselect Custom Tracking. +- For 2) determine tracked fields based on a specific domain + - Select "Automatic configuration", and then set the domain accordingly. + - Click "Update" for the domain to take effect. + +.. image:: ./static/description/model_view.png + +- Then select the fields to track + +.. image:: ./static/description/fields.png diff --git a/tracking_manager/static/description/fields.png b/tracking_manager/static/description/fields.png new file mode 100644 index 000000000..013a06f78 Binary files /dev/null and b/tracking_manager/static/description/fields.png differ diff --git a/tracking_manager/static/description/icon.png b/tracking_manager/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/tracking_manager/static/description/icon.png differ diff --git a/tracking_manager/static/description/index.html b/tracking_manager/static/description/index.html new file mode 100644 index 000000000..ab8d461d5 --- /dev/null +++ b/tracking_manager/static/description/index.html @@ -0,0 +1,444 @@ + + + + + + +Tracking Manager + + + +
+

Tracking Manager

+ + +

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

+

This module allows to track all fields on every model that has a chatter, including one2many and many2many ones. This excludes the computed, readonly, related fields by default. +In addition, line changes of a one2many field can be tracked (e.g. product_uom_qty of an order_line in a sale order).

+

Table of contents

+ +
+

Usage

+
    +
  • In setting > models: select a model
  • +
  • Check “Active” under Custom Tracking.
  • +
  • You have two options - 1) manually configure tracked fields one by one, or 2) determine tracked fields based on a specific domain.
  • +
  • For 1) manually configure tracked fields one by one +- Click on Tracked Fields smart button, and select/unselect Custom Tracking.
  • +
  • For 2) determine tracked fields based on a specific domain +- Select “Automatic configuration”, and then set the domain accordingly. +- Click “Update” for the domain to take effect.
  • +
+https://raw.githubusercontent.com/OCA/server-tools/16.0/tracking_manager/static/description/model_view.png +
    +
  • Then select the fields to track
  • +
+https://raw.githubusercontent.com/OCA/server-tools/16.0/tracking_manager/static/description/fields.png +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

Current maintainers:

+

Kev-Roche sebastienbeau

+

This module is part of the OCA/server-tools project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/tracking_manager/static/description/model_view.png b/tracking_manager/static/description/model_view.png new file mode 100644 index 000000000..bc04d2ea5 Binary files /dev/null and b/tracking_manager/static/description/model_view.png differ diff --git a/tracking_manager/tests/__init__.py b/tracking_manager/tests/__init__.py new file mode 100644 index 000000000..3e7e471c7 --- /dev/null +++ b/tracking_manager/tests/__init__.py @@ -0,0 +1 @@ +from . import test_tracking_manager diff --git a/tracking_manager/tests/models.py b/tracking_manager/tests/models.py new file mode 100644 index 000000000..1a6116dc0 --- /dev/null +++ b/tracking_manager/tests/models.py @@ -0,0 +1,13 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResPartnerBank(models.Model): + _inherit = "res.partner.bank" + + category_ids = fields.Many2many( + comodel_name="res.partner.category", string="Categories" + ) diff --git a/tracking_manager/tests/test_tracking_manager.py b/tracking_manager/tests/test_tracking_manager.py new file mode 100644 index 000000000..2ddde1a06 --- /dev/null +++ b/tracking_manager/tests/test_tracking_manager.py @@ -0,0 +1,268 @@ +# Copyright 2022 Akretion (https://www.akretion.com). +# @author Kévin Roche +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo_test_helper import FakeModelLoader + +from odoo.tests.common import TransactionCase + + +class TestTrackingManager(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Load fake models ->/ + cls.loader = FakeModelLoader(cls.env, cls.__module__) + cls.loader.backup_registry() + from .models import ResPartnerBank + + cls.loader.update_registry((ResPartnerBank,)) + + cls.partner_categ_1, cls.partner_categ_2, cls.partner_categ_3 = cls.env[ + "res.partner.category" + ].create( + [ + {"name": "FOO"}, + {"name": "BAR"}, + {"name": "TOOH"}, + ] + ) + cls.partner = cls.env["res.partner"].create( + { + "name": "Foo", + "bank_ids": [(0, 0, {"acc_number": "007"})], + "category_id": [(6, 0, [cls.partner_categ_1.id])], + } + ) + cls.partner_model = cls.env.ref("base.model_res_partner") + cls._active_tracking(["bank_ids", "category_id"]) + cls.flush_tracking() + cls.partner.message_ids.unlink() + + @classmethod + def tearDownClass(cls): + cls.loader.restore_registry() + super().tearDownClass() + + @classmethod + def _active_tracking(cls, fields_list): + cls.partner_model.active_custom_tracking = True + for field in cls._get_fields(fields_list): + field.custom_tracking = True + + @classmethod + def _get_fields(cls, fields_list): + return cls.partner_model.field_id.filtered(lambda s: s.name in fields_list) + + def test_not_tracked(self): + field = self._get_fields(["name"])[0] + self.assertFalse(field.native_tracking) + self.assertFalse(field.custom_tracking) + + def test_native_tracked(self): + field = self._get_fields(["email"])[0] + self.assertTrue(field.native_tracking) + self.assertTrue(field.custom_tracking) + + def test_update_tracked(self): + field = self._get_fields(["name"])[0] + self.assertFalse(field.native_tracking) + self.partner_model.automatic_custom_tracking = True + self.partner_model.update_custom_tracking() + self.assertTrue(field.custom_tracking) + + @classmethod + def flush_tracking(cls): + """Force the creation of tracking values.""" + cls.env["base"].flush_model() + cls.env.cr.precommit.run() + + @property + def messages(self): + # Force the creation of tracking values + self.flush_tracking() + return self.partner.message_ids + + def test_m2m_add_line(self): + self.partner.write( + { + "category_id": [ + ( + 6, + 0, + [ + self.partner_categ_1.id, + self.partner_categ_2.id, + ], + ) + ] + } + ) + self.assertEqual(len(self.messages), 1) + tracking = self.messages.tracking_value_ids + self.assertEqual(len(tracking), 1) + self.assertEqual(tracking.old_value_text, "FOO") + self.assertEqual(tracking.new_value_text, "FOO; BAR") + + def test_m2m_delete_line(self): + self.partner.write({"category_id": [(6, 0, [])]}) + self.assertEqual(len(self.messages), 1) + tracking = self.messages.tracking_value_ids + self.assertEqual(len(tracking), 1) + self.assertEqual(tracking.old_value_text, "FOO") + self.assertEqual(tracking.new_value_text, "") + + def test_m2m_multi_line(self): + self.partner.write( + { + "category_id": [ + ( + 6, + 0, + [ + self.partner_categ_2.id, + self.partner_categ_3.id, + ], + ) + ] + } + ) + self.assertEqual(len(self.messages), 1) + tracking = self.messages.tracking_value_ids + self.assertEqual(len(tracking), 1) + self.assertEqual(tracking.old_value_text, "FOO") + self.assertEqual(tracking.new_value_text, "BAR; TOOH") + + def test_o2m_create_indirectly(self): + self.partner.write({"bank_ids": [(0, 0, {"acc_number": "1234567890"})]}) + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("New"), 1) + + def test_o2m_unlink_indirectly(self): + self.partner.write({"bank_ids": [(2, self.partner.bank_ids[0].id)]}) + self.assertEqual(len(self.messages), 1) + self.assertIn("Delete", self.messages.body) + + def test_o2m_write_indirectly(self): + self.partner.write( + { + "bank_ids": [(1, self.partner.bank_ids[0].id, {"acc_number": "123"})], + } + ) + self.assertEqual(len(self.messages), 1) + self.assertIn("Change", self.messages.body) + + def test_o2m_write_indirectly_on_not_tracked_fields(self): + # Active custom tracking on res.partner.bank and remove tracking on acc_number + bank_model = self.env["ir.model"].search([("model", "=", "res.partner.bank")]) + bank_model.active_custom_tracking = True + acc_number = bank_model.field_id.filtered(lambda x: x.name == "acc_number") + acc_number.custom_tracking = False + self.partner.write( + { + "bank_ids": [(1, self.partner.bank_ids[0].id, {"acc_number": "123"})], + } + ) + self.assertEqual(len(self.messages), 0) + + def test_o2m_create_and_unlink_indirectly(self): + self.partner.write( + { + "bank_ids": [ + (2, self.partner.bank_ids[0].id, 0), + (0, 0, {"acc_number": "1234567890"}), + ] + } + ) + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("New"), 1) + self.assertEqual(self.messages.body.count("Delete"), 1) + + def test_o2m_update_m2m_indirectly(self): + self.partner.write( + { + "bank_ids": [ + ( + 1, + self.partner.bank_ids[0].id, + { + "category_ids": [ + ( + 6, + 0, + [self.partner_categ_1.id, self.partner_categ_2.id], + ) + ] + }, + ), + ] + } + ) + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("Changed"), 1) + + def test_o2m_update_m2o_indirectly(self): + self.partner.write( + { + "bank_ids": [ + ( + 1, + self.partner.bank_ids[0].id, + {"bank_id": self.env["res.bank"].create({"name": "DOO"}).id}, + ), + ] + } + ) + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("Changed"), 1) + + def test_o2m_write_and_unlink_indirectly(self): + # when editing a o2m in some special case + # like the computed field amount_tax of purchase order line + # some write can be done on a line before behind deleted + # line._compute_amount() is called manually inside see link behind + # https://github.com/odoo/odoo/blob/009f35f3d3659792ef18ac510a6ec323708becec/addons/purchase/models/purchase.py#L28 # noqa + # So we are in a case that we do some change and them we delete them + # in that case we should only have one message of deletation + # and no error + self.partner.write( + { + "bank_ids": [(1, self.partner.bank_ids[0].id, {"acc_number": "123"})], + } + ) + self.partner.write( + { + "bank_ids": [(2, self.partner.bank_ids[0].id, 0)], + } + ) + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("Change"), 0) + self.assertEqual(self.messages.body.count("Delete"), 1) + + def test_o2m_create_directly(self): + self.env["res.partner.bank"].create( + { + "acc_number": "1234567890", + "partner_id": self.partner.id, + } + ) + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("New"), 1) + + def test_o2m_unlink_directly(self): + self.partner.bank_ids.unlink() + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("Delete"), 1) + + def test_o2m_update_directly(self): + self.partner.bank_ids.write({"acc_number": "0987654321"}) + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("Change :"), 1) + + def test_o2m_write_and_unlink_directly(self): + # see explanation of test_o2m_write_and_unlink_indirectly + self.partner.bank_ids.write({"acc_number": "0987654321"}) + self.partner.bank_ids.unlink() + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("Change"), 0) + self.assertEqual(self.messages.body.count("Delete"), 1) diff --git a/tracking_manager/tools.py b/tracking_manager/tools.py new file mode 100644 index 000000000..1e1794d80 --- /dev/null +++ b/tracking_manager/tools.py @@ -0,0 +1,7 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +def format_m2m(records): + return "; ".join(records.mapped("display_name")) diff --git a/tracking_manager/views/ir_model.xml b/tracking_manager/views/ir_model.xml new file mode 100644 index 000000000..7cb5cade8 --- /dev/null +++ b/tracking_manager/views/ir_model.xml @@ -0,0 +1,66 @@ + + + + + tracking.ir.model form + ir.model + + + + + + + + + + + + + + diff --git a/tracking_manager/views/ir_model_fields.xml b/tracking_manager/views/ir_model_fields.xml new file mode 100644 index 000000000..048d68b20 --- /dev/null +++ b/tracking_manager/views/ir_model_fields.xml @@ -0,0 +1,59 @@ + + + + + + Trackable Fields + ir.actions.act_window + ir.model.fields + tree,form + [("trackable", "=", True), ("model_id", "=", context['active_id']), ("ttype", "!=", "binary")] + {} + current + + + + ir.model.fields + + + + + + + + + + + + + + tree + + + + + + ir.model.fields.search + ir.model.fields + + + + + + + + + + diff --git a/tracking_manager/views/message_template.xml b/tracking_manager/views/message_template.xml new file mode 100644 index 000000000..cc68cf77f --- /dev/null +++ b/tracking_manager/views/message_template.xml @@ -0,0 +1,52 @@ + + + + +