Merge PR #2916 into 17.0

Signed-off-by pedrobaeza
pull/2950/head
OCA-git-bot 2024-05-02 13:11:38 +00:00
commit 4c468dd081
28 changed files with 2182 additions and 0 deletions

View File

@ -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 <https://github.com/OCA/server-tools/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 <https://github.com/OCA/server-tools/issues/new?body=module:%20tracking_manager%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Akretion
Contributors
------------
- Kévin Roche <kevin.roche@akretion.com>
- Sébastien BEAU <sebastien.beau@akretion.com>
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 <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-Kev-Roche| |maintainer-sebastienbeau|
This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/17.0/tracking_manager>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,23 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# @author Kévin Roche <kevin.roche@akretion.com>
# 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",
],
}

View File

@ -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 <informatica@totmaterial.es>\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 "<b>Change :</b>"
msgstr "<b>Cambio :</b>"
#. module: tracking_manager
#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template
msgid "<b>Delete :</b>"
msgstr "<b>Eliminar:</b>"
#. module: tracking_manager
#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template
msgid "<b>New :</b>"
msgstr "<b> Nuevo: </b>"
#. 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"

View File

@ -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 "<b>Change :</b>"
msgstr "<b>Modifié :</b>"
#. module: tracking_manager
#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template
msgid "<b>Delete :</b>"
msgstr "<b>Supprimé :</b>"
#. module: tracking_manager
#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template
msgid "<b>New :</b>"
msgstr "<b>Nouveau :</b>"
#. 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"

View File

@ -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 <stefano.consolaro@mymage.it>\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 "<b>Change :</b>"
msgstr "<b>Modifica:</b>"
#. module: tracking_manager
#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template
msgid "<b>Delete :</b>"
msgstr "<b>Cancella:</b>"
#. module: tracking_manager
#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template
msgid "<b>New :</b>"
msgstr "<b>Nuovo:</b>"
#. 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"

View File

@ -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 "<b>Change :</b>"
msgstr ""
#. module: tracking_manager
#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template
msgid "<b>Delete :</b>"
msgstr ""
#. module: tracking_manager
#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template
msgid "<b>New :</b>"
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 ""

View File

@ -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

View File

@ -0,0 +1,162 @@
# Copyright (C) 2022 Akretion (<http://www.akretion.com>).
# @author Kévin Roche <kevin.roche@akretion.com>
# 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)

View File

@ -0,0 +1,62 @@
# Copyright (C) 2022 Akretion (<http://www.akretion.com>).
# @author Kévin Roche <kevin.roche@akretion.com>
# 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)

View File

@ -0,0 +1,17 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# @author Kévin Roche <kevin.roche@akretion.com>
# 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()

View File

@ -0,0 +1,27 @@
# Copyright 2023 Akretion (https://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# 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
)

View File

@ -0,0 +1,156 @@
# Copyright 2023 Akretion (https://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# 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()

View File

@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@ -0,0 +1,2 @@
- Kévin Roche \<<kevin.roche@akretion.com>\>
- Sébastien BEAU \<<sebastien.beau@akretion.com>\>

View File

@ -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).

View File

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,456 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Tracking Manager</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="tracking-manager">
<h1 class="title">Tracking Manager</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:a7d5bd552b419ff6e2058562ce30c52387acbe3f01cbd1ec3804a78a9fc5a1c9
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/server-tools/tree/17.0/tracking_manager"><img alt="OCA/server-tools" src="https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/server-tools-17-0/server-tools-17-0-tracking_manager"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/server-tools&amp;target_branch=17.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>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).</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<ul class="simple">
<li>In setting &gt; models: select a model</li>
<li>Check “Active” under Custom Tracking.</li>
<li>You have two options - 1) manually configure tracked fields one by
one, or 2) determine tracked fields based on a specific domain.</li>
<li>For 1) manually configure tracked fields one by one<ul>
<li>Click on Tracked Fields smart button, and select/unselect Custom
Tracking.</li>
</ul>
</li>
<li>For 2) determine tracked fields based on a specific domain<ul>
<li>Select “Automatic configuration”, and then set the domain
accordingly.</li>
<li>Click “Update” for the domain to take effect.</li>
</ul>
</li>
</ul>
<p><img alt="image" src="https://raw.githubusercontent.com/OCA/server-tools/17.0/tracking_manager/static/description/model_view.png" /></p>
<ul class="simple">
<li>Then select the fields to track</li>
</ul>
<p><img alt="image1" src="https://raw.githubusercontent.com/OCA/server-tools/17.0/tracking_manager/static/description/fields.png" /></p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/server-tools/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/server-tools/issues/new?body=module:%20tracking_manager%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
<ul class="simple">
<li>Akretion</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
<ul class="simple">
<li>Kévin Roche &lt;<a class="reference external" href="mailto:kevin.roche&#64;akretion.com">kevin.roche&#64;akretion.com</a>&gt;</li>
<li>Sébastien BEAU &lt;<a class="reference external" href="mailto:sebastien.beau&#64;akretion.com">sebastien.beau&#64;akretion.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainers</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/Kev-Roche"><img alt="Kev-Roche" src="https://github.com/Kev-Roche.png?size=40px" /></a> <a class="reference external image-reference" href="https://github.com/sebastienbeau"><img alt="sebastienbeau" src="https://github.com/sebastienbeau.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/server-tools/tree/17.0/tracking_manager">OCA/server-tools</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -0,0 +1 @@
from . import test_tracking_manager

