From 056f895aae0c7067ac5cdd11ec5194feef987f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Tue, 21 Oct 2025 16:50:31 +0200 Subject: [PATCH 1/3] [IMP] RepositoryScanner: enable code scanning with new 'parse_code' parameter --- odoo_repository/lib/scanner.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/odoo_repository/lib/scanner.py b/odoo_repository/lib/scanner.py index d06dfd67..905bdb8a 100644 --- a/odoo_repository/lib/scanner.py +++ b/odoo_repository/lib/scanner.py @@ -15,7 +15,8 @@ import git import oca_port -from odoo_addons_parser import ModuleParser +from odoo_addons_parser import ModuleParser, OdooParser +from odoo_addons_parser.odoo import ODOO_BASE_ADDONS_PATH # Disable logging from 'pygount' (used by odoo_addons_parser) logging.getLogger("pygount").setLevel(logging.ERROR) @@ -869,6 +870,7 @@ def __init__( token: str = None, workaround_fs_errors: bool = False, clone_name: str = None, + parse_code: bool = False, ): super().__init__( org, @@ -885,6 +887,7 @@ def __init__( self.version = version self.branch = branch self.addons_paths_data = addons_paths_data + self.parse_code = parse_code def detect_modules_to_scan(self): res = self.sync() @@ -1057,9 +1060,28 @@ def _run_module_code_analysis( self, repo, module_path, branch, from_commit, to_commit ): """Perform a code analysis of `module_path`.""" - # Get current code analysis data - parser = ModuleParser(f"{self.path}/{module_path}", scan_models=False) - data = parser.to_dict() + module = pathlib.Path(module_path).parts[-1] + # When scanning 'base' module, we want to include framework base models + # in it as well (AbstractModel, Model...) to ease the data exploration + # afterwards. + if self.parse_code and module == "base": + parser = OdooParser( + self.path, + scan_models=self.parse_code, + # No addons path provided to scan only framework code + # Put ODOO_BASE_ADDONS_PATH to avoid the merge of modules data + # with results of two parsers. + # This will also scan other modules (mainly Odoo test modules) but + # it's fine. + addons_paths=(ODOO_BASE_ADDONS_PATH,), + base_models_key=module, + ) + data = parser.to_dict()[module] + else: + # Get current code analysis data + full_module_path = self.path.joinpath(module_path) + parser = ModuleParser(full_module_path, scan_models=self.parse_code) + data = parser.to_dict() # Append the history of versions versions = self._read_module_versions( repo, module_path, branch, from_commit, to_commit From f844acb609a88485860d4cc96b560425ca32043d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Tue, 21 Oct 2025 16:50:31 +0200 Subject: [PATCH 2/3] [ADD] odoo_repository_code: to scan modules code --- odoo_repository_code/README.rst | 83 ++++ odoo_repository_code/__init__.py | 1 + odoo_repository_code/__manifest__.py | 24 + odoo_repository_code/models/__init__.py | 7 + odoo_repository_code/models/odoo_model.py | 11 + .../models/odoo_model_version.py | 35 ++ .../models/odoo_module_branch.py | 221 +++++++++ .../models/odoo_module_branch_model.py | 294 ++++++++++++ .../models/odoo_module_branch_model_field.py | 215 +++++++++ .../models/odoo_module_branch_model_method.py | 155 +++++++ .../models/odoo_repository.py | 13 + odoo_repository_code/pyproject.toml | 3 + odoo_repository_code/readme/CONTRIBUTORS.md | 1 + odoo_repository_code/readme/DESCRIPTION.md | 6 + .../security/ir.model.access.csv | 9 + .../static/description/index.html | 430 ++++++++++++++++++ odoo_repository_code/views/menu.xml | 11 + .../views/odoo_model_version.xml | 37 ++ .../views/odoo_module_branch.xml | 34 ++ .../views/odoo_module_branch_model.xml | 201 ++++++++ .../views/odoo_module_branch_model_field.xml | 170 +++++++ .../views/odoo_module_branch_model_method.xml | 146 ++++++ 22 files changed, 2107 insertions(+) create mode 100644 odoo_repository_code/README.rst create mode 100644 odoo_repository_code/__init__.py create mode 100644 odoo_repository_code/__manifest__.py create mode 100644 odoo_repository_code/models/__init__.py create mode 100644 odoo_repository_code/models/odoo_model.py create mode 100644 odoo_repository_code/models/odoo_model_version.py create mode 100644 odoo_repository_code/models/odoo_module_branch.py create mode 100644 odoo_repository_code/models/odoo_module_branch_model.py create mode 100644 odoo_repository_code/models/odoo_module_branch_model_field.py create mode 100644 odoo_repository_code/models/odoo_module_branch_model_method.py create mode 100644 odoo_repository_code/models/odoo_repository.py create mode 100644 odoo_repository_code/pyproject.toml create mode 100644 odoo_repository_code/readme/CONTRIBUTORS.md create mode 100644 odoo_repository_code/readme/DESCRIPTION.md create mode 100644 odoo_repository_code/security/ir.model.access.csv create mode 100644 odoo_repository_code/static/description/index.html create mode 100644 odoo_repository_code/views/menu.xml create mode 100644 odoo_repository_code/views/odoo_model_version.xml create mode 100644 odoo_repository_code/views/odoo_module_branch.xml create mode 100644 odoo_repository_code/views/odoo_module_branch_model.xml create mode 100644 odoo_repository_code/views/odoo_module_branch_model_field.xml create mode 100644 odoo_repository_code/views/odoo_module_branch_model_method.xml diff --git a/odoo_repository_code/README.rst b/odoo_repository_code/README.rst new file mode 100644 index 00000000..f4480eb5 --- /dev/null +++ b/odoo_repository_code/README.rst @@ -0,0 +1,83 @@ +==================== +Odoo Repository Code +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:359273ec7d389c3dd991ea041c8277187c0af57b041d284ae6ac681670b6688c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmodule--composition--analysis-lightgray.png?logo=github + :target: https://github.com/OCA/module-composition-analysis/tree/18.0/odoo_repository_code + :alt: OCA/module-composition-analysis +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/module-composition-analysis-18-0/module-composition-analysis-18-0-odoo_repository_code + :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/module-composition-analysis&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This modules extends ``odoo_repository`` to also collect coding elements +from Odoo modules. + +It will collect the following: + +- Odoo models +- Fields +- Methods with their signature and body + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* sebalix + +Contributors +------------ + +- Sébastien Alix + +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. + +This module is part of the `OCA/module-composition-analysis `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo_repository_code/__init__.py b/odoo_repository_code/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/odoo_repository_code/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo_repository_code/__manifest__.py b/odoo_repository_code/__manifest__.py new file mode 100644 index 00000000..2722d243 --- /dev/null +++ b/odoo_repository_code/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2025 Sebastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Odoo Repository Code", + "summary": "Collect modules coding elements from Odoo Repositories.", + "version": "18.0.1.0.0", + "category": "Tools", + "author": "sebalix, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/module-composition-analysis", + "data": [ + "security/ir.model.access.csv", + "views/menu.xml", + "views/odoo_model_version.xml", + "views/odoo_module_branch_model.xml", + "views/odoo_module_branch_model_field.xml", + "views/odoo_module_branch_model_method.xml", + "views/odoo_module_branch.xml", + ], + "installable": True, + "depends": [ + "odoo_repository", + ], + "license": "AGPL-3", +} diff --git a/odoo_repository_code/models/__init__.py b/odoo_repository_code/models/__init__.py new file mode 100644 index 00000000..23139f1b --- /dev/null +++ b/odoo_repository_code/models/__init__.py @@ -0,0 +1,7 @@ +from . import odoo_repository +from . import odoo_model +from . import odoo_model_version +from . import odoo_module_branch +from . import odoo_module_branch_model +from . import odoo_module_branch_model_field +from . import odoo_module_branch_model_method diff --git a/odoo_repository_code/models/odoo_model.py b/odoo_repository_code/models/odoo_model.py new file mode 100644 index 00000000..b41b67a5 --- /dev/null +++ b/odoo_repository_code/models/odoo_model.py @@ -0,0 +1,11 @@ +# Copyright 2025 Sebastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class OdooModel(models.Model): + _name = "odoo.model" + _description = "Odoo Model Technical Name" + + name = fields.Char(required=True, index=True) diff --git a/odoo_repository_code/models/odoo_model_version.py b/odoo_repository_code/models/odoo_model_version.py new file mode 100644 index 00000000..4e9f06d0 --- /dev/null +++ b/odoo_repository_code/models/odoo_model_version.py @@ -0,0 +1,35 @@ +# Copyright 2025 Sebastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class OdooModelVersion(models.Model): + _name = "odoo.model.version" + _description = "Odoo Model Technical Name for Odoo Version" + + odoo_model_id = fields.Many2one( + comodel_name="odoo.model", + ondelete="cascade", + string="Odoo Model", + required=True, + index=True, + ) + odoo_version_id = fields.Many2one( + comodel_name="odoo.branch", + ondelete="restrict", + string="Odoo Version", + required=True, + index=True, + ) + name = fields.Char(compute="_compute_name", store=True) + module_branch_model_ids = fields.One2many( + comodel_name="odoo.module.branch.model", + inverse_name="odoo_model_version_id", + string="Models", + ) + + @api.depends("odoo_model_id", "odoo_version_id") + def _compute_name(self): + for rec in self: + rec.name = f"[{rec.odoo_version_id.name}] {rec.odoo_model_id.name}" diff --git a/odoo_repository_code/models/odoo_module_branch.py b/odoo_repository_code/models/odoo_module_branch.py new file mode 100644 index 00000000..c94b0334 --- /dev/null +++ b/odoo_repository_code/models/odoo_module_branch.py @@ -0,0 +1,221 @@ +# Copyright 2025 Sebastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models + + +class OdooModuleBranch(models.Model): + _inherit = "odoo.module.branch" + + model_ids = fields.One2many( + comodel_name="odoo.module.branch.model", + inverse_name="module_branch_id", + string="Models", + ) + models_count = fields.Integer(compute="_compute_models_count") + + @api.depends("model_ids") + def _compute_models_count(self): + for rec in self: + rec.models_count = len(self.model_ids) + + def open_models(self): + self.ensure_one() + xml_id = "odoo_repository_code.odoo_module_branch_model_action" + action = self.env["ir.actions.actions"]._for_xml_id(xml_id) + action["name"] = _("Models") + action["domain"] = [("id", "in", self.model_ids.ids)] + action["context"] = {} + return action + + def push_scanned_data(self, repo_branch_id, module, data): + module_branch = super().push_scanned_data(repo_branch_id, module, data) + module_branch._import_code(data) + return module_branch + + def _import_code(self, data): + self.ensure_one() + # Import Odoo data models only if the module is installable + if data.get("models") and self.installable: + self._import_code_odoo_models(data["models"]) + else: + self._cleanup_code_odoo_models() + + def _import_code_odoo_models(self, data_models): + self.ensure_one() + # These are entries created/updated from scanned module + mb_model_ids = [] + mb_model_field_ids = [] + mb_model_method_ids = [] + for model_name, data in data_models.items(): + odoo_model = self._get_or_create_odoo_model(model_name, data) + odoo_model_version = self._get_or_create_odoo_model_version(odoo_model) + vals = self._prepare_odoo_module_branch_model_values( + odoo_model_version, data + ) + mb_model = self._create_or_update_odoo_module_branch_model(vals) + mb_model_ids.append(mb_model.id) + # Import fields + for field_data in data.get("fields", {}).values(): + vals = self._prepare_odoo_module_branch_model_field_values( + mb_model, field_data + ) + field_ = self._create_or_update_odoo_module_branch_model_field(vals) + mb_model_field_ids.append(field_.id) + # Import methods + for method_data in data.get("methods", {}).values(): + vals = self._prepare_odoo_module_branch_model_method_values( + mb_model, method_data + ) + method = self._create_or_update_odoo_module_branch_model_method(vals) + mb_model_method_ids.append(method.id) + self._archive_code_elements( + mb_model_ids, mb_model_field_ids, mb_model_method_ids + ) + + def _cleanup_code_odoo_models(self): + self.ensure_one() + self.model_ids.sudo().unlink() + + def _archive_code_elements( + self, scanned_model_ids, scanned_field_ids, scanned_method_ids + ): + """Archive code elements that doesn't exist anymore.""" + models = ( + self.model_ids + - self.env["odoo.module.branch.model"].browse(scanned_model_ids).sudo() + ) + fields = ( + self.model_ids.field_ids + - self.env["odoo.module.branch.model.field"] + .browse(scanned_field_ids) + .sudo() + ) + methods = ( + self.model_ids.method_ids + - self.env["odoo.module.branch.model.method"] + .browse(scanned_method_ids) + .sudo() + ) + models.active = fields.active = methods.active = False + + def _prepare_odoo_module_branch_model_values(self, odoo_model_version, data): + self.ensure_one() + # Handle custom types + model_type = data["type"] + available_types = [ + elt[0] + for elt in self.env["odoo.module.branch.model"] + ._fields["model_type"] + .selection + ] + if model_type not in available_types: + model_type = "Other" + return { + "module_branch_id": self.id, + "odoo_model_version_id": odoo_model_version.id, + "data": data, + "model_type": model_type, + "active": True, + } + + def _create_or_update_odoo_module_branch_model(self, vals): + self.ensure_one() + rec = self.env["odoo.module.branch.model"].search( + [ + ("module_branch_id", "=", vals["module_branch_id"]), + ("odoo_model_version_id", "=", vals["odoo_model_version_id"]), + ] + ) + if rec: + rec.sudo().write(vals) + else: + rec = self.env["odoo.module.branch.model"].sudo().create(vals) + return rec + + def _get_or_create_odoo_model(self, model_name, data): + self.ensure_one() + rec = self.env["odoo.model"].search([("name", "=", model_name)], limit=1) + if not rec: + vals = {"name": model_name} + rec = self.env["odoo.model"].sudo().create(vals) + return rec + + def _get_or_create_odoo_model_version(self, odoo_model): + self.ensure_one() + rec = self.env["odoo.model.version"].search( + [ + ("odoo_model_id", "=", odoo_model.id), + ("odoo_version_id", "=", self.branch_id.id), + ], + limit=1, + ) + if not rec: + vals = { + "odoo_model_id": odoo_model.id, + "odoo_version_id": self.branch_id.id, + } + rec = self.env["odoo.model.version"].sudo().create(vals) + return rec + + # == fields import == + + def _prepare_odoo_module_branch_model_field_values(self, module_branch_model, data): + self.ensure_one() + return { + "module_branch_model_id": module_branch_model.id, + "name": data["name"], + "data": data, + "field_type": data["type"], + "active": True, + } + + def _create_or_update_odoo_module_branch_model_field(self, vals): + self.ensure_one() + model_ = self.env["odoo.module.branch.model.field"] + rec = model_.search( + [ + ("module_branch_model_id", "=", vals["module_branch_model_id"]), + ("name", "=", vals["name"]), + ], + limit=1, + ) + if rec: + rec.sudo().write(vals) + else: + rec = model_.sudo().create(vals) + return rec + + # == methods import == + + def _prepare_odoo_module_branch_model_method_values( + self, module_branch_model, data + ): + self.ensure_one() + return { + "module_branch_model_id": module_branch_model.id, + "name": data["name"], + "data": data, + "active": True, + } + + def _create_or_update_odoo_module_branch_model_method(self, vals): + self.ensure_one() + model_ = self.env["odoo.module.branch.model.method"] + rec = model_.search( + [ + ("module_branch_model_id", "=", vals["module_branch_model_id"]), + ("name", "=", vals["name"]), + ], + limit=1, + ) + if rec: + rec.sudo().write(vals) + else: + rec = model_.sudo().create(vals) + return rec + + def _to_dict(self): + data = super()._to_dict() + data["models"] = [model._to_dict() for model in self.model_ids] + return data diff --git a/odoo_repository_code/models/odoo_module_branch_model.py b/odoo_repository_code/models/odoo_module_branch_model.py new file mode 100644 index 00000000..66b965f9 --- /dev/null +++ b/odoo_repository_code/models/odoo_module_branch_model.py @@ -0,0 +1,294 @@ +# Copyright 2025 Sebastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from markupsafe import Markup +from odoo_addons_parser.code import BASE_CLASSES + +from odoo import _, api, fields, models +from odoo.tools.safe_eval import safe_eval + + +class OdooModuleBranchModel(models.Model): + _name = "odoo.module.branch.model" + _description = "Odoo model" + _order = ( + "repository_sequence, odoo_model_name, " + "odoo_version_sequence DESC, global_dependency_level, module_name" + ) + + module_branch_id = fields.Many2one( + comodel_name="odoo.module.branch", + ondelete="cascade", + string="Module", + index=True, + ) + module_id = fields.Many2one( + related="module_branch_id.module_id", store=True, index=True + ) + module_name = fields.Char( + related="module_branch_id.module_name", store=True, index=True + ) + odoo_model_version_id = fields.Many2one( + comodel_name="odoo.model.version", + ondelete="restrict", + string="Model (for this Odoo version)", + required=True, + index=True, + ) + odoo_model_id = fields.Many2one( + related="odoo_model_version_id.odoo_model_id", + string="Model", + store=True, + index=True, + ) + odoo_model_name = fields.Char( + related="odoo_model_version_id.odoo_model_id.name", + string="Model name", + store=True, + index=True, + ) + odoo_version_id = fields.Many2one( + related="module_branch_id.branch_id", store=True, index=True + ) + odoo_version_sequence = fields.Integer( + related="module_branch_id.branch_id.sequence", + store=True, + string="Odoo Version Sequence", + ) + org_id = fields.Many2one(related="module_branch_id.org_id", store=True, index=True) + repository_id = fields.Many2one( + related="module_branch_id.repository_id", store=True, index=True + ) + repository_sequence = fields.Integer( + related="module_branch_id.repository_id.sequence", + store=True, + string="Repository Sequence", + ) + global_dependency_level = fields.Integer( + string="Dep. Level", + related="module_branch_id.global_dependency_level", + store=True, + ) + name = fields.Char(compute="_compute_name", store=True) + active = fields.Boolean(default=True) + data = fields.Serialized() + model_type = fields.Selection( + selection=[ + ("AbstractModel", "AbstractModel"), + ("Model", "Model"), + ("TransientModel", "TransientModel"), + ("Other", "Other"), + ], + string="Type", + default="Model", + required=True, + index=True, + ) + order = fields.Char() + field_ids = fields.One2many( + comodel_name="odoo.module.branch.model.field", + inverse_name="module_branch_model_id", + string="Fields", + ) + method_ids = fields.One2many( + comodel_name="odoo.module.branch.model.method", + inverse_name="module_branch_model_id", + string="Methods", + ) + root_id = fields.Many2one( + string="Origin", + comodel_name="odoo.module.branch.model", + compute="_compute_root_id", + recursive=True, + ) + root_warning = fields.Html(compute="_compute_root_id") + inherit_ids = fields.Many2many( + comodel_name="odoo.model.version", + compute="_compute_inherit_ids", + ) + inherits_ids = fields.Many2many( + comodel_name="odoo.model.version", + compute="_compute_inherits_ids", + ) + next_odoo_version_model_id = fields.Many2one( + comodel_name="odoo.module.branch.model", + compute="_compute_next_odoo_version_model_id", + ) + + @api.depends("odoo_model_id", "module_branch_id") + def _compute_name(self): + for rec in self: + rec.name = ( + f"{rec.odoo_model_id.name} in {rec.module_branch_id.display_name}" + ) + + @api.depends("data") + def _compute_inherit_ids(self): + for rec in self: + rec.inherit_ids = False + inherit = safe_eval(repr(rec.data.get("inherit"))) + if isinstance(inherit, str): + inherit = [inherit] + if inherit: + models = self.env["odoo.model.version"].search( + [ + ("odoo_model_id", "in", inherit), + ("odoo_version_id", "=", rec.odoo_version_id.id), + ] + ) + rec.inherit_ids = models + + @api.depends("data") + def _compute_inherits_ids(self): + for rec in self: + rec.inherits_ids = False + inherits = safe_eval(repr(rec.data.get("inherits"))) + if inherits: + models = self.env["odoo.model.version"].search( + [ + ("odoo_model_id", "in", list(inherits)), + ("odoo_version_id", "=", rec.odoo_version_id.id), + ] + ) + rec.inherits_ids = models + + @api.depends("odoo_model_version_id") + def _compute_root_id(self): + for rec in self: + root = rec.search( + [("odoo_model_version_id", "=", rec.odoo_model_version_id.id)], + order="global_dependency_level", + limit=1, + ) + rec.root_id = root if root != rec else False + # Warning if the root module is not found in known dependencies + rec.root_warning = False + deps = rec.module_branch_id._get_recursive_dependencies() + if ( + rec.root_id + and root.module_branch_id not in deps + # All modules are depending on base even if not listed, + # no need to display a warning in such case + and rec.root_id.module_name != "base" + ): + rec.root_warning = _( + Markup( + "{root} is not in the known dependencies " + "of {module}." + ) + ).format(root=rec.root_id.module_name, module=rec.module_name) + + def _get_inherited_models(self): + """Return all inherited models, sorted by level of dependency. + + This is based on actual dependencies of current module. + """ + self.ensure_one() + # Get all dependencies of current module (including self) + dependencies = self.module_branch_id._get_recursive_dependencies() + dependencies |= self.module_branch_id + # Collect inherited models recursively + # NOTE: includes base models + inherited_model_ids = self.search( + [ + ("odoo_version_id", "=", self.odoo_version_id.id), + ("odoo_model_name", "in", BASE_CLASSES), + ] + ).ids + visited = set() + + def collect_inherited(current_model, inherited_model_ids=inherited_model_ids): + if current_model.id in visited: + return + visited.add(current_model.id) + + # For each model version this model inherits from + all_inherits = current_model.inherit_ids | current_model.inherits_ids + for model_version in all_inherits: + # Find implementations of this model version in dependencies + implementations = self.search( + [ + ("odoo_model_version_id", "=", model_version.id), + ("module_branch_id", "in", dependencies.ids), + ] + ) + for impl in implementations: + if impl.id not in inherited_model_ids: + inherited_model_ids.append(impl.id) + collect_inherited(impl, inherited_model_ids=inherited_model_ids) + + collect_inherited(self, inherited_model_ids=inherited_model_ids) + # Remove self from results + if self.id in inherited_model_ids: + inherited_model_ids.remove(self.id) + # Sort by dependency level (lower levels first = base modules first) + return self.search( + [("id", "in", inherited_model_ids)], order="global_dependency_level" + ) + + @api.depends("odoo_version_id.next_id") + def _compute_next_odoo_version_model_id(self): + for rec in self: + rec.next_odoo_version_model_id = False + # Stop there if no next version + if not rec.odoo_version_id.next_id: + continue + # Look for the next available version for this module name + rec.next_odoo_version_model_id = self.search( + [ + ( + "odoo_version_sequence", + ">=", + rec.odoo_version_id.next_id.sequence, + ), + ("odoo_model_id", "=", rec.odoo_model_id.id), + ], + order="odoo_version_sequence,global_dependency_level", + limit=1, + ) + # # Stop there if no renaming/relacement + # if not rec.timeline_ids: + # continue + # rec.next_odoo_version_module_branch_id = self.search( + # [ + # ("branch_sequence", ">=", rec.branch_id.next_id.sequence), + # ("module_id", "=", rec.timeline_ids.next_module_id.id), + # ], + # order="branch_sequence", + # limit=1, + # ) + + def open_next_odoo_version_model(self): + self.ensure_one() + xml_id = "odoo_repository_code.odoo_module_branch_model_action" + if not self.next_odoo_version_model_id: + return False + action = self.env["ir.actions.actions"]._for_xml_id(xml_id) + action["name"] = _("Next version") + # action["domain"] = [("id", "=", self.next_odoo_version_model_id.id)] + del action["view_id"] # = (468, 'odoo.module.branch.model.tree'), + action["view_mode"] = "form" + action["views"] = [(False, "form")] + action["res_id"] = self.next_odoo_version_model_id.id + return action + + def open_inherited_models(self): + self.ensure_one() + inherited_models = self._get_inherited_models() + xml_id = "odoo_repository_code.odoo_module_branch_model_action" + action = self.env["ir.actions.actions"]._for_xml_id(xml_id) + action["name"] = _("Inherited Models") + action["domain"] = [("id", "in", inherited_models.ids)] + action["context"] = {} + return action + + def _to_dict(self): + self.ensure_one() + return { + "name": self.odoo_model_name, + "inherit": self.inherit_ids.odoo_model_id.mapped("name"), + "inherits": self.inherits_ids.odoo_model_id.mapped("name"), + "type": self.model_type, + "order": self.order, + "fields": [rec._to_dict() for rec in self.field_ids], + "methods": [rec._to_dict() for rec in self.method_ids], + } diff --git a/odoo_repository_code/models/odoo_module_branch_model_field.py b/odoo_repository_code/models/odoo_module_branch_model_field.py new file mode 100644 index 00000000..c0f9ca22 --- /dev/null +++ b/odoo_repository_code/models/odoo_module_branch_model_field.py @@ -0,0 +1,215 @@ +# Copyright 2025 Sebastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class OdooModuleBranchModelField(models.Model): + _name = "odoo.module.branch.model.field" + _description = "Odoo field" + + module_branch_model_id = fields.Many2one( + comodel_name="odoo.module.branch.model", + ondelete="cascade", + string="Model", + required=True, + index=True, + ) + module_branch_id = fields.Many2one( + related="module_branch_model_id.module_branch_id", + store=True, + index=True, + ) + module_name = fields.Char( + related="module_branch_model_id.module_name", + string="Technical module name", + required=True, + store=True, + precompute=True, + index=True, + ) + odoo_model_id = fields.Many2one( + related="module_branch_model_id.odoo_model_id", + string="Model ", + required=True, + store=True, + precompute=True, + index=True, + ) + odoo_version_id = fields.Many2one( + related="module_branch_model_id.odoo_version_id", + string="Odoo Version", + required=True, + store=True, + precompute=True, + index=True, + ) + org_id = fields.Many2one(related="module_branch_id.org_id", store=True, index=True) + repository_id = fields.Many2one( + related="module_branch_id.repository_id", store=True, index=True + ) + global_dependency_level = fields.Integer( + string="Dep. Level", + related="module_branch_id.global_dependency_level", + store=True, + ) + name = fields.Char(required=True, index=True) + active = fields.Boolean(default=True) + field_type = fields.Char(string="Type", required=True, index=True) + data = fields.Serialized() + code = fields.Text(compute="_compute_code", store=True, index="trigram") + comodel_name = fields.Char(compute="_compute_comodel_name", store=True, index=True) + comodel_id = fields.Many2one( + comodel_name="odoo.model.version", + compute="_compute_comodel_id", + ) + is_relational = fields.Boolean(compute="_compute_is_relational", store=True) + is_computed = fields.Boolean(compute="_compute_is_computed", store=True) + is_readonly = fields.Boolean(compute="_compute_is_readonly", store=True) + is_required = fields.Boolean(compute="_compute_is_required", store=True) + is_stored = fields.Boolean(compute="_compute_is_stored", store=True) + inverse_method = fields.Char( + string="Inverse method name", + compute="_compute_methods", + store=True, + ) + inverse_method_id = fields.Many2one( + comodel_name="odoo.module.branch.model.method", + compute="_compute_methods_id", + ) + search_method = fields.Char( + string="Search method name", + compute="_compute_methods", + store=True, + ) + search_method_id = fields.Many2one( + comodel_name="odoo.module.branch.model.method", + compute="_compute_methods_id", + ) + + @api.depends("data") + def _compute_code(self): + for rec in self: + rec.code = rec.data.get("code", False) + + @api.depends("data") + def _compute_comodel_name(self): + for rec in self: + rec.comodel_name = rec.data.get("comodel_name") + + @api.depends("comodel_name", "odoo_model_id", "odoo_version_id") + def _compute_comodel_id(self): + model_model = self.env["odoo.model.version"] + for rec in self: + rec.comodel_id = False + if rec.comodel_name: + model = model_model.search( + [ + ("odoo_model_id.name", "=", rec.comodel_name), + ("odoo_version_id", "=", rec.odoo_version_id.id), + ] + ) + rec.comodel_id = model + + @api.depends("field_type") + def _compute_is_relational(self): + for rec in self: + rec.is_relational = ( + rec.field_type.startswith("One2") + or rec.field_type.startswith("Many2") + or rec.field_type == "Reference" + ) + + @api.depends("data") + def _compute_is_computed(self): + for rec in self: + # Default: only computed + rec.is_computed = False + kwargs = rec.data.get("kwargs", {}) + # Case of onchange computed field => we do not consider it as computed + if ( + kwargs.get("compute") + and kwargs.get("readonly") is False + and kwargs.get("store") + ): + continue + rec.is_computed = kwargs.get("compute") or kwargs.get("related") + + @api.depends("data") + def _compute_is_readonly(self): + for rec in self: + # Default: not readonly + rec.is_readonly = False + kwargs = rec.data.get("kwargs", {}) + # Simple case: 'readonly' attribute manually set + if "readonly" in kwargs: + rec.is_readonly = kwargs["readonly"] + # Computed field without inverse + elif kwargs.get("compute") and not kwargs.get("inverse"): + rec.is_readonly = True + # Related field + elif kwargs.get("related"): + rec.is_readonly = True + + @api.depends("data") + def _compute_is_required(self): + for rec in self: + # Default: not required + rec.is_required = False + kwargs = rec.data.get("kwargs", {}) + # Simple case: 'required' attribute manually set + if "required" in kwargs: + rec.is_required = kwargs["required"] + + @api.depends("data") + def _compute_is_stored(self): + for rec in self: + # Default: stored + rec.is_stored = True + kwargs = rec.data.get("kwargs", {}) + # Simple case: 'store' attribute manually set + if "store" in kwargs: + rec.is_stored = kwargs["store"] + # Computed or related field + elif kwargs.get("compute") or kwargs.get("related"): + rec.is_stored = False + + @api.depends("data") + def _compute_methods(self): + for rec in self: + kwargs = rec.data.get("kwargs", {}) + rec.inverse_method = kwargs.get("inverse") + rec.search_method = kwargs.get("search") + + @api.depends("inverse_method", "search_method") + def _compute_methods_id(self): + method_model = self.env["odoo.module.branch.model.method"] + for rec in self: + rec.inverse_method_id = rec.search_method_id = False + # Inverse method + if rec.inverse_method: + method = method_model.search( + [ + ("module_branch_model_id", "=", rec.module_branch_model_id.id), + ("name", "=", rec.inverse_method), + ], + limit=1, + ) + rec.inverse_method_id = method + # Search method + if rec.search_method: + method = method_model.search( + [ + ("module_branch_model_id", "=", rec.module_branch_model_id.id), + ("name", "=", rec.search_method), + ], + limit=1, + ) + rec.search_method_id = method + + def _to_dict(self): + self.ensure_one() + return { + "name": self.name, + "field_type": self.field_type, + } diff --git a/odoo_repository_code/models/odoo_module_branch_model_method.py b/odoo_repository_code/models/odoo_module_branch_model_method.py new file mode 100644 index 00000000..a3f6e296 --- /dev/null +++ b/odoo_repository_code/models/odoo_module_branch_model_method.py @@ -0,0 +1,155 @@ +# Copyright 2025 Sebastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models + + +class OdooModuleBranchModelMethod(models.Model): + _name = "odoo.module.branch.model.method" + _description = "Odoo method" + _order = "odoo_model_name, name" + + module_branch_model_id = fields.Many2one( + comodel_name="odoo.module.branch.model", + ondelete="cascade", + string="Model", + required=True, + index=True, + ) + module_branch_id = fields.Many2one( + related="module_branch_model_id.module_branch_id", + store=True, + index=True, + ) + module_name = fields.Char( + related="module_branch_model_id.module_name", + string="Technical module name", + required=True, + store=True, + precompute=True, + index=True, + ) + odoo_model_id = fields.Many2one( + related="module_branch_model_id.odoo_model_id", + string="Model ", + required=True, + store=True, + precompute=True, + index=True, + ) + odoo_model_name = fields.Char( + related="module_branch_model_id.odoo_model_name", + string="Model name", + required=True, + store=True, + precompute=True, + index=True, + ) + odoo_version_id = fields.Many2one( + related="module_branch_model_id.odoo_version_id", + string="Odoo Version", + required=True, + store=True, + precompute=True, + index=True, + ) + org_id = fields.Many2one(related="module_branch_id.org_id", store=True, index=True) + repository_id = fields.Many2one( + related="module_branch_id.repository_id", store=True, index=True + ) + global_dependency_level = fields.Integer( + string="Dep. Level", + related="module_branch_id.global_dependency_level", + store=True, + ) + display_name = fields.Char( + compute="_compute_display_name", + store=True, + index=True, + ) + name = fields.Char(required=True, index=True) + active = fields.Boolean(default=True) + data = fields.Serialized() + signature = fields.Char(compute="_compute_signature", store=True) + code = fields.Text(compute="_compute_code", store=True) + global_dependency_level = fields.Integer( + related="module_branch_model_id.module_branch_id.global_dependency_level", + store=True, + ) + root_id = fields.Many2one( + string="Origin", + comodel_name="odoo.module.branch.model.method", + compute="_compute_root_id", + ) + parent_ids = fields.One2many( + comodel_name="odoo.module.branch.model.method", + compute="_compute_parent_ids", + string="Parent Methods", + ) + + @api.depends("odoo_model_name", "name") + def _compute_display_name(self): + for rec in self: + model_name = rec.odoo_model_name + rec.display_name = f"<{model_name}>.{rec.name}" + + @api.depends("data") + def _compute_signature(self): + for rec in self: + signature = ", ".join(rec.data.get("signature", [])) + rec.signature = f"{rec.name}({signature})" + + @api.depends("data") + def _compute_code(self): + for rec in self: + decorators = "\n @".join(rec.data.get("decorators", [])).strip() + if decorators: + decorators = f" @{decorators}\n" + code = rec.data.get("code", "") + rec.code = f"{decorators}{code}" + + @api.depends("module_branch_model_id.module_branch_id", "odoo_version_id", "name") + def _compute_root_id(self): + for rec in self: + parent_methods = rec._get_parent_methods() + root = fields.first(parent_methods) + rec.root_id = root if root != rec else False + + @api.depends("module_branch_model_id", "name") + def _compute_parent_ids(self): + for rec in self: + rec.parent_ids = rec._get_parent_methods() + + def _get_parent_methods(self, order="global_dependency_level"): + """Return all parent methods from dependencies (call stack).""" + self.ensure_one() + # Get all inherited models + inherited_models = self.module_branch_model_id._get_inherited_models() + # Find methods with the same name in inherited models + parent_methods = self.search( + [ + ("module_branch_model_id", "in", inherited_models.ids), + ("name", "=", self.name), + ], + order=order, + ) + return parent_methods + + def open_parent_methods(self): + self.ensure_one() + xml_id = "odoo_repository_code.odoo_module_branch_model_method_action2" + action = self.env["ir.actions.actions"]._for_xml_id(xml_id) + action["name"] = _("Parent Methods") + action["domain"] = [("id", "in", self.parent_ids.ids)] + action["context"] = {} + return action + + def _to_dict(self, code=False): + self.ensure_one() + data = { + "name": self.name, + "signature": self.signature, + } + if code: + data["code"] = self.code + return data diff --git a/odoo_repository_code/models/odoo_repository.py b/odoo_repository_code/models/odoo_repository.py new file mode 100644 index 00000000..fcdb2d40 --- /dev/null +++ b/odoo_repository_code/models/odoo_repository.py @@ -0,0 +1,13 @@ +# Copyright 2025 Sebastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class OdooRepository(models.Model): + _inherit = "odoo.repository" + + def _prepare_scanner_parameters(self, version, branch): + params = super()._prepare_scanner_parameters(version, branch) + params["parse_code"] = True + return params diff --git a/odoo_repository_code/pyproject.toml b/odoo_repository_code/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/odoo_repository_code/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/odoo_repository_code/readme/CONTRIBUTORS.md b/odoo_repository_code/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..09975b58 --- /dev/null +++ b/odoo_repository_code/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Sébastien Alix \ diff --git a/odoo_repository_code/readme/DESCRIPTION.md b/odoo_repository_code/readme/DESCRIPTION.md new file mode 100644 index 00000000..f0565f28 --- /dev/null +++ b/odoo_repository_code/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +This modules extends `odoo_repository` to also collect coding elements from Odoo modules. + +It will collect the following: +- Odoo models +- Fields +- Methods with their signature and body diff --git a/odoo_repository_code/security/ir.model.access.csv b/odoo_repository_code/security/ir.model.access.csv new file mode 100644 index 00000000..4ad9e737 --- /dev/null +++ b/odoo_repository_code/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_odoo_model_user,odoo_model_user,model_odoo_model,odoo_repository.group_odoo_repository_user,1,0,0,0 +access_odoo_model_version_user,odoo_model_version_user,model_odoo_model_version,odoo_repository.group_odoo_repository_user,1,0,0,0 +access_odoo_module_branch_model_user,odoo_module_branch_model_user,model_odoo_module_branch_model,odoo_repository.group_odoo_repository_user,1,0,0,0 +access_odoo_module_branch_model_admin,odoo_module_branch_model_admin,model_odoo_module_branch_model,base.group_system,1,1,0,0 +access_odoo_module_branch_model_field_user,odoo_module_branch_model_field_user,model_odoo_module_branch_model_field,odoo_repository.group_odoo_repository_user,1,0,0,0 +access_odoo_module_branch_model_field_admin,odoo_module_branch_model_field_admin,model_odoo_module_branch_model_field,base.group_system,1,1,0,0 +access_odoo_module_branch_model_method_user,odoo_module_branch_model_method_user,model_odoo_module_branch_model_method,odoo_repository.group_odoo_repository_user,1,0,0,0 +access_odoo_module_branch_model_method_admin,odoo_module_branch_model_method_admin,model_odoo_module_branch_model_method,base.group_system,1,1,0,0 diff --git a/odoo_repository_code/static/description/index.html b/odoo_repository_code/static/description/index.html new file mode 100644 index 00000000..3d4a7a9d --- /dev/null +++ b/odoo_repository_code/static/description/index.html @@ -0,0 +1,430 @@ + + + + + +Odoo Repository Code + + + +
+

