diff --git a/tracking_manager/README.rst b/tracking_manager/README.rst new file mode 100644 index 00000000000..02f806b617b --- /dev/null +++ b/tracking_manager/README.rst @@ -0,0 +1,125 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +================ +Tracking Manager +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ae03b47c76a9373b925885f6a4674ae97580ce1e189e24d1e3ad100cff03c705 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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/19.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-19-0/server-tools-19-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=19.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/19.0/tracking_manager/static/description/model_view.png +.. |image1| image:: https://raw.githubusercontent.com/OCA/server-tools/19.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 +- Christopher Rogos +- Michel Guiheneuf + +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 00000000000..0650744f6bc --- /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 00000000000..30fb07ed9eb --- /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": "19.0.1.1.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 00000000000..bf5c3dbe312 --- /dev/null +++ b/tracking_manager/i18n/es.po @@ -0,0 +1,153 @@ +# 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_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:ir.model.fields,field_description:tracking_manager.field_ir_model__automatic_custom_tracking_domain +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 marked, the fields matching the matched by the domain below will be " +"automatically tracked for this model." +msgstr "" + +#. 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" + +#~ msgid "Automatic Custom Tracking Domain" +#~ msgstr "Dominio de Seguimiento Automático Personalizado" + +#~ 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" diff --git a/tracking_manager/i18n/fr.po b/tracking_manager/i18n/fr.po new file mode 100644 index 00000000000..d4fc9849715 --- /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_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:ir.model.fields,field_description:tracking_manager.field_ir_model__automatic_custom_tracking_domain +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 marked, the fields matching the matched by the domain below will be " +"automatically tracked for this model." +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 "Automatic Custom Tracking Domain" +#~ msgstr "Suivi personnalisé" + +#, 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 00000000000..f2fea28c127 --- /dev/null +++ b/tracking_manager/i18n/it.po @@ -0,0 +1,155 @@ +# 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-05-03 09:36+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_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:ir.model.fields,field_description:tracking_manager.field_ir_model__automatic_custom_tracking_domain +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 marked, the fields matching the matched by the domain below will be " +"automatically tracked for this model." +msgstr "" +"Se attiva, i campi che corrispondono alla ricerca del dominio sottostante " +"verranno tracciati automaticamente per questo modello." + +#. 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" + +#~ msgid "Automatic Custom Tracking Domain" +#~ msgstr "Dominio tracciamento personalizzato automatico" + +#~ 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" diff --git a/tracking_manager/i18n/tracking_manager.pot b/tracking_manager/i18n/tracking_manager.pot new file mode 100644 index 00000000000..986e526b699 --- /dev/null +++ b/tracking_manager/i18n/tracking_manager.pot @@ -0,0 +1,158 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * tracking_manager +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.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_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:ir.model.fields,field_description:tracking_manager.field_ir_model__automatic_custom_tracking_domain +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 marked, the fields matching the matched by the domain below will be " +"automatically tracked for this model." +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 "" diff --git a/tracking_manager/i18n/zh_CN.po b/tracking_manager/i18n/zh_CN.po new file mode 100644 index 00000000000..3b3587b8e15 --- /dev/null +++ b/tracking_manager/i18n/zh_CN.po @@ -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 17.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-06-16 17:40+0000\n" +"Last-Translator: xtanuiha \n" +"Language-Team: none\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\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 "更改:" + +#. 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_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:ir.model.fields,field_description:tracking_manager.field_ir_model__automatic_custom_tracking_domain +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 marked, the fields matching the matched by the domain below will be " +"automatically tracked for this model." +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 00000000000..0617d405fbf --- /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 00000000000..68e3e73d984 --- /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 00000000000..0d6e70c02f8 --- /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("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 00000000000..bcb67019872 --- /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 00000000000..5b56ca1abcc --- /dev/null +++ b/tracking_manager/models/mail_tracking_value.py @@ -0,0 +1,68 @@ +# Copyright 2025 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import api, models +from odoo.tools import html2plaintext + + +class MailTracking(models.Model): + _inherit = "mail.tracking.value" + + # TODO: Remove if merged https://github.com/odoo/odoo/pull/156236 + def _create_tracking_values_property( + self, initial_value, new_value, col_name, col_info, record + ): + field = self.env["ir.model.fields"]._get(record._name, col_name) + field_info = { + "desc": f"{field.field_description}: {col_info['string']}", + "name": col_name, + "type": col_info["type"], + } + if col_info["type"] in ("many2one", "many2many"): + comodel = self.env[col_info["comodel"]] + initial_value = comodel.browse(initial_value) if initial_value else False + new_value = comodel.browse(new_value) if new_value else False + values = self.env["mail.tracking.value"]._create_tracking_values( + initial_value, new_value, col_name, col_info, record + ) + del values["field_id"] + return {**values, "field_info": field_info} + + @api.model + def _create_tracking_values( + self, initial_value, new_value, col_name, col_info, record + ): + try: + return super()._create_tracking_values( + initial_value, new_value, col_name, col_info, record + ) + except NotImplementedError: + if col_info["type"] == "html": + field = self.env["ir.model.fields"]._get(record._name, col_name) + values = {"field_id": field.id} + values.update( + { + "old_value_char": html2plaintext(initial_value) or "", + "new_value_char": html2plaintext(new_value) or "", + } + ) + return values + elif col_info["type"] == "properties": + # TODO: Remove if merged https://github.com/odoo/odoo/pull/156236 + # A return is necessary to avoid the NotImplementedError error + field = self.env["ir.model.fields"]._get(record._name, col_name) + return {"field_id": field.id} + elif col_info["type"] == "tags": + # TODO: Remove if merged https://github.com/odoo/odoo/pull/156236 + field = self.env["ir.model.fields"]._get(record._name, col_name) + return { + "field_id": field.id, + "old_value_char": ( + ", ".join(value for value in initial_value) + if initial_value + else "" + ), + "new_value_char": ( + ", ".join(value for value in new_value) if new_value else "" + ), + } + raise diff --git a/tracking_manager/models/models.py b/tracking_manager/models/models.py new file mode 100644 index 00000000000..ad3f86379c7 --- /dev/null +++ b/tracking_manager/models/models.py @@ -0,0 +1,224 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# Copyright 2025 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from collections import defaultdict + +from odoo import Command, api, models, tools +from odoo.exceptions import AccessError + +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] + # Skip processing if the owner is not a valid Odoo recordset or is empty. + if not (owner and isinstance(owner, models.BaseModel)): + continue + 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(): + # check if record has mail.thread mixin + if not getattr(self.env[model_name], "message_post_with_source", False): + continue + for record_id, messages_by_field in model_data.items(): + # Avoid error if no record is linked (example: child_ids of res.partner) + if not record_id: + continue + 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() + ] + # We do not use message_post_with_view() because emails would be sent + rendered_template = self.env["ir.qweb"]._render( + "tracking_manager.track_o2m_m2m_template", + {"lines": messages, "object": record}, + minimal_qcontext=True, + ) + record._message_log(body=rendered_template) + + 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: + try: + values.setdefault(fname, record[fname]) + except AccessError: + # User does not have access to the field (example with groups) + continue + + 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() + + # TODO: Remove if merged https://github.com/odoo/odoo/pull/156236 + def _mail_track(self, tracked_fields, initial_values): + _tracked_fields = tracked_fields + tracked_fields_properties = {} + for tf_key in list(_tracked_fields.keys()): + tracked_field = tracked_fields[tf_key] + if tracked_field["type"] == "properties": + tracked_fields_properties[tf_key] = tracked_field + updated, tracking_value_ids = super()._mail_track( + tracked_fields, initial_values + ) + # Remove unnecessary tracking_value_ids + tracking_value_ids_keys_to_delete = [] + for tf_key in list(tracked_fields_properties.keys()): + field = self.env["ir.model.fields"]._get(self._name, tf_key) + for key, vals in enumerate(tracking_value_ids): + if vals[2]["field_id"] == field.id: + tracking_value_ids_keys_to_delete.append(key) + for key in tracking_value_ids_keys_to_delete: + tracking_value_ids.pop(key) + # Extra things for properties + for col_name, _sequence in self._mail_track_order_fields( + tracked_fields_properties + ): + if col_name not in initial_values: + continue + initial_value, new_value = initial_values[col_name], self[col_name] + if new_value == initial_value or (not new_value and not initial_value): + continue + p_keys = list(initial_value.keys()) if initial_value else [] + properties_data = {} + for definition in self.read([col_name])[0][col_name]: + properties_data[definition["name"]] = definition + tracking_value_ids.extend( + Command.create( + self.env["mail.tracking.value"]._create_tracking_values_property( + initial_value[p_key], + new_value[p_key], + col_name, + properties_data[p_key], + self, + ), + ) + for p_key in p_keys + if ( + p_key in properties_data + and properties_data[p_key]["type"] != "separator" + and initial_value[p_key] != new_value[p_key] + ) + ) + return updated, tracking_value_ids diff --git a/tracking_manager/pyproject.toml b/tracking_manager/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /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 00000000000..975f7015140 --- /dev/null +++ b/tracking_manager/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- Kévin Roche \<\> +- Sébastien BEAU \<\> +- Christopher Rogos \<\> +- Michel Guiheneuf \<\> \ No newline at end of file diff --git a/tracking_manager/readme/DESCRIPTION.md b/tracking_manager/readme/DESCRIPTION.md new file mode 100644 index 00000000000..62f2ec2705b --- /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 00000000000..08ce3006223 --- /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 00000000000..013a06f7816 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 00000000000..3a0328b516c 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 00000000000..3a38d6ea1c5 --- /dev/null +++ b/tracking_manager/static/description/index.html @@ -0,0 +1,464 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

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 00000000000..bc04d2ea53b 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 00000000000..1b7fbd3e85a --- /dev/null +++ b/tracking_manager/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_tracking_manager +from . import test_mail_tracking_value diff --git a/tracking_manager/tests/test_mail_tracking_value.py b/tracking_manager/tests/test_mail_tracking_value.py new file mode 100644 index 00000000000..a924552e3d3 --- /dev/null +++ b/tracking_manager/tests/test_mail_tracking_value.py @@ -0,0 +1,179 @@ +# Copyright 2025 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.addons.base.tests.common import BaseCommon + + +class TestMailTracking(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.MailTracking = cls.env["mail.tracking.value"] + + def test_create_tracking_values_html(self): + initial_value = "

