diff --git a/tracking_manager/README.rst b/tracking_manager/README.rst new file mode 100644 index 000000000..997ae71b9 --- /dev/null +++ b/tracking_manager/README.rst @@ -0,0 +1,119 @@ +================ +Tracking Manager +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a7d5bd552b419ff6e2058562ce30c52387acbe3f01cbd1ec3804a78a9fc5a1c9 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/17.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-17-0/server-tools-17-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=17.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| + +- Then select the fields to track + +|image1| + +.. |image| image:: https://raw.githubusercontent.com/OCA/server-tools/17.0/tracking_manager/static/description/model_view.png +.. |image1| image:: https://raw.githubusercontent.com/OCA/server-tools/17.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..d05a77bdb --- /dev/null +++ b/tracking_manager/__manifest__.py @@ -0,0 +1,23 @@ +# 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": "17.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": ["mail"], + "data": [ + "views/ir_model_fields.xml", + "views/ir_model.xml", + "views/message_template.xml", + ], +} diff --git a/tracking_manager/i18n/es.po b/tracking_manager/i18n/es.po new file mode 100644 index 000000000..7fbd22830 --- /dev/null +++ b/tracking_manager/i18n/es.po @@ -0,0 +1,150 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * tracking_manager +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-02-14 15:37+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "Change :" +msgstr "Cambio :" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "Delete :" +msgstr "Eliminar:" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "New :" +msgstr " Nuevo: " + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Active" +msgstr "Activo" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__active_custom_tracking +msgid "Active Custom Tracking" +msgstr "Seguimiento Personalizado Activo" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__automatic_custom_tracking +msgid "Automatic Custom Tracking" +msgstr "Seguimiento Automático Personalizado" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__automatic_custom_tracking_domain +msgid "Automatic Custom Tracking Domain" +msgstr "Dominio de Seguimiento Automático Personalizado" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Automatic configuration" +msgstr "Configuración automática" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_base +msgid "Base" +msgstr "Base" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "Changed" +msgstr "Cambiado" + +#. 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 "Seguimiento Personalizado" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_track_fields_search +msgid "Custom Tracking OFF" +msgstr "Seguimiento Personalizado OFF" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_track_fields_search +msgid "Custom Tracking ON" +msgstr "Seguimiento Personalizado ON" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Domain" +msgstr "Dominio" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_mail_thread +msgid "Email Thread" +msgstr "Hilo de Correo Electrónico" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_ir_model_fields +msgid "Fields" +msgstr "Campos" + +#. 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 "" +"Si se marca el nuevo campo se rastreará automáticamente si el dominio " +"coincide" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_mail_tracking_value +msgid "Mail Tracking Value" +msgstr "Valor de Seguimiento del Correo" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_ir_model +msgid "Models" +msgstr "Modelos" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__native_tracking +msgid "Native Tracking" +msgstr "Seguimiento Nativo" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__trackable +msgid "Trackable" +msgstr "Rastreable" + +#. module: tracking_manager +#: model:ir.actions.act_window,name:tracking_manager.ir_model_fields_action +msgid "Trackable Fields" +msgstr "Campos Rastreables" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__tracked_field_count +msgid "Tracked Field Count" +msgstr "Recuento de Campos Rastreados" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Tracked Fields" +msgstr "Campos Rastreados" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Update" +msgstr "Actualización" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Update fields configuration" +msgstr "Actualizar configuración de campos" diff --git a/tracking_manager/i18n/fr.po b/tracking_manager/i18n/fr.po new file mode 100644 index 000000000..bdf20ce25 --- /dev/null +++ b/tracking_manager/i18n/fr.po @@ -0,0 +1,210 @@ +# 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__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_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_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,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,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_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 "" + +#, fuzzy +#~ msgid "Changesets" +#~ msgstr "Modifié" + +#~ msgid "Display Name" +#~ msgstr "Nom affiché" + +#~ msgid "Last Modified on" +#~ msgstr "Dernière modification le" + +#~ 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/it.po b/tracking_manager/i18n/it.po new file mode 100644 index 000000000..49de622cf --- /dev/null +++ b/tracking_manager/i18n/it.po @@ -0,0 +1,150 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * tracking_manager +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-01-18 09:35+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "Change :" +msgstr "Modifica:" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "Delete :" +msgstr "Cancella:" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "New :" +msgstr "Nuovo:" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Active" +msgstr "Attivo" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__active_custom_tracking +msgid "Active Custom Tracking" +msgstr "Attiva tracciamento personalizzato" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__automatic_custom_tracking +msgid "Automatic Custom Tracking" +msgstr "Tracciamento personalizzato automatico" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__automatic_custom_tracking_domain +msgid "Automatic Custom Tracking Domain" +msgstr "Dominio tracciamento personalizzato automatico" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Automatic configuration" +msgstr "Configurazione automatica" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_base +msgid "Base" +msgstr "Base" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template +msgid "Changed" +msgstr "Modificato" + +#. 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 "Tracciamento personalizzato" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_track_fields_search +msgid "Custom Tracking OFF" +msgstr "Tracciamento personalizzato spento" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_track_fields_search +msgid "Custom Tracking ON" +msgstr "Tracciamento personalizzato acceso" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Domain" +msgstr "Dominio" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_mail_thread +msgid "Email Thread" +msgstr "Discussione e-mail" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_ir_model_fields +msgid "Fields" +msgstr "Campi" + +#. 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 "" +"Se toccare un nuovo campo viene tracciato automaticamente se il dominio " +"corrisponde" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_mail_tracking_value +msgid "Mail Tracking Value" +msgstr "Valore tracciamento e-mail" + +#. module: tracking_manager +#: model:ir.model,name:tracking_manager.model_ir_model +msgid "Models" +msgstr "Modelli" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__native_tracking +msgid "Native Tracking" +msgstr "Tracciamento nativo" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__trackable +msgid "Trackable" +msgstr "Tracciabile" + +#. module: tracking_manager +#: model:ir.actions.act_window,name:tracking_manager.ir_model_fields_action +msgid "Trackable Fields" +msgstr "Campi tracciabili" + +#. module: tracking_manager +#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__tracked_field_count +msgid "Tracked Field Count" +msgstr "Conteggio campi tracciati" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Tracked Fields" +msgstr "Campi tracciati" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Update" +msgstr "Aggiorna" + +#. module: tracking_manager +#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form +msgid "Update fields configuration" +msgstr "Aggiorna configurazione campi" diff --git a/tracking_manager/i18n/tracking_manager.pot b/tracking_manager/i18n/tracking_manager.pot new file mode 100644 index 000000000..d7720617c --- /dev/null +++ b/tracking_manager/i18n/tracking_manager.pot @@ -0,0 +1,145 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * tracking_manager +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.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__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_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_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,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,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_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 "" diff --git a/tracking_manager/models/__init__.py b/tracking_manager/models/__init__.py new file mode 100644 index 000000000..0617d405f --- /dev/null +++ b/tracking_manager/models/__init__.py @@ -0,0 +1,5 @@ +from . import mail_thread +from . import ir_model +from . import ir_model_fields +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..68e3e73d9 --- /dev/null +++ b/tracking_manager/models/ir_model.py @@ -0,0 +1,162 @@ +# 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 +from odoo.osv import expression + + +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 marked, the fields matching the matched by the domain" + " below will be automatically tracked for this model." + ), + ) + automatic_custom_tracking_domain = fields.Char( + string="Domain", + 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, model=model: 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, model=model: 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": [ + ("readonly", "=", False), + "|", + ("ttype", "!=", "one2many"), + ("name", "in", ["barcode_ids"]), + ], + "sale.order": [ + ("readonly", "=", False), + "|", + ("ttype", "!=", "one2many"), + ("name", "in", ["order_line"]), + ], + "account.move": [ + ("readonly", "=", False), + "|", + ("ttype", "!=", "one2many"), + ("name", "in", ["invoice_line_ids"]), + ], + "default_automatic_rule": [ + ("ttype", "!=", "one2many"), + ("readonly", "=", False), + ], + } + + @api.depends("automatic_custom_tracking") + def _compute_automatic_custom_tracking_domain(self): + rules = self._default_automatic_custom_tracking_domain_rules() + for record in self: + automatic_custom_tracking_domain = rules.get(record.model) or rules.get( + "default_automatic_rule", [] + ) + automatic_custom_tracking_domain = expression.AND( + [automatic_custom_tracking_domain, [("model", "=", record.model)]] + ) + record.automatic_custom_tracking_domain = str( + automatic_custom_tracking_domain + ) + + 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.env.registry.clear_cache() + return super().write(vals) diff --git a/tracking_manager/models/ir_model_fields.py b/tracking_manager/models/ir_model_fields.py new file mode 100644 index 000000000..46e28ab57 --- /dev/null +++ b/tracking_manager/models/ir_model_fields.py @@ -0,0 +1,62 @@ +# 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 + + +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("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.related + + def write(self, vals): + custom_tracking = None + if "custom_tracking" in vals: + self.env.registry.clear_cache() + 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..71c47567f --- /dev/null +++ b/tracking_manager/models/mail_tracking_value.py @@ -0,0 +1,27 @@ +# 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, + record, + ): + 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, record + ) diff --git a/tracking_manager/models/models.py b/tracking_manager/models/models.py new file mode 100644 index 000000000..360b996f7 --- /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_source( + "tracking_manager.track_o2m_m2m_template", + render_values={"lines": messages}, + subtype_xmlid="mail.mt_note", + ) + + 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/pyproject.toml b/tracking_manager/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/tracking_manager/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/tracking_manager/readme/CONTRIBUTORS.md b/tracking_manager/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..38de8f924 --- /dev/null +++ b/tracking_manager/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Kévin Roche \<\> +- Sébastien BEAU \<\> diff --git a/tracking_manager/readme/DESCRIPTION.md b/tracking_manager/readme/DESCRIPTION.md new file mode 100644 index 000000000..62f2ec270 --- /dev/null +++ b/tracking_manager/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +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.md b/tracking_manager/readme/USAGE.md new file mode 100644 index 000000000..08ce30062 --- /dev/null +++ b/tracking_manager/readme/USAGE.md @@ -0,0 +1,17 @@ +- 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..de8b8f84a --- /dev/null +++ b/tracking_manager/static/description/index.html @@ -0,0 +1,456 @@ + + + + + +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.
    • +
    +
  • +