Odoo Repository Code

+ + +

Beta License: AGPL-3 OCA/module-composition-analysis Translate me on Weblate Try me on Runboat

+

This modules extends odoo_repository to also collect coding elements +from Odoo modules.

+

It will collect the following:

+
    +
  • Odoo models
  • +
  • Fields
  • +
  • Methods with their signature and body
  • +
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • sebalix
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

This module is part of the OCA/module-composition-analysis project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo_repository_code/views/menu.xml b/odoo_repository_code/views/menu.xml new file mode 100644 index 00000000..6954cf14 --- /dev/null +++ b/odoo_repository_code/views/menu.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/odoo_repository_code/views/odoo_model_version.xml b/odoo_repository_code/views/odoo_model_version.xml new file mode 100644 index 00000000..9923eef2 --- /dev/null +++ b/odoo_repository_code/views/odoo_model_version.xml @@ -0,0 +1,37 @@ + + + + + odoo.model.version.form + odoo.model.version + +
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/odoo_repository_code/views/odoo_module_branch.xml b/odoo_repository_code/views/odoo_module_branch.xml new file mode 100644 index 00000000..86d56134 --- /dev/null +++ b/odoo_repository_code/views/odoo_module_branch.xml @@ -0,0 +1,34 @@ + + + + + odoo.module.branch.form.inherit + odoo.module.branch + + +
+ +
+
+
+ + + odoo.module.branch.search.inherit + odoo.module.branch + + + + + + + +
diff --git a/odoo_repository_code/views/odoo_module_branch_model.xml b/odoo_repository_code/views/odoo_module_branch_model.xml new file mode 100644 index 00000000..b7b2786b --- /dev/null +++ b/odoo_repository_code/views/odoo_module_branch_model.xml @@ -0,0 +1,201 @@ + + + + + odoo.module.branch.model.form + odoo.module.branch.model + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + odoo.module.branch.model.method.list + odoo.module.branch.model.method + + + + + + + + + + + + + + odoo.module.branch.model.method.list + odoo.module.branch.model.method + + + + + + + + + + + odoo.module.branch.model.method.search + odoo.module.branch.model.method + search + + + + + + + + + + + + + + + + + + + + + + + Methods + ir.actions.act_window + odoo.module.branch.model.method + + {'search_default_group_by_odoo_version_id': 1, 'search_default_group_by_odoo_model_id': 2} + + + + Methods + ir.actions.act_window + odoo.module.branch.model.method + + + + +
From 9e5766c01106818d27bd6821ded4a9fd353ffe08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Sat, 20 Jun 2026 14:35:12 +0200 Subject: [PATCH 3/3] fixup! [ADD] odoo_repository_code: to scan modules code --- odoo_repository_code/models/__init__.py | 1 + .../models/odoo_module_branch_model.py | 100 +++++++++--- .../models/odoo_module_branch_model_field.py | 154 ++++++------------ .../models/odoo_module_branch_model_method.py | 75 +-------- ...odoo_module_branch_model_resource_mixin.py | 79 +++++++++ .../security/ir.model.access.csv | 3 - .../views/odoo_module_branch_model.xml | 62 +++---- .../views/odoo_module_branch_model_field.xml | 88 +++++----- .../views/odoo_module_branch_model_method.xml | 15 +- 9 files changed, 282 insertions(+), 295 deletions(-) create mode 100644 odoo_repository_code/models/odoo_module_branch_model_resource_mixin.py diff --git a/odoo_repository_code/models/__init__.py b/odoo_repository_code/models/__init__.py index 23139f1b..d0b7a523 100644 --- a/odoo_repository_code/models/__init__.py +++ b/odoo_repository_code/models/__init__.py @@ -3,5 +3,6 @@ from . import odoo_model_version from . import odoo_module_branch from . import odoo_module_branch_model +from . import odoo_module_branch_model_resource_mixin from . import odoo_module_branch_model_field from . import odoo_module_branch_model_method diff --git a/odoo_repository_code/models/odoo_module_branch_model.py b/odoo_repository_code/models/odoo_module_branch_model.py index 66b965f9..95a853d3 100644 --- a/odoo_repository_code/models/odoo_module_branch_model.py +++ b/odoo_repository_code/models/odoo_module_branch_model.py @@ -1,9 +1,11 @@ # Copyright 2025 Sebastien Alix # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import psycopg2.sql as pgsql from markupsafe import Markup from odoo_addons_parser.code import BASE_CLASSES from odoo import _, api, fields, models +from odoo.osv.expression import SQL_OPERATORS from odoo.tools.safe_eval import safe_eval @@ -22,10 +24,16 @@ class OdooModuleBranchModel(models.Model): index=True, ) module_id = fields.Many2one( - related="module_branch_id.module_id", store=True, index=True + string="Module technical name", + related="module_branch_id.module_id", + store=True, + index=True, ) module_name = fields.Char( - related="module_branch_id.module_name", store=True, index=True + string="Module technical name ", + related="module_branch_id.module_name", + store=True, + index="trigram", ) odoo_model_version_id = fields.Many2one( comodel_name="odoo.model.version", @@ -68,7 +76,9 @@ class OdooModuleBranchModel(models.Model): related="module_branch_id.global_dependency_level", store=True, ) - name = fields.Char(compute="_compute_name", store=True) + display_name = fields.Char( + compute="_compute_display_name", store=True, index="trigram" + ) active = fields.Boolean(default=True) data = fields.Serialized() model_type = fields.Selection( @@ -104,10 +114,12 @@ class OdooModuleBranchModel(models.Model): inherit_ids = fields.Many2many( comodel_name="odoo.model.version", compute="_compute_inherit_ids", + search="_search_inherit_ids", ) inherits_ids = fields.Many2many( comodel_name="odoo.model.version", compute="_compute_inherits_ids", + search="_search_inherits_ids", ) next_odoo_version_model_id = fields.Many2one( comodel_name="odoo.module.branch.model", @@ -115,11 +127,12 @@ class OdooModuleBranchModel(models.Model): ) @api.depends("odoo_model_id", "module_branch_id") - def _compute_name(self): + def _compute_display_name(self): for rec in self: - rec.name = ( - f"{rec.odoo_model_id.name} in {rec.module_branch_id.display_name}" - ) + model_name = rec.odoo_model_id.name + if model_name not in BASE_CLASSES: + model_name = f"<{model_name}>" + rec.display_name = f"{model_name} in {rec.module_branch_id.display_name}" @api.depends("data") def _compute_inherit_ids(self): @@ -137,11 +150,32 @@ def _compute_inherit_ids(self): ) rec.inherit_ids = models + def _search_inherit_ids(self, operator, value): + sql_operator = SQL_OPERATORS[operator].code + query = pgsql.SQL( + """ + SELECT array_agg(id) + FROM {table} + WHERE data::json->>'inherit' {operator} %s; + """ + ) + query = query.format( + table=pgsql.Identifier(self._table), + operator=pgsql.SQL(sql_operator), + ) + args = (f"%{value}%",) + self.env.cr.execute(query, args) + model_ids = self.env.cr.fetchone()[0] or [] + return [("id", "in", model_ids)] + @api.depends("data") def _compute_inherits_ids(self): for rec in self: rec.inherits_ids = False - inherits = safe_eval(repr(rec.data.get("inherits"))) + if not rec.data.get("inherits"): + continue + inherits_list = list(rec.data["inherits"]) + inherits = [safe_eval(elt) for elt in inherits_list] if inherits: models = self.env["odoo.model.version"].search( [ @@ -151,6 +185,24 @@ def _compute_inherits_ids(self): ) rec.inherits_ids = models + def _search_inherits_ids(self, operator, value): + sql_operator = SQL_OPERATORS[operator].code + query = pgsql.SQL( + """ + SELECT array_agg(id) + FROM {table} + WHERE data::json->>'inherits' {operator} %s; + """ + ) + query = query.format( + table=pgsql.Identifier(self._table), + operator=pgsql.SQL(sql_operator), + ) + args = (f"%{value}%",) + self.env.cr.execute(query, args) + model_ids = self.env.cr.fetchone()[0] or [] + return [("id", "in", model_ids)] + @api.depends("odoo_model_version_id") def _compute_root_id(self): for rec in self: @@ -177,8 +229,8 @@ def _compute_root_id(self): ) ).format(root=rec.root_id.module_name, module=rec.module_name) - def _get_inherited_models(self): - """Return all inherited models, sorted by level of dependency. + def _get_parent_models(self, include_self=False): + """Return all parent models, sorted by level of dependency. This is based on actual dependencies of current module. """ @@ -186,9 +238,9 @@ def _get_inherited_models(self): # Get all dependencies of current module (including self) dependencies = self.module_branch_id._get_recursive_dependencies() dependencies |= self.module_branch_id - # Collect inherited models recursively + # Collect parent models recursively # NOTE: includes base models - inherited_model_ids = self.search( + parent_model_ids = self.search( [ ("odoo_version_id", "=", self.odoo_version_id.id), ("odoo_model_name", "in", BASE_CLASSES), @@ -196,7 +248,7 @@ def _get_inherited_models(self): ).ids visited = set() - def collect_inherited(current_model, inherited_model_ids=inherited_model_ids): + def collect_parents(current_model, parent_model_ids=parent_model_ids): if current_model.id in visited: return visited.add(current_model.id) @@ -212,17 +264,17 @@ def collect_inherited(current_model, inherited_model_ids=inherited_model_ids): ] ) for impl in implementations: - if impl.id not in inherited_model_ids: - inherited_model_ids.append(impl.id) - collect_inherited(impl, inherited_model_ids=inherited_model_ids) + if impl.id not in parent_model_ids: + parent_model_ids.append(impl.id) + collect_parents(impl, parent_model_ids=parent_model_ids) - collect_inherited(self, inherited_model_ids=inherited_model_ids) + collect_parents(self, parent_model_ids=parent_model_ids) # Remove self from results - if self.id in inherited_model_ids: - inherited_model_ids.remove(self.id) + if not include_self and self.id in parent_model_ids: + parent_model_ids.remove(self.id) # Sort by dependency level (lower levels first = base modules first) return self.search( - [("id", "in", inherited_model_ids)], order="global_dependency_level" + [("id", "in", parent_model_ids)], order="global_dependency_level" ) @api.depends("odoo_version_id.next_id") @@ -271,13 +323,13 @@ def open_next_odoo_version_model(self): action["res_id"] = self.next_odoo_version_model_id.id return action - def open_inherited_models(self): + def open_parent_models(self): self.ensure_one() - inherited_models = self._get_inherited_models() + parent_models = self._get_parent_models(include_self=True) xml_id = "odoo_repository_code.odoo_module_branch_model_action" action = self.env["ir.actions.actions"]._for_xml_id(xml_id) - action["name"] = _("Inherited Models") - action["domain"] = [("id", "in", inherited_models.ids)] + action["name"] = _("Parent Models") + action["domain"] = [("id", "in", parent_models.ids)] action["context"] = {} return action diff --git a/odoo_repository_code/models/odoo_module_branch_model_field.py b/odoo_repository_code/models/odoo_module_branch_model_field.py index c0f9ca22..8bb93cc6 100644 --- a/odoo_repository_code/models/odoo_module_branch_model_field.py +++ b/odoo_repository_code/models/odoo_module_branch_model_field.py @@ -1,60 +1,14 @@ # Copyright 2025 Sebastien Alix # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import _, api, fields, models class OdooModuleBranchModelField(models.Model): _name = "odoo.module.branch.model.field" + _inherit = "odoo.module.branch.model.resource.mixin" _description = "Odoo field" - module_branch_model_id = fields.Many2one( - comodel_name="odoo.module.branch.model", - ondelete="cascade", - string="Model", - required=True, - index=True, - ) - module_branch_id = fields.Many2one( - related="module_branch_model_id.module_branch_id", - store=True, - index=True, - ) - module_name = fields.Char( - related="module_branch_model_id.module_name", - string="Technical module name", - required=True, - store=True, - precompute=True, - index=True, - ) - odoo_model_id = fields.Many2one( - related="module_branch_model_id.odoo_model_id", - string="Model ", - required=True, - store=True, - precompute=True, - index=True, - ) - odoo_version_id = fields.Many2one( - related="module_branch_model_id.odoo_version_id", - string="Odoo Version", - required=True, - store=True, - precompute=True, - index=True, - ) - org_id = fields.Many2one(related="module_branch_id.org_id", store=True, index=True) - repository_id = fields.Many2one( - related="module_branch_id.repository_id", store=True, index=True - ) - global_dependency_level = fields.Integer( - string="Dep. Level", - related="module_branch_id.global_dependency_level", - store=True, - ) - name = fields.Char(required=True, index=True) - active = fields.Boolean(default=True) field_type = fields.Char(string="Type", required=True, index=True) data = fields.Serialized() code = fields.Text(compute="_compute_code", store=True, index="trigram") @@ -64,10 +18,6 @@ class OdooModuleBranchModelField(models.Model): compute="_compute_comodel_id", ) is_relational = fields.Boolean(compute="_compute_is_relational", store=True) - is_computed = fields.Boolean(compute="_compute_is_computed", store=True) - is_readonly = fields.Boolean(compute="_compute_is_readonly", store=True) - is_required = fields.Boolean(compute="_compute_is_required", store=True) - is_stored = fields.Boolean(compute="_compute_is_stored", store=True) inverse_method = fields.Char( string="Inverse method name", compute="_compute_methods", @@ -86,6 +36,16 @@ class OdooModuleBranchModelField(models.Model): comodel_name="odoo.module.branch.model.method", compute="_compute_methods_id", ) + root_id = fields.Many2one( + string="Origin", + comodel_name="odoo.module.branch.model.field", + compute="_compute_root_id", + ) + parent_ids = fields.One2many( + comodel_name="odoo.module.branch.model.field", + compute="_compute_parent_ids", + string="Parent Fields", + ) @api.depends("data") def _compute_code(self): @@ -120,60 +80,6 @@ def _compute_is_relational(self): or rec.field_type == "Reference" ) - @api.depends("data") - def _compute_is_computed(self): - for rec in self: - # Default: only computed - rec.is_computed = False - kwargs = rec.data.get("kwargs", {}) - # Case of onchange computed field => we do not consider it as computed - if ( - kwargs.get("compute") - and kwargs.get("readonly") is False - and kwargs.get("store") - ): - continue - rec.is_computed = kwargs.get("compute") or kwargs.get("related") - - @api.depends("data") - def _compute_is_readonly(self): - for rec in self: - # Default: not readonly - rec.is_readonly = False - kwargs = rec.data.get("kwargs", {}) - # Simple case: 'readonly' attribute manually set - if "readonly" in kwargs: - rec.is_readonly = kwargs["readonly"] - # Computed field without inverse - elif kwargs.get("compute") and not kwargs.get("inverse"): - rec.is_readonly = True - # Related field - elif kwargs.get("related"): - rec.is_readonly = True - - @api.depends("data") - def _compute_is_required(self): - for rec in self: - # Default: not required - rec.is_required = False - kwargs = rec.data.get("kwargs", {}) - # Simple case: 'required' attribute manually set - if "required" in kwargs: - rec.is_required = kwargs["required"] - - @api.depends("data") - def _compute_is_stored(self): - for rec in self: - # Default: stored - rec.is_stored = True - kwargs = rec.data.get("kwargs", {}) - # Simple case: 'store' attribute manually set - if "store" in kwargs: - rec.is_stored = kwargs["store"] - # Computed or related field - elif kwargs.get("compute") or kwargs.get("related"): - rec.is_stored = False - @api.depends("data") def _compute_methods(self): for rec in self: @@ -207,6 +113,42 @@ def _compute_methods_id(self): ) rec.search_method_id = method + @api.depends("module_branch_model_id.module_branch_id", "odoo_version_id", "name") + def _compute_root_id(self): + for rec in self: + parent_fields = rec._get_parent_fields() + root = fields.first(parent_fields) + rec.root_id = root if root != rec else False + + @api.depends("module_branch_model_id", "name") + def _compute_parent_ids(self): + for rec in self: + rec.parent_ids = rec._get_parent_fields() + + def _get_parent_fields(self, order="global_dependency_level"): + """Return all parent fields from dependencies (call stack).""" + self.ensure_one() + # Get all parent models + parent_models = self.module_branch_model_id._get_parent_models() + # Find fields with the same name in parent models + parent_fields = self.search( + [ + ("module_branch_model_id", "in", parent_models.ids), + ("name", "=", self.name), + ], + order=order, + ) + return parent_fields + + def open_parent_fields(self): + self.ensure_one() + xml_id = "odoo_repository_code.odoo_module_branch_model_field_action2" + action = self.env["ir.actions.actions"]._for_xml_id(xml_id) + action["name"] = _("Parent Fields") + action["domain"] = [("id", "in", self.parent_ids.ids)] + action["context"] = {} + return action + def _to_dict(self): self.ensure_one() return { diff --git a/odoo_repository_code/models/odoo_module_branch_model_method.py b/odoo_repository_code/models/odoo_module_branch_model_method.py index a3f6e296..c49a6346 100644 --- a/odoo_repository_code/models/odoo_module_branch_model_method.py +++ b/odoo_repository_code/models/odoo_module_branch_model_method.py @@ -6,69 +6,10 @@ class OdooModuleBranchModelMethod(models.Model): _name = "odoo.module.branch.model.method" + _inherit = "odoo.module.branch.model.resource.mixin" _description = "Odoo method" _order = "odoo_model_name, name" - module_branch_model_id = fields.Many2one( - comodel_name="odoo.module.branch.model", - ondelete="cascade", - string="Model", - required=True, - index=True, - ) - module_branch_id = fields.Many2one( - related="module_branch_model_id.module_branch_id", - store=True, - index=True, - ) - module_name = fields.Char( - related="module_branch_model_id.module_name", - string="Technical module name", - required=True, - store=True, - precompute=True, - index=True, - ) - odoo_model_id = fields.Many2one( - related="module_branch_model_id.odoo_model_id", - string="Model ", - required=True, - store=True, - precompute=True, - index=True, - ) - odoo_model_name = fields.Char( - related="module_branch_model_id.odoo_model_name", - string="Model name", - required=True, - store=True, - precompute=True, - index=True, - ) - odoo_version_id = fields.Many2one( - related="module_branch_model_id.odoo_version_id", - string="Odoo Version", - required=True, - store=True, - precompute=True, - index=True, - ) - org_id = fields.Many2one(related="module_branch_id.org_id", store=True, index=True) - repository_id = fields.Many2one( - related="module_branch_id.repository_id", store=True, index=True - ) - global_dependency_level = fields.Integer( - string="Dep. Level", - related="module_branch_id.global_dependency_level", - store=True, - ) - display_name = fields.Char( - compute="_compute_display_name", - store=True, - index=True, - ) - name = fields.Char(required=True, index=True) - active = fields.Boolean(default=True) data = fields.Serialized() signature = fields.Char(compute="_compute_signature", store=True) code = fields.Text(compute="_compute_code", store=True) @@ -87,12 +28,6 @@ class OdooModuleBranchModelMethod(models.Model): string="Parent Methods", ) - @api.depends("odoo_model_name", "name") - def _compute_display_name(self): - for rec in self: - model_name = rec.odoo_model_name - rec.display_name = f"<{model_name}>.{rec.name}" - @api.depends("data") def _compute_signature(self): for rec in self: @@ -123,12 +58,12 @@ def _compute_parent_ids(self): def _get_parent_methods(self, order="global_dependency_level"): """Return all parent methods from dependencies (call stack).""" self.ensure_one() - # Get all inherited models - inherited_models = self.module_branch_model_id._get_inherited_models() - # Find methods with the same name in inherited models + # Get all parent models + parent_models = self.module_branch_model_id._get_parent_models() + # Find methods with the same name in parent models parent_methods = self.search( [ - ("module_branch_model_id", "in", inherited_models.ids), + ("module_branch_model_id", "in", parent_models.ids), ("name", "=", self.name), ], order=order, diff --git a/odoo_repository_code/models/odoo_module_branch_model_resource_mixin.py b/odoo_repository_code/models/odoo_module_branch_model_resource_mixin.py new file mode 100644 index 00000000..a22bca3f --- /dev/null +++ b/odoo_repository_code/models/odoo_module_branch_model_resource_mixin.py @@ -0,0 +1,79 @@ +# Copyright 2025 Sebastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class OdooModuleBranchModelResourceMixin(models.AbstractModel): + _name = "odoo.module.branch.model.resource.mixin" + _description = "Odoo Model Resource Mixin" + _order = "odoo_model_name, name" + + module_branch_model_id = fields.Many2one( + comodel_name="odoo.module.branch.model", + ondelete="cascade", + string="Model", + required=True, + index=True, + ) + module_branch_id = fields.Many2one( + related="module_branch_model_id.module_branch_id", + store=True, + index=True, + ) + module_name = fields.Char( + related="module_branch_model_id.module_name", + string="Technical module name", + required=True, + store=True, + precompute=True, + index=True, + ) + odoo_model_id = fields.Many2one( + related="module_branch_model_id.odoo_model_id", + string="Model ", + required=True, + store=True, + precompute=True, + index=True, + ) + odoo_model_name = fields.Char( + related="module_branch_model_id.odoo_model_name", + string="Model name", + required=True, + store=True, + precompute=True, + index=True, + ) + odoo_version_id = fields.Many2one( + related="module_branch_model_id.odoo_version_id", + string="Odoo Version", + required=True, + store=True, + precompute=True, + index=True, + ) + org_id = fields.Many2one(related="module_branch_id.org_id", store=True, index=True) + repository_id = fields.Many2one( + related="module_branch_id.repository_id", store=True, index=True + ) + global_dependency_level = fields.Integer( + string="Dep. Level", + related="module_branch_id.global_dependency_level", + store=True, + ) + display_name = fields.Char( + compute="_compute_display_name", + store=True, + index="trigram", + ) + name = fields.Char(required=True, index=True) + active = fields.Boolean(default=True) + + @api.depends("module_name", "odoo_model_name", "name") + def _compute_display_name(self): + for rec in self: + model_name = rec.odoo_model_name + rec.display_name = ( + f"<{model_name}>.{rec.name} in {rec.module_branch_id.display_name}" + ) diff --git a/odoo_repository_code/security/ir.model.access.csv b/odoo_repository_code/security/ir.model.access.csv index 4ad9e737..91922c69 100644 --- a/odoo_repository_code/security/ir.model.access.csv +++ b/odoo_repository_code/security/ir.model.access.csv @@ -2,8 +2,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_odoo_model_user,odoo_model_user,model_odoo_model,odoo_repository.group_odoo_repository_user,1,0,0,0 access_odoo_model_version_user,odoo_model_version_user,model_odoo_model_version,odoo_repository.group_odoo_repository_user,1,0,0,0 access_odoo_module_branch_model_user,odoo_module_branch_model_user,model_odoo_module_branch_model,odoo_repository.group_odoo_repository_user,1,0,0,0 -access_odoo_module_branch_model_admin,odoo_module_branch_model_admin,model_odoo_module_branch_model,base.group_system,1,1,0,0 access_odoo_module_branch_model_field_user,odoo_module_branch_model_field_user,model_odoo_module_branch_model_field,odoo_repository.group_odoo_repository_user,1,0,0,0 -access_odoo_module_branch_model_field_admin,odoo_module_branch_model_field_admin,model_odoo_module_branch_model_field,base.group_system,1,1,0,0 access_odoo_module_branch_model_method_user,odoo_module_branch_model_method_user,model_odoo_module_branch_model_method,odoo_repository.group_odoo_repository_user,1,0,0,0 -access_odoo_module_branch_model_method_admin,odoo_module_branch_model_method_admin,model_odoo_module_branch_model_method,base.group_system,1,1,0,0 diff --git a/odoo_repository_code/views/odoo_module_branch_model.xml b/odoo_repository_code/views/odoo_module_branch_model.xml index b7b2786b..c0fb7572 100644 --- a/odoo_repository_code/views/odoo_module_branch_model.xml +++ b/odoo_repository_code/views/odoo_module_branch_model.xml @@ -10,12 +10,12 @@
- + @@ -49,40 +49,27 @@ /> - - - - - - - - - - - - + + + + + + + + + - - - - - - + + + + + + + + - - - - + + odoo.module.branch.model.field.list + odoo.module.branch.model.field + + + + + + + + + odoo.module.branch.model.field.search odoo.module.branch.model.field @@ -85,36 +97,11 @@ - - - - - - + {'search_default_group_by_odoo_version_id': 1, 'search_default_group_by_odoo_model_id': 2} + + FIelds + ir.actions.act_window + odoo.module.branch.model.field + + + Parents - - - - - - + + + + + - +