Initial Value

" + new_value = "

New Value

" + col_name = "comment" + col_info = {"type": "html"} + record = self.env["res.partner"].create({"name": "Test Partner"}) + + values = self.MailTracking._create_tracking_values( + initial_value, new_value, col_name, col_info, record + ) + + self.assertEqual(values["old_value_char"], "Initial Value") + self.assertEqual(values["new_value_char"], "New Value") + + def _test_create_tracking_values_property(self, values): + property_type_mapped = { + "char": "char", + "boolean": "integer", + "integer": "integer", + "float": "float", + "date": "datetime", + "datetime": "datetime", + "selection": "char", + "tags": "char", + "many2one": "integer", + "many2many": "char", + } + test_properties_info = { + "property_01": {"string": "property_01", "type": "char"}, + "property_02": {"string": "property_02", "type": "boolean"}, + "property_03": {"string": "property_03", "type": "integer"}, + "property_04": {"string": "property_04", "type": "float"}, + "property_05": {"string": "property_05", "type": "date"}, + "property_06": {"string": "property_06", "type": "datetime"}, + "property_07": { + "string": "property_07", + "type": "selection", + "selection": [["key1", "value1"], ["key2", "value2"]], + }, + "property_08": {"string": "property_08", "type": "tags"}, + "property_09": { + "string": "property_09", + "type": "many2one", + "comodel": self.partner._name, + }, + "property_10": { + "string": "property_10", + "type": "many2many", + "comodel": self.partner._name, + }, + } + for p_name, col_info in test_properties_info.items(): + initial_value = values[p_name][0] + new_value = values[p_name][1] + res = self.MailTracking._create_tracking_values_property( + initial_value, new_value, "name", col_info, self.partner + ) + del res["field_info"] + f_name = property_type_mapped[col_info["type"]] + expected_old_value = initial_value + expected_new_value = new_value + if col_info["type"] == "date": + expected_old_value = ( + f"{expected_old_value} 00:00:00" if expected_old_value else False + ) + expected_new_value = ( + f"{expected_new_value} 00:00:00" if expected_new_value else False + ) + elif col_info["type"] == "selection": + expected_old_value = values[p_name][2] + expected_new_value = values[p_name][3] + elif col_info["type"] == "tags": + expected_old_value = ( + ", ".join(value for value in expected_old_value) + if expected_old_value + else "" + ) + expected_new_value = ( + ", ".join(value for value in expected_new_value) + if expected_new_value + else "" + ) + elif col_info["type"] == "many2one": + del res["old_value_char"] + del res["new_value_char"] + elif col_info["type"] == "many2many": + comodel = self.env[col_info["comodel"]] + expected_old_value = ( + comodel.browse(expected_old_value) if expected_old_value else False + ) + expected_new_value = ( + comodel.browse(expected_new_value) if expected_new_value else False + ) + expected_old_value = ( + ", ".join(expected_old_value.mapped("display_name")) + if expected_old_value + else "" + ) + expected_new_value = ( + ", ".join(expected_new_value.mapped("display_name")) + if expected_new_value + else "" + ) + expected_values = { + f"old_value_{f_name}": expected_old_value, + f"new_value_{f_name}": expected_new_value, + } + self.assertEqual(res, expected_values) + + def test_mail_tracking_value_properties(self): + partner_extra = self.env["res.partner"].create({"name": "Test partner extra"}) + test_properties_01 = { + # property: initial_value, new_value + "property_01": ("", "value1"), + "property_02": (False, True), + "property_03": (0, 10), + "property_04": (0, 10.10), + "property_05": (False, "2025-01-01"), + "property_06": (False, "2025-01-01 00:00:00"), + "property_07": (False, "key1", "", "value1"), + "property_08": (False, ["tag1", "tag2"]), + "property_09": (False, self.partner.id), + "property_10": (False, [self.partner.id, partner_extra.id]), + } + # 1- Test the case that all the initial values were empty and now have a value + self._test_create_tracking_values_property(test_properties_01) + # 2- Test the case that all the initial values had something set and now have + # a different value + test_properties_02 = { + # property: initial_value, new_value + "property_01": ("value1", "value2"), + "property_02": (True, False), + "property_03": (10, 11), + "property_04": (10.10, 11.10), + "property_05": ("2025-01-01", "2025-01-02"), + "property_06": ("2025-01-01 00:00:00", "2025-01-02 00:00:00"), + "property_07": ("key1", "key2", "value1", "value2"), + "property_08": ( + ["tag1", "tag2"], + [ + "tag1", + ], + ), + "property_09": (self.partner.id, partner_extra.id), + "property_10": ( + [self.partner.id, partner_extra.id], + [ + self.partner.id, + ], + ), + } + self._test_create_tracking_values_property(test_properties_02) + # 3- Test the case that all initial values had something set and now has + # no value + test_properties_03 = { + # property: initial_value, new_value + "property_01": ("value2", ""), + "property_02": (False, True), + "property_03": (11, 0), + "property_04": (11.10, 0), + "property_05": ("2025-01-02", False), + "property_06": ("2025-01-02 00:00:00", False), + "property_07": ("key1", False, "value1", ""), + "property_08": (["tag1", "tag2"], False), + "property_09": (self.partner.id, False), + "property_10": ([self.partner.id, partner_extra.id], False), + } + self._test_create_tracking_values_property(test_properties_03) diff --git a/tracking_manager/tests/test_tracking_manager.py b/tracking_manager/tests/test_tracking_manager.py new file mode 100644 index 00000000000..f71c3244568 --- /dev/null +++ b/tracking_manager/tests/test_tracking_manager.py @@ -0,0 +1,273 @@ +# Copyright 2022 Akretion (https://www.akretion.com). +# Copyright 2024 Tecnativa - Víctor Martínez +# @author Kévin Roche +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import Command +from odoo.tests.common import TransactionCase +from odoo.tools import mute_logger + + +class TestTrackingManager(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + 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", + "user_ids": [(Command.CREATE, 0, {"login": "007"})], + "category_id": [(Command.SET, 0, [cls.partner_categ_1.id])], + } + ) + cls.partner_model = cls.env.ref("base.model_res_partner") + cls._active_tracking(["user_ids", "category_id"]) + cls.flush_tracking() + cls.partner.message_ids.unlink() + + @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(["phone"])[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(["phone"])[0] + self.assertTrue(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_char, "FOO") + self.assertEqual(tracking.new_value_char, "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_char, "FOO") + self.assertEqual(tracking.new_value_char, "") + + 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_char, "FOO") + self.assertEqual(tracking.new_value_char, "BAR, TOOH") + + def test_o2m_create_indirectly(self): + self.partner.write({"user_ids": [(Command.CREATE, 0, {"login": "1234567890"})]}) + self.assertEqual(len(self.messages), 2) + self.assertEqual(self.messages[0].body.count("New"), 1) + + @mute_logger("odoo.models.unlink") + def test_o2m_unlink_indirectly(self): + self.partner.write( + {"user_ids": [(Command.DELETE, self.partner.user_ids[0].id)]} + ) + self.assertEqual(len(self.messages), 1) + self.assertIn("Delete", self.messages.body) + + def test_o2m_write_indirectly(self): + self.partner.write( + { + "user_ids": [ + (Command.UPDATE, self.partner.user_ids[0].id, {"login": "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.users and remove tracking on login + res_users_model = self.env["ir.model"].search([("model", "=", "res.users")]) + res_users_model.active_custom_tracking = True + login_field = res_users_model.field_id.filtered(lambda x: x.name == "login") + login_field.custom_tracking = False + self.partner.write( + { + "user_ids": [ + (Command.UPDATE, self.partner.user_ids[0].id, {"login": "123"}) + ], + } + ) + self.assertEqual(len(self.messages), 0) + + @mute_logger("odoo.models.unlink") + def test_o2m_create_and_unlink_indirectly(self): + self.partner.write( + { + "user_ids": [ + (Command.DELETE, self.partner.user_ids[0].id, 0), + (Command.CREATE, 0, {"login": "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.group_extra = self.env["res.groups"].create({"name": "Test group"}) + self.partner.write( + { + "user_ids": [ + ( + Command.UPDATE, + self.partner.user_ids[0].id, + { + "group_ids": [ + ( + 6, + 0, + [ + self.env.ref("base.group_user").id, + self.group_extra.id, + ], + ) + ] + }, + ), + ] + } + ) + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("Changed"), 1) + + def test_o2m_update_m2o_indirectly(self): + user = self.partner.user_ids[0] + action = self.env["ir.actions.act_window"].create( + {"name": "test", "type": "ir.actions.act_window", "res_model": user._name} + ) + self.partner.write( + {"user_ids": [(Command.UPDATE, user.id, {"action_id": action.id})]} + ) + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("Changed"), 1) + + @mute_logger("odoo.models.unlink") + 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( + { + "user_ids": [ + (Command.UPDATE, self.partner.user_ids[0].id, {"login": "123"}) + ], + } + ) + self.partner.write( + { + "user_ids": [(Command.DELETE, self.partner.user_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): + # Add custom context to prevent message from mail addon + self.env["res.users"].with_context( + mail_create_nolog=True, mail_notrack=True + ).create( + { + "name": "1234567890", + "login": "1234567890", + "partner_id": self.partner.id, + } + ) + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("New"), 1) + + @mute_logger("odoo.models.unlink") + def test_o2m_unlink_directly(self): + self.partner.user_ids.unlink() + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("Delete"), 1) + + def test_o2m_update_directly(self): + self.partner.user_ids.write({"login": "0987654321"}) + self.assertEqual(len(self.messages), 1) + self.assertEqual(self.messages.body.count("Change :"), 1) + + @mute_logger("odoo.models.unlink") + def test_o2m_write_and_unlink_directly(self): + # see explanation of test_o2m_write_and_unlink_indirectly + self.partner.user_ids.write({"login": "0987654321"}) + self.partner.user_ids.unlink() + 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_update_record(self): + self.env.ref("base.field_res_partner__child_ids").custom_tracking = True + child = self.env["res.partner"].create( + {"name": "Test child", "parent_id": self.partner.id} + ) + child.write({"parent_id": False}) + self.assertEqual(len(self.messages), 1) diff --git a/tracking_manager/tools.py b/tracking_manager/tools.py new file mode 100644 index 00000000000..90f6c17feb7 --- /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 00000000000..3f17eccf4cf --- /dev/null +++ b/tracking_manager/views/ir_model.xml @@ -0,0 +1,63 @@ + + + + + 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 00000000000..eb6a2286b1a --- /dev/null +++ b/tracking_manager/views/ir_model_fields.xml @@ -0,0 +1,57 @@ + + + + + Trackable Fields + ir.actions.act_window + ir.model.fields + list,form + [("trackable", "=", True), ("model_id", "=", context['active_id']), ("ttype", "!=", "binary")] + {} + current + + + + ir.model.fields + + + + + + + + + + + + + + list + + + + + + 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 00000000000..e03ae875b8e --- /dev/null +++ b/tracking_manager/views/message_template.xml @@ -0,0 +1,52 @@ + + + + +