View File

@ -0,0 +1,13 @@
# Copyright 2023 Akretion (https://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# 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"
)

View File

@ -0,0 +1,271 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# @author Kévin Roche <kevin.roche@akretion.com>
# 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)

View File

@ -0,0 +1,9 @@
# Copyright 2023 Akretion (https://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# 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 ""

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright (C) 2022 Akretion (<http://www.akretion.com>).
@author Kévin Roche <kevin.roche@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_model_form" model="ir.ui.view">
<field name="name">tracking.ir.model form</field>
<field name="model">ir.model</field>
<field name="inherit_id" ref="base.view_model_form" />
<field name="arch" type="xml">
<xpath expr="//sheet/group[1]" position="after">
<group string="Custom Tracking">
<group>
<field name="active_custom_tracking" string="Active" />
<field
name="automatic_custom_tracking"
string="Automatic configuration"
invisible="not active_custom_tracking"
/>
<field
name="automatic_custom_tracking_domain"
invisible="not automatic_custom_tracking"
required="automatic_custom_tracking"
widget="domain"
options="{'model': 'ir.model.fields', 'in_dialog': True, 'foldable': True}"
/>
<label
for="update_custom_tracking"
string="Update fields configuration"
invisible="not automatic_custom_tracking"
/>
<button
name="update_custom_tracking"
string="Update"
icon="fa-refresh"
type="object"
class="btn-secondary"
invisible="not automatic_custom_tracking"
/>
</group>
</group>
</xpath>
<xpath expr="//sheet/group[1]" position="before">
<div class="oe_button_box" name="button_box">
<button
name="%(ir_model_fields_action)d"
type="action"
class="oe_stat_button"
icon="fa-server"
invisible="not active_custom_tracking"
>
<field
name="tracked_field_count"
widget="statinfo"
string="Tracked Fields"
/>
</button>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright (C) 2022 Akretion (<http://www.akretion.com>).
@author Kévin Roche <kevin.roche@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.actions.act_window" id="ir_model_fields_action">
<field name="name">Trackable Fields</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ir.model.fields</field>
<field name="view_mode">tree,form</field>
<field
name="domain"
>[("trackable", "=", True), ("model_id", "=", context['active_id']), ("ttype", "!=", "binary")]</field>
<field name="context">{}</field>
<field name="target">current</field>
</record>
<record id="ir_model_fields_view_tree_custom_tracking" model="ir.ui.view">
<field name="model">ir.model.fields</field>
<field name="arch" type="xml">
<tree editable="bottom" create="0" delete="0" duplicate="0">
<field name="name" readonly="True" />
<field name="field_description" readonly="True" />
<field name="ttype" readonly="True" />
<field name="native_tracking" readonly="True" />
<field name="custom_tracking" widget="boolean_toggle" />
</tree>
</field>
</record>
<record id="ir_model_fields_action_view" model="ir.actions.act_window.view">
<field name="sequence" eval="2" />
<field name="view_mode">tree</field>
<field name="view_id" ref="ir_model_fields_view_tree_custom_tracking" />
<field name="act_window_id" ref="ir_model_fields_action" />
</record>
<record id="view_model_track_fields_search" model="ir.ui.view">
<field name="name">ir.model.fields.search</field>
<field name="model">ir.model.fields</field>
<field name="inherit_id" ref="base.view_model_fields_search" />
<field name="arch" type="xml">
<xpath expr="//filter[@name='translate']" position="after">
<filter
name="tracking_on"
string="Custom Tracking ON"
domain="[('custom_tracking','=', True)]"
/>
<filter
name="tracking_off"
string="Custom Tracking OFF"
domain="[('custom_tracking','=', False)]"
/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright (C) 2022 Akretion (<http://www.akretion.com>).
@author Kévin Roche <kevin.roche@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<template id="track_o2m_m2m_template">
<div>
<ul>
<t t-foreach="lines" t-as="line">
<li>
<b>
<t t-esc="line.get('name')" />: </b>
<br />
<t t-foreach="line.get('messages')" t-as="message">
<ul>
<t t-if="message.get('mode', False) == 'create'">
<b>New :</b>
</t>
<t t-if="message.get('mode', False) == 'unlink'">
<b>Delete :</b>
</t>
<t t-if="message.get('mode', False) == 'update'">
<b>Change :</b>
</t>
<t t-esc="message.get('record')" />
<t t-if="message.get('mode', False) == 'update'">
<ul>
<t
t-foreach="message.get('changes')"
t-as="change"
>
<li>
<t t-esc="change.get('name')" /> :
<t t-esc="change.get('old')" />
<div
class="o_Message_trackingValueSeparator o_Message_trackingValueItem fa fa-long-arrow-right"
title="Changed"
role="img"
/>
<t t-esc="change.get('new')" />
</li>
</t>
</ul>
</t>
</ul>
</t>
</li>
</t>
</ul>
</div>
</template>
</odoo>