[14.0][ADD] tracking_manager
parent
a7bb9b1433
commit
70e3e93a07
|
@ -0,0 +1 @@
|
|||
from . import models
|
|
@ -0,0 +1,27 @@
|
|||
# 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": "14.0.1.0.0",
|
||||
"category": "Tools",
|
||||
"website": "https://github.com/OCA/server-tools",
|
||||
"author": "Akretion, Odoo Community Association (OCA)",
|
||||
"maintainers": ["Kev-Roche"],
|
||||
"license": "AGPL-3",
|
||||
"application": False,
|
||||
"installable": True,
|
||||
"depends": [
|
||||
"base",
|
||||
"mail",
|
||||
],
|
||||
"data": [
|
||||
"views/ir_model.xml",
|
||||
"views/ir_model_fields.xml",
|
||||
"views/message_template.xml",
|
||||
"security/ir.model.access.csv",
|
||||
],
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
# 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:ir.model.fields,help:tracking_manager.field_ir_model__apply_custom_tracking
|
||||
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."
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__apply_custom_tracking
|
||||
msgid "Apply custom tracking on fields"
|
||||
msgstr "Active le suivi personnalisé des champs"
|
||||
|
||||
#. 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_tracking_model_field__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Créé par"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_tracking_model_field__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Créé le"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form
|
||||
msgid "Custom Tracked fields"
|
||||
msgstr "Champs Suivis"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_tracking_model_field__custom_tracking
|
||||
msgid "Custom Tracking"
|
||||
msgstr "Suivi personnalisé"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__display_name
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__display_name
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__display_name
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_tracking_model_field__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Nom affiché"
|
||||
|
||||
#. 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.fields,field_description:tracking_manager.field_tracking_model_field__tracking_field_id
|
||||
msgid "Field"
|
||||
msgstr "Champ"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__field_count
|
||||
msgid "Field Count"
|
||||
msgstr "Nb de champs"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_tracking_model_field__name
|
||||
msgid "Field Name"
|
||||
msgstr "Nom du champs"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model,name:tracking_manager.model_ir_model_fields
|
||||
msgid "Fields"
|
||||
msgstr "Champs"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__id
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__id
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread__id
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_tracking_model_field__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,help:tracking_manager.field_tracking_model_field__native_tracking_field
|
||||
msgid ""
|
||||
"If set every modification done to this field is tracked in the chatter. "
|
||||
"Value is used to order tracking values."
|
||||
msgstr ""
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_tracking_model_field__type_field
|
||||
msgid "Kind Field"
|
||||
msgstr "Type de champs"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_ir_model____last_update
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields____last_update
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_mail_thread____last_update
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_tracking_model_field____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "Dernière modification le"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_tracking_model_field__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_tracking_model_field__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. 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_tracking_model_field__native_tracking_field
|
||||
msgid "Native Tracking"
|
||||
msgstr "Suivi natif"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_ir_model__o2m_model_ids
|
||||
msgid "One2many Models"
|
||||
msgstr "Modèles One2many"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model_terms:ir.ui.view,arch_db:tracking_manager.view_model_form
|
||||
msgid "One2many related models"
|
||||
msgstr "Modèles One2many présents"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_ir_model_fields__custom_tracking_id
|
||||
msgid "Technical Custom Tracking Field"
|
||||
msgstr ""
|
||||
|
||||
#. 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 "Tracking"
|
||||
msgstr "Suivi"
|
||||
|
||||
#. module: tracking_manager
|
||||
#: model:ir.model,name:tracking_manager.model_tracking_model_field
|
||||
#: model:ir.model.fields,field_description:tracking_manager.field_tracking_model_field__model_id
|
||||
msgid "Tracking Model Field"
|
||||
msgstr "Model suivi"
|
|
@ -0,0 +1,3 @@
|
|||
from . import mail_thread
|
||||
from . import tracking_model
|
||||
from . import ir_model
|
|
@ -0,0 +1,117 @@
|
|||
# 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 odoo import api, fields, models
|
||||
|
||||
|
||||
class IrModel(models.Model):
|
||||
_inherit = "ir.model"
|
||||
|
||||
o2m_model_ids = fields.One2many(
|
||||
string="One2many Models",
|
||||
comodel_name="ir.model",
|
||||
compute="_compute_o2m_model_ids",
|
||||
readonly=True,
|
||||
)
|
||||
custom_tracking_field_ids = fields.One2many(
|
||||
comodel_name="tracking.model.field",
|
||||
inverse_name="model_id",
|
||||
string="Custom Tracked Fields",
|
||||
compute="_compute_custom_tracking_fields",
|
||||
store=True,
|
||||
)
|
||||
field_count = fields.Integer(compute="_compute_tracked_field_count")
|
||||
apply_custom_tracking = fields.Boolean(
|
||||
"Apply custom tracking on fields",
|
||||
default=False,
|
||||
help="""Add tracking on all this model fields if they
|
||||
are not readonly True, neither computed.""",
|
||||
)
|
||||
|
||||
@api.depends("field_id", "apply_custom_tracking")
|
||||
def _compute_custom_tracking_fields(self):
|
||||
for rec in self:
|
||||
fields = rec.env["ir.model.fields"].search(
|
||||
[("model_id.model", "=", rec.model)]
|
||||
)
|
||||
fields_ids = []
|
||||
for field in fields:
|
||||
values = {}
|
||||
values["tracking_field_id"] = field.id
|
||||
values["model_id"] = rec.id
|
||||
fields_ids.append((0, 0, values))
|
||||
rec.custom_tracking_field_ids = [(6, 0, [])]
|
||||
rec.custom_tracking_field_ids = fields_ids
|
||||
|
||||
@api.depends("field_id", "apply_custom_tracking")
|
||||
def _compute_o2m_model_ids(self):
|
||||
for rec in self:
|
||||
o2m_model_list = []
|
||||
if rec.apply_custom_tracking:
|
||||
for field in rec.field_id:
|
||||
if field.ttype == "one2many":
|
||||
o2m_relation_id = self.search(
|
||||
[("model", "=", field.relation)], limit=1
|
||||
)
|
||||
o2m_model_list.append(o2m_relation_id.id)
|
||||
rec.o2m_model_ids = [(6, 0, o2m_model_list)]
|
||||
|
||||
@api.depends(
|
||||
"custom_tracking_field_ids", "custom_tracking_field_ids.custom_tracking"
|
||||
)
|
||||
def _compute_tracked_field_count(self):
|
||||
for rec in self:
|
||||
rec.field_count = sum(
|
||||
rec.custom_tracking_field_ids.mapped("custom_tracking")
|
||||
)
|
||||
|
||||
def show_custom_tracked_field(self):
|
||||
return {
|
||||
"name": "Custom tracked fields",
|
||||
"type": "ir.actions.act_window",
|
||||
"res_id": self.id,
|
||||
"view_mode": "tree",
|
||||
"res_model": "tracking.model.field",
|
||||
"view_id": self.env.ref(
|
||||
"tracking_manager.custom_tracking_field_view_tree"
|
||||
).id,
|
||||
"target": "current",
|
||||
"domain": [("id", "in", self.custom_tracking_field_ids.ids)],
|
||||
}
|
||||
|
||||
def show_o2m_models(self):
|
||||
return {
|
||||
"name": "o2m models",
|
||||
"type": "ir.actions.act_window",
|
||||
"res_id": self.id,
|
||||
"view_mode": "tree,form",
|
||||
"res_model": "ir.model",
|
||||
"views": [
|
||||
(self.env.ref("base.view_model_tree").id, "tree"),
|
||||
(self.env.ref("base.view_model_form").id, "form"),
|
||||
],
|
||||
"target": "current",
|
||||
"domain": [("id", "in", self.o2m_model_ids.ids)],
|
||||
}
|
||||
|
||||
|
||||
class IrModelFields(models.Model):
|
||||
_inherit = "ir.model.fields"
|
||||
|
||||
custom_tracking_id = fields.Many2one(
|
||||
comodel_name="tracking.model.field",
|
||||
string="Technical Custom Tracking Field",
|
||||
compute="_compute_custom_tracking_field_id",
|
||||
)
|
||||
|
||||
@api.depends("model_id.apply_custom_tracking")
|
||||
def _compute_custom_tracking_field_id(self):
|
||||
for rec in self:
|
||||
if rec.model_id.apply_custom_tracking:
|
||||
rec.custom_tracking_id = self.env["tracking.model.field"].search(
|
||||
[("tracking_field_id", "=", rec.id), ("model_id", "=", rec._name)],
|
||||
limit=1,
|
||||
)
|
||||
else:
|
||||
rec.custom_tracking_id = False
|
|
@ -0,0 +1,208 @@
|
|||
# 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 _get_tracked_fields(self):
|
||||
res = super()._get_tracked_fields()
|
||||
custom_tracked_fields = self._get_custom_tracked_fields()
|
||||
if custom_tracked_fields:
|
||||
return custom_tracked_fields
|
||||
return res
|
||||
|
||||
def _get_custom_tracked_fields(self):
|
||||
tracking_model = (
|
||||
self.env["ir.model"]
|
||||
.sudo()
|
||||
.search(
|
||||
[("model", "=", self._name), ("apply_custom_tracking", "=", True)],
|
||||
limit=1,
|
||||
)
|
||||
)
|
||||
if tracking_model:
|
||||
track_fields = tracking_model.custom_tracking_field_ids.filtered(
|
||||
lambda f: f.custom_tracking
|
||||
).mapped("name")
|
||||
return track_fields and set(self.fields_get(track_fields))
|
||||
return
|
||||
|
||||
def _create_tracking_message(self, mode, field_id, record):
|
||||
return {
|
||||
"mode": mode,
|
||||
"name": field_id.field_description,
|
||||
"record": self._get_tracked_record_name(record),
|
||||
}
|
||||
|
||||
def _get_tracked_record_name(self, record):
|
||||
return (
|
||||
getattr(record, "display_name", False)
|
||||
or getattr(record, "name", False)
|
||||
or f"No name, record :{record}"
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
fields = list(vals.keys())
|
||||
tracked_fields = self._get_custom_tracked_fields()
|
||||
m2m_o2m_tracked_message = []
|
||||
records = []
|
||||
if tracked_fields:
|
||||
records = [f for f in fields if f in tracked_fields]
|
||||
for field in records:
|
||||
del_and_chg_records = self._get_m2m_m2o_tracked_fields(
|
||||
field, tracked_fields, vals
|
||||
)
|
||||
if del_and_chg_records:
|
||||
m2m_o2m_tracked_message.extend(del_and_chg_records)
|
||||
res = super().write(vals)
|
||||
for field in records:
|
||||
add_records = self._check_o2m_add_lines(field, tracked_fields, vals)
|
||||
if add_records:
|
||||
m2m_o2m_tracked_message.extend(add_records)
|
||||
if m2m_o2m_tracked_message:
|
||||
self._post_custom_tracking_message(m2m_o2m_tracked_message)
|
||||
return res
|
||||
|
||||
def _get_m2m_m2o_tracked_fields(self, field, tracked_fields, vals):
|
||||
field_id = self.env["ir.model.fields"].search(
|
||||
[("name", "=", field), ("model_id", "=", self._name)], limit=1
|
||||
)
|
||||
message = []
|
||||
if field in tracked_fields and field_id.ttype in ["one2many", "many2many"]:
|
||||
if field_id.ttype == "one2many":
|
||||
message.extend(self._check_o2m_delete_lines(field_id, vals))
|
||||
message.extend(self._check_o2m_change_in_lines(field_id, vals))
|
||||
if field_id.ttype == "many2many":
|
||||
message.extend(self._check_m2m_tracking(field_id, vals))
|
||||
return message
|
||||
|
||||
def _check_o2m_delete_lines(self, field_id, vals):
|
||||
lines = [line for line in vals[field_id.name]]
|
||||
del_message = []
|
||||
for line in lines:
|
||||
if line[0] in [2, 3]:
|
||||
record = getattr(self, field_id.name).browse(line[-1] or line[1])
|
||||
message_line = self._create_tracking_message("Delete", field_id, record)
|
||||
del_message.append(message_line)
|
||||
return del_message
|
||||
|
||||
def _check_o2m_change_in_lines(self, field_id, vals):
|
||||
lines = [line for line in vals[field_id.name] if line[0] in [0, 1]]
|
||||
chg_message = []
|
||||
for line in lines:
|
||||
if len(line) == 3 and line[0] == 1:
|
||||
record = getattr(self, field_id.name).browse(line[1])
|
||||
line_fields = [f for f in line[2]]
|
||||
message_line = self._create_tracking_message("Change", field_id, record)
|
||||
changes = []
|
||||
for line_field in line_fields:
|
||||
line_field_id = self.env["ir.model.fields"].search(
|
||||
[("name", "=", line_field), ("model", "=", record._name)],
|
||||
limit=1,
|
||||
)
|
||||
line_model_id = self.env["ir.model"].search(
|
||||
[("model", "=", record._name)], limit=1
|
||||
)
|
||||
# if the o2m related model is configured for custom
|
||||
# tracking (apply_custom_tracking = True), we track
|
||||
# the changes only for fields with custom_tracking.
|
||||
if (
|
||||
line_model_id.apply_custom_tracking
|
||||
and not line_field_id.custom_tracking_id.custom_tracking
|
||||
):
|
||||
old = new = False
|
||||
elif line_field_id.ttype in ["one2many", "many2one", "many2many"]:
|
||||
old_ids = getattr(record, line_field).ids
|
||||
if line[2][line_field]:
|
||||
new_ids = line[2][line_field][0][2]
|
||||
new_line_ids = set(new_ids) - set(old_ids)
|
||||
delete_line_ids = set(old_ids) - set(new_ids)
|
||||
old = ", ".join(
|
||||
getattr(record, line_field)
|
||||
.browse(delete_line_ids)
|
||||
.mapped("name")
|
||||
)
|
||||
new = ", ".join(
|
||||
getattr(record, line_field)
|
||||
.browse(new_line_ids)
|
||||
.mapped("name")
|
||||
)
|
||||
else:
|
||||
old = getattr(record, line_field)
|
||||
new = line[2][line_field]
|
||||
if old != new:
|
||||
changes.append(
|
||||
{
|
||||
"name": line_field,
|
||||
"old": old,
|
||||
"new": new,
|
||||
}
|
||||
)
|
||||
if changes:
|
||||
message_line["changes"] = changes
|
||||
chg_message.append(message_line)
|
||||
return chg_message
|
||||
|
||||
def _check_o2m_add_lines(self, field, tracked_fields, vals):
|
||||
field_id = self.env["ir.model.fields"].search(
|
||||
[("name", "=", field), ("model_id", "=", self._name)], limit=1
|
||||
)
|
||||
if field in tracked_fields and vals.get(field) and field_id.ttype == "one2many":
|
||||
message = []
|
||||
line_ids_in_vals = [
|
||||
line[1]
|
||||
for line in vals[field]
|
||||
if line[0] != 4 or (line[0] == 4 and not line[-1])
|
||||
]
|
||||
new_ids = [
|
||||
line
|
||||
for line in [line.id for line in getattr(self, field)]
|
||||
if line not in line_ids_in_vals
|
||||
]
|
||||
for line in new_ids:
|
||||
record = getattr(self, field).browse(line)
|
||||
message_line = self._create_tracking_message("New", field_id, record)
|
||||
message.append(message_line)
|
||||
return message
|
||||
|
||||
def _check_m2m_tracking(self, field_id, vals):
|
||||
m2m_message = []
|
||||
new_ids = [line[-1] for line in vals[field_id.name] if line[0] in [0, 4]]
|
||||
delete_ids = [line[-1] for line in vals[field_id.name] if line[0] in [2, 3]]
|
||||
new_line_ids = set(new_ids) - set(getattr(self, field_id.name).ids)
|
||||
for line in new_line_ids:
|
||||
record = getattr(self, field_id.name).browse(line)
|
||||
message_line = self._create_tracking_message("New", field_id, record)
|
||||
m2m_message.append(message_line)
|
||||
for line in delete_ids:
|
||||
record = getattr(self, field_id.name).browse(line)
|
||||
message_line = self._create_tracking_message("Delete", field_id, record)
|
||||
m2m_message.append(message_line)
|
||||
return m2m_message
|
||||
|
||||
def _post_custom_tracking_message(self, message):
|
||||
formated_message = []
|
||||
for field in {line["name"] for line in message}:
|
||||
message_by_field = {"name": field, "message_by_field": []}
|
||||
modes = {line["mode"] for line in message if line["name"] == field}
|
||||
for mode in modes:
|
||||
message_by_mode = {"mode": mode}
|
||||
for line in message:
|
||||
if line["name"] == field and line["mode"] == mode:
|
||||
message_by_mode["record"] = line["record"]
|
||||
if line.get("changes", False):
|
||||
message_by_mode["changes"] = line["changes"]
|
||||
message_by_field["message_by_field"].append(message_by_mode)
|
||||
formated_message.append(message_by_field)
|
||||
self.message_post_with_view(
|
||||
"tracking_manager.track_o2m_m2m_template",
|
||||
values={
|
||||
"lines": formated_message,
|
||||
},
|
||||
subtype_id=self.env.ref("mail.mt_note").id,
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_tracking_model,tracking.model.user,model_tracking_model,base.group_user,1,1,1,1
|
||||
access_tracking_model_field,tracking.model.field.user,model_tracking_model_field,base.group_user,1,1,1,1
|
|
|
@ -0,0 +1,52 @@
|
|||
# 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 odoo import api, fields, models
|
||||
|
||||
|
||||
class TrackingModelField(models.Model):
|
||||
_name = "tracking.model.field"
|
||||
_description = "Tracking Model Field"
|
||||
|
||||
name = fields.Char(related="tracking_field_id.name")
|
||||
tracking_field_id = fields.Many2one(
|
||||
"ir.model.fields",
|
||||
"Field",
|
||||
)
|
||||
model_id = fields.Many2one(
|
||||
comodel_name="ir.model",
|
||||
string="Tracking Model Field",
|
||||
)
|
||||
custom_tracking = fields.Boolean(
|
||||
string="Custom Tracking",
|
||||
compute="_compute_custom_tracking",
|
||||
readonly=False,
|
||||
store=True,
|
||||
)
|
||||
native_tracking_field = fields.Integer(
|
||||
string="Native Tracking", related="tracking_field_id.tracking"
|
||||
)
|
||||
type_field = fields.Selection(
|
||||
string="Kind Field", related="tracking_field_id.ttype"
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"tracking_field_id",
|
||||
"tracking_field_id.readonly",
|
||||
"tracking_field_id.related",
|
||||
"tracking_field_id.store",
|
||||
)
|
||||
def _compute_custom_tracking(self):
|
||||
# No tracking on compute / readonly fields
|
||||
# Custom tracking include native tracking attribute.
|
||||
for rec in self:
|
||||
field_id = rec.tracking_field_id
|
||||
if (
|
||||
field_id.readonly
|
||||
or field_id.related
|
||||
or (field_id.compute and field_id.store and not field_id.readonly)
|
||||
) and not field_id.tracking:
|
||||
rec.custom_tracking = False
|
||||
else:
|
||||
rec.custom_tracking = True
|
|
@ -0,0 +1 @@
|
|||
* Kévin Roche <kevin.roche@akretion.com>
|
|
@ -0,0 +1,2 @@
|
|||
This module tracks 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 are also tracked (e.g. product_uom_qty of an order_line in a sale order).
|
|
@ -0,0 +1,4 @@
|
|||
- In setting > models: select a model
|
||||
- Check "Apply custom tracking on fields", all the non readonly, related, computed fields will be tracked.
|
||||
- Button or smart button "Tracked Fields" allow to activate / deactivate custom_tracking on each field
|
||||
- By default, all sub fields of a one2many field are tracked if they changed. To manage the sub fields, you need to activate "Apply custom tracking on fields" on the one2many model. Button "One2many related models" is a shortcut for these models.
|
|
@ -0,0 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_custom_tracking_model_field,tracking.model.field.user,model_tracking_model_field,base.group_user,1,1,1,1
|
|
|
@ -0,0 +1 @@
|
|||
from . import test_tracking_manager
|
|
@ -0,0 +1,155 @@
|
|||
# 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.tests.common import SavepointCase
|
||||
|
||||
|
||||
class TestTrackingManager(SavepointCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# Using the model res.partner for testing :
|
||||
# - street field as regular field
|
||||
# - parent_name as related field
|
||||
# - category_id as many2many field
|
||||
# - bank_ids as one2many field with acc_number field to test
|
||||
# changes.
|
||||
|
||||
cls.partner_1 = cls.env.ref("base.res_partner_1")
|
||||
cls.partner_1.bank_ids = [(6, 0, cls.env.ref("base.bank_partner_demo").ids)]
|
||||
cls.partner_model = cls.env["ir.model"].search(
|
||||
[("model", "=", cls.partner_1._name)], limit=1
|
||||
)
|
||||
cls.bank_partner_2 = cls.env["res.partner.bank"].create(
|
||||
{
|
||||
"acc_number": "1234567890",
|
||||
"partner_id": cls.partner_1.id,
|
||||
}
|
||||
)
|
||||
|
||||
def test_1_ir_model_config(self):
|
||||
self.partner_model.apply_custom_tracking = True
|
||||
# custom_tracking_field_ids
|
||||
self.assertEqual(
|
||||
len(self.partner_model.field_id),
|
||||
len(self.partner_model.custom_tracking_field_ids),
|
||||
)
|
||||
# o2m_model_ids
|
||||
self.assertEqual(
|
||||
len(self.partner_model.o2m_model_ids),
|
||||
len(self.partner_model.field_id.filtered(lambda x: x.ttype == "one2many")),
|
||||
)
|
||||
|
||||
def test_2_tracking_model_field_config(self):
|
||||
self.partner_2 = self.env.ref("base.res_partner_2")
|
||||
self.partner_1_w_parent = self.env.ref("base.res_partner_address_1")
|
||||
self.partner_model.apply_custom_tracking = True
|
||||
# Readonly, related are not tracked
|
||||
parent_name = self.partner_model.custom_tracking_field_ids.filtered(
|
||||
lambda x: x.name == "parent_name"
|
||||
)
|
||||
self.assertFalse(parent_name.custom_tracking)
|
||||
# Other fields are tracked
|
||||
street = self.partner_model.custom_tracking_field_ids.filtered(
|
||||
lambda x: x.name == "street"
|
||||
)
|
||||
self.assertTrue(street.custom_tracking)
|
||||
|
||||
def test_3_m2m_add_line(self):
|
||||
initial_msg = self.partner_1.message_ids
|
||||
self.partner_1.category_id = [
|
||||
(4, self.env.ref("base.res_partner_category_3").id)
|
||||
]
|
||||
after_msg = self.partner_1.message_ids
|
||||
self.assertEqual(len(initial_msg), len(after_msg))
|
||||
|
||||
self.partner_model.apply_custom_tracking = True
|
||||
self.partner_1.category_id = [
|
||||
(4, self.env.ref("base.res_partner_category_8").id)
|
||||
]
|
||||
after_msg = self.partner_1.message_ids
|
||||
self.assertEqual(len(initial_msg) + 1, len(after_msg))
|
||||
self.assertTrue("New" in after_msg[0].body)
|
||||
|
||||
def test_4_m2m_delete_line(self):
|
||||
initial_msg = self.partner_1.message_ids
|
||||
self.partner_1.category_id = [(3, self.partner_1.category_id[0].id)]
|
||||
after_msg = self.partner_1.message_ids
|
||||
self.assertEqual(len(initial_msg), len(after_msg))
|
||||
|
||||
self.partner_model.apply_custom_tracking = True
|
||||
self.partner_1.category_id = [(3, self.partner_1.category_id[0].id)]
|
||||
after_msg = self.partner_1.message_ids
|
||||
self.assertEqual(len(initial_msg) + 1, len(after_msg))
|
||||
self.assertTrue("Delete" in after_msg[0].body)
|
||||
|
||||
def test_5_m2m_multi_line(self):
|
||||
initial_msg = self.partner_1.message_ids
|
||||
self.partner_model.apply_custom_tracking = True
|
||||
self.partner_1.category_id = [
|
||||
(3, self.partner_1.category_id[0].id),
|
||||
(4, self.env.ref("base.res_partner_category_8").id),
|
||||
(4, self.env.ref("base.res_partner_category_11").id),
|
||||
]
|
||||
after_msg = self.partner_1.message_ids
|
||||
self.assertEqual(len(initial_msg) + 1, len(after_msg))
|
||||
self.assertEqual(after_msg[0].body.count("New"), 2)
|
||||
self.assertEqual(after_msg[0].body.count("Delete"), 1)
|
||||
|
||||
def test_6_o2m_add(self):
|
||||
initial_msg = self.partner_1.message_ids
|
||||
self.partner_model.apply_custom_tracking = True
|
||||
self.partner_1.bank_ids = [(4, self.bank_partner_2.id)]
|
||||
after_msg = self.partner_1.message_ids
|
||||
self.assertEqual(len(initial_msg) + 1, len(after_msg))
|
||||
self.assertTrue("New" in after_msg[0].body)
|
||||
|
||||
def test_7_o2m_delete(self):
|
||||
self.partner_model.apply_custom_tracking = True
|
||||
initial_msg = self.partner_1.message_ids
|
||||
self.partner_1.write({"bank_ids": [(3, self.partner_1.bank_ids[0].id)]})
|
||||
after_msg = self.partner_1.message_ids
|
||||
self.assertEqual(len(initial_msg) + 1, len(after_msg))
|
||||
self.assertTrue("Delete" in after_msg[0].body)
|
||||
|
||||
def test_8_o2m_change_in_line(self):
|
||||
self.partner_1.bank_ids = [(6, 0, self.bank_partner_2.id)]
|
||||
initial_msg = self.partner_1.message_ids
|
||||
self.partner_model.apply_custom_tracking = True
|
||||
self.partner_1.write(
|
||||
{
|
||||
"bank_ids": [(1, self.partner_1.bank_ids.id, {"acc_number": "123"})],
|
||||
}
|
||||
)
|
||||
after_msg = self.partner_1.message_ids
|
||||
self.assertEqual(len(initial_msg) + 1, len(after_msg))
|
||||
self.assertTrue("Change" in after_msg[0].body)
|
||||
# Restrict the tracking of acc_number
|
||||
bank_model = self.env["ir.model"].search(
|
||||
[("model", "=", self.bank_partner_2._name)], limit=1
|
||||
)
|
||||
bank_model.apply_custom_tracking = True
|
||||
acc_number = bank_model.custom_tracking_field_ids.filtered(
|
||||
lambda x: x.name == "acc_number"
|
||||
)
|
||||
acc_number.custom_tracking = False
|
||||
self.partner_1.write(
|
||||
{
|
||||
"bank_ids": [(1, self.partner_1.bank_ids.id, {"acc_number": "456"})],
|
||||
}
|
||||
)
|
||||
after_msg_2 = self.partner_1.message_ids
|
||||
self.assertEqual(len(after_msg), len(after_msg_2))
|
||||
|
||||
def test_9_o2m_multi_line(self):
|
||||
initial_msg = self.partner_1.message_ids
|
||||
self.partner_model.apply_custom_tracking = True
|
||||
self.partner_1.bank_ids = [
|
||||
(3, self.partner_1.bank_ids[0].id),
|
||||
(4, self.bank_partner_2.id),
|
||||
]
|
||||
after_msg = self.partner_1.message_ids
|
||||
self.assertEqual(len(initial_msg) + 1, len(after_msg))
|
||||
self.assertEqual(after_msg[0].body.count("New"), 1)
|
||||
self.assertEqual(after_msg[0].body.count("Delete"), 1)
|
|
@ -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="Tracking">
|
||||
<group>
|
||||
<field name="apply_custom_tracking" />
|
||||
<button
|
||||
name="show_custom_tracked_field"
|
||||
string="Custom Tracked fields"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
attrs="{'invisible': [('apply_custom_tracking', '=', False)]}"
|
||||
/>
|
||||
<button
|
||||
name="show_o2m_models"
|
||||
string="One2many related models"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
attrs="{'invisible': [('apply_custom_tracking', '=', False)]}"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
</xpath>
|
||||
<xpath expr="//sheet/group[1]" position="before">
|
||||
<div
|
||||
class="oe_button_box"
|
||||
name="button_box"
|
||||
attrs="{'invisible': [('apply_custom_tracking', '=', False)]}"
|
||||
>
|
||||
<button
|
||||
name="show_custom_tracked_field"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-server"
|
||||
>
|
||||
<field
|
||||
name="field_count"
|
||||
widget="statinfo"
|
||||
string="Tracked Fields"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="custom_tracking_view_tree" model="ir.ui.view">
|
||||
<field name="model">ir.model</field>
|
||||
<field name="name">tracking.ir.model.tree</field>
|
||||
<field name="inherit_id" ref="base.view_model_tree" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//tree/field[@name='transient']" position="after">
|
||||
<field name="apply_custom_tracking" readonly="True" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
|
@ -0,0 +1,19 @@
|
|||
<?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="custom_tracking_field_view_tree" model="ir.ui.view">
|
||||
<field name="model">tracking.model.field</field>
|
||||
<field name="name">tracking.model.field.tree</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom" create="false">
|
||||
<field name="name" readonly="True" />
|
||||
<field name="custom_tracking" widget="boolean_toggle" />
|
||||
<field name="native_tracking_field" readonly="True" />
|
||||
<field name="type_field" readonly="True" />
|
||||
<field name="model_id" readonly="True" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -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('message_by_field')" t-as="message">
|
||||
<ul>
|
||||
<t t-if="message.get('mode', False) == 'New'">
|
||||
<b>New :</b>
|
||||
</t>
|
||||
<t t-if="message.get('mode', False) == 'Delete'">
|
||||
<b>Delete :</b>
|
||||
</t>
|
||||
<t t-if="message.get('mode', False) == 'Change'">
|
||||
<b>Change :</b>
|
||||
</t>
|
||||
<t t-esc="message.get('record')" />
|
||||
<t t-if="message.get('mode', False) == 'Change'">
|
||||
<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>
|
Loading…
Reference in New Issue