Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,4 @@
from . import mail_thread
from . import ir_model
from . import models
from . import mail_tracking_value

View file

@ -0,0 +1,206 @@
# 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
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 tick new field will be automatically tracked if the domain match"),
)
automatic_custom_tracking_domain = fields.Char(
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: 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: 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:
record.automatic_custom_tracking_domain = str(
rules.get(record.model) or rules.get("default_automatic_rule")
)
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.clear_caches()
return super().write(vals)
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.clear_caches()
self.check_access_rights("write")
custom_tracking = vals.pop("custom_tracking")
self._write({"custom_tracking": custom_tracking})
self.invalidate_model(fnames=["custom_tracking"])
return super().write(vals)

View file

@ -0,0 +1,17 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# @author Kévin Roche <kevin.roche@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models, tools
class MailThread(models.AbstractModel):
_inherit = "mail.thread"
@tools.ormcache("self.env.uid", "self.env.su")
def _track_get_fields(self):
fields_per_models = self.env["ir.model"]._get_custom_tracked_fields_per_model()
if self._name in fields_per_models:
return set(self.fields_get(fields_per_models[self._name]))
else:
return super()._track_get_fields()

View file

@ -0,0 +1,28 @@
# Copyright 2023 Akretion (https://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models
from ..tools import format_m2m
class MailTrackingValue(models.Model):
_inherit = "mail.tracking.value"
def create_tracking_values(
self,
initial_value,
new_value,
col_name,
col_info,
tracking_sequence,
model_name,
):
if col_info["type"] == "many2many":
col_info["type"] = "text"
initial_value = format_m2m(initial_value)
new_value = format_m2m(new_value)
return super().create_tracking_values(
initial_value, new_value, col_name, col_info, tracking_sequence, model_name
)

View file

@ -0,0 +1,177 @@
# Copyright 2023 Akretion (https://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from collections import defaultdict
from odoo import api, models, tools
from 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]
model_name = target_id = False
if isinstance(owner, models.BaseModel):
model_name = owner._name
target_id = owner.id
# In case of specific O2M (ex: ir.attachment with res_id)
elif isinstance(owner, int) and hasattr(self, "res_model"):
model_name = self.res_model
target_id = owner
if model_name and target_id:
data[model_name][target_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_view", 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()