Initial commit: Odoo 18.0-20251222 extra-addons
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled

This commit is contained in:
tocmo0nlord
2026-03-13 20:43:25 +00:00
parent 36e847a7df
commit adbe430761
9472 changed files with 1265727 additions and 0 deletions

124
tracking_manager/README.rst Executable file
View File

@@ -0,0 +1,124 @@
.. 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/18.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-18-0/server-tools-18-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=18.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/18.0/tracking_manager/static/description/model_view.png
.. |image1| image:: https://raw.githubusercontent.com/OCA/server-tools/18.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:%2018.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>
- Christopher Rogos <crogos@gmail.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/18.0/tracking_manager>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

1
tracking_manager/__init__.py Executable file
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": "18.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",
],
}

153
tracking_manager/i18n/es.po Executable file
View File

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

210
tracking_manager/i18n/fr.po Executable file
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_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"

155
tracking_manager/i18n/it.po Executable file
View File

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

View File

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

145
tracking_manager/i18n/zh_CN.po Executable file
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 17.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-06-16 17:40+0000\n"
"Last-Translator: xtanuiha <feihu.zhang@live.com>\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 "<b>Change :</b>"
msgstr "<b>更改:</b>"
#. module: tracking_manager
#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template
msgid "<b>Delete :</b>"
msgstr "<b>删除:</b>"
#. module: tracking_manager
#: model_terms:ir.ui.view,arch_db:tracking_manager.track_o2m_m2m_template
msgid "<b>New :</b>"
msgstr "<b>新建:</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
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 "更新字段配置"

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("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,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

224
tracking_manager/models/models.py Executable file
View File

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

View File

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

View File

@@ -0,0 +1,3 @@
- Kévin Roche \<<kevin.roche@akretion.com>\>
- Sébastien BEAU \<<sebastien.beau@akretion.com>\>
- Christopher Rogos \<<crogos@gmail.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,463 @@
<!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>README.rst</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">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="tracking-manager">
<h1>Tracking Manager</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:ae03b47c76a9373b925885f6a4674ae97580ce1e189e24d1e3ad100cff03c705
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<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/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/server-tools/tree/18.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-18-0/server-tools-18-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=18.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">
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<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/18.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/18.0/tracking_manager/static/description/fields.png" /></p>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h2>
<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:%2018.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">
<h2><a class="toc-backref" href="#toc-entry-3">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-4">Authors</a></h3>
<ul class="simple">
<li>Akretion</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
<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>
<li>Christopher Rogos &lt;<a class="reference external" href="mailto:crogos&#64;gmail.com">crogos&#64;gmail.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h3>
<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/18.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>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

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

View File

@@ -0,0 +1,183 @@
# 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 = "<p>Initial Value</p>"
new_value = "<p>New Value</p>"
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, "title", 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]),
}
# Test all the property types using as fake title field because there is no
# property field in base to test.
# We do not want to create a FakeModel and add the property field in partner
# because the partner_property module could have conflicts.
# 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)

View File

@@ -0,0 +1,273 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# Copyright 2024 Tecnativa - Víctor Martínez
# @author Kévin Roche <kevin.roche@akretion.com>
# 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(["mobile"])[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(["mobile"])[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_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,
{
"groups_id": [
(
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)

9
tracking_manager/tools.py Executable file
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,63 @@
<?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,57 @@
<?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">list,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">
<list 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" />
</list>
</field>
</record>
<record id="ir_model_fields_action_view" model="ir.actions.act_window.view">
<field name="sequence" eval="2" />
<field name="view_mode">list</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>