[14.0][ADD] tracking_manager

pull/2789/head
Kev-Roche 2022-10-20 18:34:47 +02:00 committed by Christopher Rogos
parent a7bb9b1433
commit 70e3e93a07
17 changed files with 898 additions and 0 deletions

View File

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

View File

@ -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",
],
}

View File

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

View File

@ -0,0 +1,3 @@
from . import mail_thread
from . import tracking_model
from . import ir_model

View File

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

View File

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

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_tracking_model tracking.model.user model_tracking_model base.group_user 1 1 1 1
3 access_tracking_model_field tracking.model.field.user model_tracking_model_field base.group_user 1 1 1 1

View File

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

View File

@ -0,0 +1 @@
* Kévin Roche <kevin.roche@akretion.com>

View File

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

View File

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

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_custom_tracking_model_field tracking.model.field.user model_tracking_model_field base.group_user 1 1 1 1

View File

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

View File

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

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright (C) 2022 Akretion (<http://www.akretion.com>).
@author Kévin Roche <kevin.roche@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_model_form" model="ir.ui.view">
<field name="name">tracking.ir.model form</field>
<field name="model">ir.model</field>
<field name="inherit_id" ref="base.view_model_form" />
<field name="arch" type="xml">
<xpath expr="//sheet/group[1]" position="after">
<group string="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>

View File

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

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('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>