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 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..d0b7a523 --- /dev/null +++ b/odoo_repository_code/models/__init__.py @@ -0,0 +1,8 @@ +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_resource_mixin +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..95a853d3 --- /dev/null +++ b/odoo_repository_code/models/odoo_module_branch_model.py @@ -0,0 +1,346 @@ +# 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 + + +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( + string="Module technical name", + related="module_branch_id.module_id", + store=True, + index=True, + ) + module_name = fields.Char( + 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", + 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, + ) + display_name = fields.Char( + compute="_compute_display_name", store=True, index="trigram" + ) + 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", + 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", + compute="_compute_next_odoo_version_model_id", + ) + + @api.depends("odoo_model_id", "module_branch_id") + def _compute_display_name(self): + for rec in self: + 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): + 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 + + 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 + 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( + [ + ("odoo_model_id", "in", list(inherits)), + ("odoo_version_id", "=", rec.odoo_version_id.id), + ] + ) + 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: + 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_parent_models(self, include_self=False): + """Return all parent 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 parent models recursively + # NOTE: includes base models + parent_model_ids = self.search( + [ + ("odoo_version_id", "=", self.odoo_version_id.id), + ("odoo_model_name", "in", BASE_CLASSES), + ] + ).ids + visited = set() + + def collect_parents(current_model, parent_model_ids=parent_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 parent_model_ids: + parent_model_ids.append(impl.id) + collect_parents(impl, parent_model_ids=parent_model_ids) + + collect_parents(self, parent_model_ids=parent_model_ids) + # Remove self from results + 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", parent_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_parent_models(self): + self.ensure_one() + 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"] = _("Parent Models") + action["domain"] = [("id", "in", parent_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..8bb93cc6 --- /dev/null +++ b/odoo_repository_code/models/odoo_module_branch_model_field.py @@ -0,0 +1,157 @@ +# 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" + _inherit = "odoo.module.branch.model.resource.mixin" + _description = "Odoo field" + + 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) + 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", + ) + 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): + 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_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 + + @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 { + "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..c49a6346 --- /dev/null +++ b/odoo_repository_code/models/odoo_module_branch_model_method.py @@ -0,0 +1,90 @@ +# 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" + _inherit = "odoo.module.branch.model.resource.mixin" + _description = "Odoo method" + _order = "odoo_model_name, name" + + 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("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 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", parent_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_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/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..91922c69 --- /dev/null +++ b/odoo_repository_code/security/ir.model.access.csv @@ -0,0 +1,6 @@ +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_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_method_user,odoo_module_branch_model_method_user,model_odoo_module_branch_model_method,odoo_repository.group_odoo_repository_user,1,0,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..c0fb7572 --- /dev/null +++ b/odoo_repository_code/views/odoo_module_branch_model.xml @@ -0,0 +1,189 @@ + + + + + odoo.module.branch.model.form + odoo.module.branch.model + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + odoo.module.branch.model.list + odoo.module.branch.model + + + + + + + + + + + + + + + odoo.module.branch.model.tree2 + odoo.module.branch.model + + + + + + + + + + + + + + odoo.module.branch.model.search + odoo.module.branch.model + search + + + + + + + + + + + + + + + + + + + + + + + + + + + + Models + ir.actions.act_window + odoo.module.branch.model + + {'search_default_group_by_odoo_version_id': 1} + + + +
diff --git a/odoo_repository_code/views/odoo_module_branch_model_field.xml b/odoo_repository_code/views/odoo_module_branch_model_field.xml new file mode 100644 index 00000000..3db05717 --- /dev/null +++ b/odoo_repository_code/views/odoo_module_branch_model_field.xml @@ -0,0 +1,164 @@ + + + + + odoo.module.branch.model.field.form + odoo.module.branch.model.field + +
+ +
+ +
+ + + + + + + + + + + + + + + +
+
+
+
+ + + odoo.module.branch.model.field.list + odoo.module.branch.model.field + + + + + + + + + + + + + + + odoo.module.branch.model.field.list + odoo.module.branch.model.field + + + + + + + + + + + odoo.module.branch.model.field.search + odoo.module.branch.model.field + search + + + + + + + + + + + + + + + + + + + + + + + + Fields + ir.actions.act_window + odoo.module.branch.model.field + + {'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 + + + + +
diff --git a/odoo_repository_code/views/odoo_module_branch_model_method.xml b/odoo_repository_code/views/odoo_module_branch_model_method.xml new file mode 100644 index 00000000..91fe6b7e --- /dev/null +++ b/odoo_repository_code/views/odoo_module_branch_model_method.xml @@ -0,0 +1,145 @@ + + + + + odoo.module.branch.model.method.form + odoo.module.branch.model.method + +
+ +
+ +
+ + + + + + + + +
+
+
+
+ + + 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 + + + + +