+

image

+
    +
  • Then select the fields to track
  • +
+

image1

+
+
+

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..42f19b468 --- /dev/null +++ b/tracking_manager/tests/test_tracking_manager.py @@ -0,0 +1,271 @@ +# 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 import Command +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": [(Command.CREATE, 0, {"acc_number": "007"})], + "category_id": [(Command.SET, 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 = self.env["res.partner"].browse(self.partner.id) + self.partner.write( + {"category_id": [(Command.LINK, self.partner_categ_2.id, 0)]} + ) + self.assertEqual(len(self.messages), 1) + tracking = self.messages.tracking_value_ids[0] + 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": [(Command.UNLINK, self.partner_categ_1.id, 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": [ + ( + Command.SET, + 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": [(Command.CREATE, 0, {"acc_number": "1234567890"})]} + ) + self.assertEqual(len(self.messages), 2) + self.assertEqual(self.messages[0].body.count("New"), 1) + + def test_o2m_unlink_indirectly(self): + self.partner.write( + {"bank_ids": [(Command.DELETE, 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": [ + (Command.UPDATE, 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": [ + (Command.UPDATE, 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": [ + (Command.DELETE, self.partner.bank_ids[0].id, 0), + (Command.CREATE, 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": [ + ( + Command.UPDATE, + 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": [ + ( + Command.UPDATE, + 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": [ + (Command.UPDATE, self.partner.bank_ids[0].id, {"acc_number": "123"}) + ], + } + ) + self.partner.write( + { + "bank_ids": [(Command.DELETE, 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..90f6c17fe --- /dev/null +++ b/tracking_manager/tools.py @@ -0,0 +1,9 @@ +# 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): + if records: + return "; ".join(records.mapped("display_name")) + return "" diff --git a/tracking_manager/views/ir_model.xml b/tracking_manager/views/ir_model.xml new file mode 100644 index 000000000..e3e7a598f --- /dev/null +++ b/tracking_manager/views/ir_model.xml @@ -0,0 +1,67 @@ + + + + + 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 @@ + + + + +