From 5ae2eb75a9fa2739e23b5a3340a001f94de9c033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Fri, 1 Sep 2023 16:14:23 +0200 Subject: [PATCH 01/38] Add 'odoo_project' Module listing all modules belonging to an Odoo project by leveraging the data provided by 'odoo_repository'. --- odoo_project/__init__.py | 2 + odoo_project/__manifest__.py | 21 ++++ odoo_project/models/__init__.py | 2 + odoo_project/models/odoo_module_branch.py | 15 +++ odoo_project/models/odoo_project.py | 117 ++++++++++++++++++ odoo_project/security/ir.model.access.csv | 4 + odoo_project/views/odoo_module_branch.xml | 37 ++++++ odoo_project/views/odoo_project.xml | 110 ++++++++++++++++ odoo_project/wizards/__init__.py | 1 + .../wizards/odoo_project_import_modules.py | 79 ++++++++++++ .../wizards/odoo_project_import_modules.xml | 33 +++++ 11 files changed, 421 insertions(+) create mode 100644 odoo_project/__init__.py create mode 100644 odoo_project/__manifest__.py create mode 100644 odoo_project/models/__init__.py create mode 100644 odoo_project/models/odoo_module_branch.py create mode 100644 odoo_project/models/odoo_project.py create mode 100644 odoo_project/security/ir.model.access.csv create mode 100644 odoo_project/views/odoo_module_branch.xml create mode 100644 odoo_project/views/odoo_project.xml create mode 100644 odoo_project/wizards/__init__.py create mode 100644 odoo_project/wizards/odoo_project_import_modules.py create mode 100644 odoo_project/wizards/odoo_project_import_modules.xml diff --git a/odoo_project/__init__.py b/odoo_project/__init__.py new file mode 100644 index 00000000..aee8895e --- /dev/null +++ b/odoo_project/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/odoo_project/__manifest__.py b/odoo_project/__manifest__.py new file mode 100644 index 00000000..64cd3410 --- /dev/null +++ b/odoo_project/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +{ + "name": "Odoo Project", + "summary": "Analyze your Odoo projects code bases.", + "version": "16.0.1.0.0", + "category": "Tools", + "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/TODO", + "data": [ + "security/ir.model.access.csv", + "views/odoo_module_branch.xml", + "views/odoo_project.xml", + "wizards/odoo_project_import_modules.xml", + ], + "installable": True, + "depends": [ + "odoo_repository", + ], + "license": "AGPL-3", +} diff --git a/odoo_project/models/__init__.py b/odoo_project/models/__init__.py new file mode 100644 index 00000000..1a51d937 --- /dev/null +++ b/odoo_project/models/__init__.py @@ -0,0 +1,2 @@ +from . import odoo_project +from . import odoo_module_branch diff --git a/odoo_project/models/odoo_module_branch.py b/odoo_project/models/odoo_module_branch.py new file mode 100644 index 00000000..d1adb2a4 --- /dev/null +++ b/odoo_project/models/odoo_module_branch.py @@ -0,0 +1,15 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class OdooModuleBranch(models.Model): + _inherit = "odoo.module.branch" + + odoo_project_ids = fields.Many2many( + comodel_name="odoo.project", + relation="odoo_project_module_branch_rel", + column1="module_branch_id", column2="odoo_project_id", + string="Projects", + ) diff --git a/odoo_project/models/odoo_project.py b/odoo_project/models/odoo_project.py new file mode 100644 index 00000000..ebb142a8 --- /dev/null +++ b/odoo_project/models/odoo_project.py @@ -0,0 +1,117 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import ast + +from odoo import api, fields, models + + +class OdooProject(models.Model): + _name = "odoo.project" + _inherits = {"odoo.repository": "repository_id"} + _description = "Odoo Project" + _order = "name" + + name = fields.Char(required=True, index=True) + active = fields.Boolean(default=True) + repository_id = fields.Many2one( + comodel_name="odoo.repository", + ondelete="restrict", + string="Repository", + domain=[ + ("clone_branch_id", "!=", False), + ("odoo_version_id", "!=", False), + ], + required=True, + ) + module_branch_ids = fields.Many2many( + comodel_name="odoo.module.branch", + relation="odoo_project_module_branch_rel", + column1="odoo_project_id", column2="module_branch_id", + string="Modules/Branch", + ) + module_ids = fields.Many2many( + comodel_name="odoo.module", + relation="odoo_project_module_rel", + column1="odoo_project_id", column2="module_id", + string="Modules", + compute="_compute_module_ids", + store=True, + ) + modules_count = fields.Integer(compute="_compute_modules_count") + module_not_installed_ids = fields.Many2many( + comodel_name="odoo.module.branch", + string="Modules not installed", + help="Modules available in the project repository but not installed.", + compute="_compute_module_not_installed_ids", + ) + unmerged_module_ids = fields.Many2many( + comodel_name="odoo.module.branch", + string="Modules to merge", + help="Modules installed belonging to an open PR.", + compute="_compute_unmerged_module_ids", + ) + unknown_module_ids = fields.Many2many( + comodel_name="odoo.module.branch", + string="Modules unknown", + help="Modules installed but cannot be found among repositories/branches.", + compute="_compute_unknown_module_ids", + ) + + @api.depends("module_branch_ids.module_id") + def _compute_module_ids(self): + for rec in self: + rec.module_ids = rec.module_branch_ids.module_id.ids + + @api.depends("module_branch_ids") + def _compute_modules_count(self): + for rec in self: + rec.modules_count = len(rec.module_branch_ids) + + @api.depends("repository_id.branch_ids.module_ids", "module_branch_ids") + def _compute_module_not_installed_ids(self): + for rec in self: + all_module_ids = set(rec.repository_id.branch_ids.module_ids.ids) + installed_module_ids = set(rec.module_branch_ids.ids) + rec.module_not_installed_ids = list(all_module_ids - installed_module_ids) + + @api.depends("module_branch_ids.pr_url") + def _compute_unmerged_module_ids(self): + for rec in self: + rec.unmerged_module_ids = rec.module_branch_ids.filtered( + lambda module: module.pr_url + ) + + @api.depends("module_branch_ids.repository_id") + def _compute_unknown_module_ids(self): + for rec in self: + rec.unknown_module_ids = rec.module_branch_ids.filtered( + lambda module: not module.repository_id + ) + + def open_import_modules(self): + """Open a wizard to import the modules of this Odoo project.""" + self.ensure_one() + action = self.env["ir.actions.actions"]._for_xml_id( + "odoo_project.odoo_project_import_modules_action" + ) + ctx = action.get("context", {}) + if isinstance(ctx, str): + ctx = ast.literal_eval(ctx) + ctx["default_odoo_project_id"] = self.id + action["context"] = ctx + return action + + def open_modules(self): + self.ensure_one() + action = self.env["ir.actions.actions"]._for_xml_id( + "odoo_repository.odoo_module_branch_action" + ) + ctx = action.get("context", {}) + if isinstance(ctx, str): + ctx = ast.literal_eval(ctx) + action["domain"] = [("id", "in", self.module_branch_ids.ids)] + ctx["search_default_group_by_org_id"] = 1 + ctx["search_default_group_by_repository_id"] = 2 + action["context"] = ctx + return action diff --git a/odoo_project/security/ir.model.access.csv b/odoo_project/security/ir.model.access.csv new file mode 100644 index 00000000..6cbba4ce --- /dev/null +++ b/odoo_project/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_odoo_project_user,odoo_project_user,model_odoo_project,base.group_user,1,1,1,0 +access_odoo_project_manager_manager,odoo_project_manager,model_odoo_project,base.group_system,1,1,1,1 +access_odoo_project_import_modules_user,odoo_project_import_modules_user,model_odoo_project_import_modules,base.group_user,1,1,1,1 diff --git a/odoo_project/views/odoo_module_branch.xml b/odoo_project/views/odoo_module_branch.xml new file mode 100644 index 00000000..84147403 --- /dev/null +++ b/odoo_project/views/odoo_module_branch.xml @@ -0,0 +1,37 @@ + + + + + + odoo.module.branch.form.inherit + odoo.module.branch + + + + + + + + + + + + odoo.module.branch.search.inherit + odoo.module.branch + + + + + + + + + + + + + diff --git a/odoo_project/views/odoo_project.xml b/odoo_project/views/odoo_project.xml new file mode 100644 index 00000000..f782bc9f --- /dev/null +++ b/odoo_project/views/odoo_project.xml @@ -0,0 +1,110 @@ + + + + + + odoo.project.form + odoo.project + +
+
+
+ + +
+ +
+
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + odoo.project.tree + odoo.project + + + + + + + + + + + odoo.project.search + odoo.project + search + + + + + + + + + + + + + + + Projects + ir.actions.act_window + odoo.project + + + + + +
diff --git a/odoo_project/wizards/__init__.py b/odoo_project/wizards/__init__.py new file mode 100644 index 00000000..7b4f991f --- /dev/null +++ b/odoo_project/wizards/__init__.py @@ -0,0 +1 @@ +from . import odoo_project_import_modules diff --git a/odoo_project/wizards/odoo_project_import_modules.py b/odoo_project/wizards/odoo_project_import_modules.py new file mode 100644 index 00000000..b68abbe3 --- /dev/null +++ b/odoo_project/wizards/odoo_project_import_modules.py @@ -0,0 +1,79 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import re + +from odoo import fields, models + + +class OdooProjectImportModules(models.TransientModel): + _name = "odoo.project.import.modules" + _description = "Import modules for an Odoo project" + + odoo_project_id = fields.Many2one( + comodel_name="odoo.project", + string="Project", + required=True, + ) + modules_list = fields.Text( + string="Modules List", + help=( + "Copy/paste your list of technical module names here.\n" + "They can be separated by spaces, tabulations, comma or any other " + "special characters." + ), + required=True, + ) + + def action_import(self): + """Import the modules for the given Odoo project.""" + self.ensure_one() + self.odoo_project_id.module_branch_ids = False + module_names = ( + re.split(r"\W+", self.modules_list) if self.modules_list else [] + ) + module_names = list(filter(None, module_names)) + module_branch_ids = [] + for module_name in module_names: + module = self._get_module(module_name) + module_branch = self._get_module_branch(module) + module_branch_ids.append(module_branch.id) + self.odoo_project_id.module_branch_ids = module_branch_ids + return True + + def _get_module(self, module_name): + """Return a `odoo.module` record. + + If it doesn't exist it'll be automatically created. + """ + module_model = self.env["odoo.module"] + module = module_model.search([("name", "=", module_name)]) + if not module: + module = module_model.sudo().create({"name": module_name}) + return module + + def _get_module_branch(self, module): + """Return a `odoo.module.branch` record for the project. + + If it doesn't exist it'll be automatically created. + """ + module_branch_model = self.env["odoo.module.branch"] + args = [ + ("module_id", "=", module.id), + ("branch_id", "=", self.odoo_project_id.odoo_version_id.id), + ] + module_branch = module_branch_model.search(args) + if not module_branch: + # Create the module to make it available for the project + branch = self.odoo_project_id.odoo_version_id + values = { + "module_id": module.id, + "branch_id": branch.id, + } + module_branch = module_branch_model.sudo().create(values) + if not module_branch.repository_branch_id: + # If the module hasn't been found in existing repositories content, + # it could be available somewhere on GitHub as a PR that could help + # to identity its repository + module_branch.with_delay().action_find_pr_url() + return module_branch diff --git a/odoo_project/wizards/odoo_project_import_modules.xml b/odoo_project/wizards/odoo_project_import_modules.xml new file mode 100644 index 00000000..60af61ef --- /dev/null +++ b/odoo_project/wizards/odoo_project_import_modules.xml @@ -0,0 +1,33 @@ + + + + + + odoo.project.import.modules.form + odoo.project.import.modules + +
+ + + + + + +
+
+
+
+
+ + + Import Project Modules + ir.actions.act_window + odoo.project.import.modules + form + new + + +
From f2bb32d443dca7dd6b593e7185e88bdec4300ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Fri, 3 Nov 2023 11:13:16 +0100 Subject: [PATCH 02/38] New data model 'odoo.project.module' This new data model is here to distinguish available upstream modules and installed modules in a project. It inherits from `odoo.module.branch` so it has access to all its data, but is linked to an `odoo.project` and has its own `installed_version` so it becomes easy to find modules that could be upgraded within a project. --- odoo_project/__manifest__.py | 2 + odoo_project/models/__init__.py | 1 + odoo_project/models/odoo_module_branch.py | 14 +++- odoo_project/models/odoo_project.py | 41 +++++----- odoo_project/models/odoo_project_module.py | 38 ++++++++++ odoo_project/security/ir.model.access.csv | 1 + odoo_project/views/menu.xml | 11 +++ odoo_project/views/odoo_module_branch.xml | 2 +- odoo_project/views/odoo_project.xml | 14 ++-- odoo_project/views/odoo_project_module.xml | 75 +++++++++++++++++++ .../wizards/odoo_project_import_modules.py | 58 ++++++++++---- 11 files changed, 215 insertions(+), 42 deletions(-) create mode 100644 odoo_project/models/odoo_project_module.py create mode 100644 odoo_project/views/menu.xml create mode 100644 odoo_project/views/odoo_project_module.xml diff --git a/odoo_project/__manifest__.py b/odoo_project/__manifest__.py index 64cd3410..89c6da71 100644 --- a/odoo_project/__manifest__.py +++ b/odoo_project/__manifest__.py @@ -9,8 +9,10 @@ "website": "https://github.com/OCA/TODO", "data": [ "security/ir.model.access.csv", + "views/menu.xml", "views/odoo_module_branch.xml", "views/odoo_project.xml", + "views/odoo_project_module.xml", "wizards/odoo_project_import_modules.xml", ], "installable": True, diff --git a/odoo_project/models/__init__.py b/odoo_project/models/__init__.py index 1a51d937..c59b850c 100644 --- a/odoo_project/models/__init__.py +++ b/odoo_project/models/__init__.py @@ -1,2 +1,3 @@ from . import odoo_project +from . import odoo_project_module from . import odoo_module_branch diff --git a/odoo_project/models/odoo_module_branch.py b/odoo_project/models/odoo_module_branch.py index d1adb2a4..d0232ef7 100644 --- a/odoo_project/models/odoo_module_branch.py +++ b/odoo_project/models/odoo_module_branch.py @@ -1,15 +1,27 @@ # Copyright 2023 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) -from odoo import fields, models +from odoo import api, fields, models class OdooModuleBranch(models.Model): _inherit = "odoo.module.branch" + odoo_project_module_ids = fields.One2many( + comodel_name="odoo.project.module", + inverse_name="module_branch_id", + string="Deployed Modules", + ) odoo_project_ids = fields.Many2many( comodel_name="odoo.project", relation="odoo_project_module_branch_rel", column1="module_branch_id", column2="odoo_project_id", string="Projects", + compute="_compute_module_ids", + store=True, ) + + @api.depends("odoo_project_module_ids.odoo_project_id") + def _compute_module_ids(self): + for rec in self: + rec.odoo_project_ids = rec.odoo_project_module_ids.odoo_project_id.ids diff --git a/odoo_project/models/odoo_project.py b/odoo_project/models/odoo_project.py index ebb142a8..596d0d18 100644 --- a/odoo_project/models/odoo_project.py +++ b/odoo_project/models/odoo_project.py @@ -24,11 +24,10 @@ class OdooProject(models.Model): ], required=True, ) - module_branch_ids = fields.Many2many( - comodel_name="odoo.module.branch", - relation="odoo_project_module_branch_rel", - column1="odoo_project_id", column2="module_branch_id", - string="Modules/Branch", + project_module_ids = fields.One2many( + comodel_name="odoo.project.module", + inverse_name="odoo_project_id", + string="Deployed Modules", ) module_ids = fields.Many2many( comodel_name="odoo.module", @@ -58,35 +57,39 @@ class OdooProject(models.Model): compute="_compute_unknown_module_ids", ) - @api.depends("module_branch_ids.module_id") + @api.depends("project_module_ids.module_id") def _compute_module_ids(self): for rec in self: - rec.module_ids = rec.module_branch_ids.module_id.ids + rec.module_ids = rec.project_module_ids.module_id.ids - @api.depends("module_branch_ids") + @api.depends("project_module_ids") def _compute_modules_count(self): for rec in self: - rec.modules_count = len(rec.module_branch_ids) + rec.modules_count = len(rec.project_module_ids) - @api.depends("repository_id.branch_ids.module_ids", "module_branch_ids") + @api.depends("repository_id.branch_ids.module_ids", "project_module_ids") def _compute_module_not_installed_ids(self): for rec in self: all_module_ids = set(rec.repository_id.branch_ids.module_ids.ids) - installed_module_ids = set(rec.module_branch_ids.ids) + installed_module_ids = set(rec.project_module_ids.ids) rec.module_not_installed_ids = list(all_module_ids - installed_module_ids) - @api.depends("module_branch_ids.pr_url") + @api.depends("project_module_ids.pr_url") def _compute_unmerged_module_ids(self): for rec in self: - rec.unmerged_module_ids = rec.module_branch_ids.filtered( - lambda module: module.pr_url + rec.unmerged_module_ids = ( + rec.project_module_ids.module_branch_id.filtered( + lambda module: module.pr_url + ) ) - @api.depends("module_branch_ids.repository_id") + @api.depends("project_module_ids.repository_id") def _compute_unknown_module_ids(self): for rec in self: - rec.unknown_module_ids = rec.module_branch_ids.filtered( - lambda module: not module.repository_id + rec.unknown_module_ids = ( + rec.project_module_ids.module_branch_id.filtered( + lambda module: not module.repository_id + ) ) def open_import_modules(self): @@ -105,12 +108,12 @@ def open_import_modules(self): def open_modules(self): self.ensure_one() action = self.env["ir.actions.actions"]._for_xml_id( - "odoo_repository.odoo_module_branch_action" + "odoo_project.odoo_project_module_action" ) ctx = action.get("context", {}) if isinstance(ctx, str): ctx = ast.literal_eval(ctx) - action["domain"] = [("id", "in", self.module_branch_ids.ids)] + action["domain"] = [("id", "in", self.project_module_ids.ids)] ctx["search_default_group_by_org_id"] = 1 ctx["search_default_group_by_repository_id"] = 2 action["context"] = ctx diff --git a/odoo_project/models/odoo_project_module.py b/odoo_project/models/odoo_project_module.py new file mode 100644 index 00000000..031ceac4 --- /dev/null +++ b/odoo_project/models/odoo_project_module.py @@ -0,0 +1,38 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models +from odoo.tools.parse_version import parse_version as v + + +class OdooProjectModule(models.Model): + _name = "odoo.project.module" + _inherits = {"odoo.module.branch": "module_branch_id"} + _description = "Odoo Project Module" + _order = "name" + + odoo_project_id = fields.Many2one( + comodel_name="odoo.project", + ondelete="cascade", + string="Project", + ) + module_branch_id = fields.Many2one( + comodel_name="odoo.module.branch", + ondelete="set null", + string="Upstream Module", + required=True, + ) + installed_version = fields.Char() + to_upgrade = fields.Boolean( + string="To Upgrade", + compute="_compute_to_upgrade", + store=True, + ) + + @api.depends("version", "installed_version") + def _compute_to_upgrade(self): + for rec in self: + rec.to_upgrade = False + installed_version = rec.installed_version or rec.version + if installed_version and rec.version: + rec.to_upgrade = v(installed_version) < v(rec.version) diff --git a/odoo_project/security/ir.model.access.csv b/odoo_project/security/ir.model.access.csv index 6cbba4ce..209d57da 100644 --- a/odoo_project/security/ir.model.access.csv +++ b/odoo_project/security/ir.model.access.csv @@ -1,4 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_odoo_project_user,odoo_project_user,model_odoo_project,base.group_user,1,1,1,0 access_odoo_project_manager_manager,odoo_project_manager,model_odoo_project,base.group_system,1,1,1,1 +access_odoo_project_module_user,odoo_project_module_user,model_odoo_project_module,base.group_user,1,0,0,0 access_odoo_project_import_modules_user,odoo_project_import_modules_user,model_odoo_project_import_modules,base.group_user,1,1,1,1 diff --git a/odoo_project/views/menu.xml b/odoo_project/views/menu.xml new file mode 100644 index 00000000..07e8d4bb --- /dev/null +++ b/odoo_project/views/menu.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/odoo_project/views/odoo_module_branch.xml b/odoo_project/views/odoo_module_branch.xml index 84147403..0bbf4cf5 100644 --- a/odoo_project/views/odoo_module_branch.xml +++ b/odoo_project/views/odoo_module_branch.xml @@ -23,7 +23,7 @@ + domain="[('odoo_project_ids', '!=', False)]"/> diff --git a/odoo_project/views/odoo_project.xml b/odoo_project/views/odoo_project.xml index f782bc9f..acbbbe8f 100644 --- a/odoo_project/views/odoo_project.xml +++ b/odoo_project/views/odoo_project.xml @@ -13,7 +13,7 @@ string="Import modules" class="btn-primary"/> - +
- + @@ -70,7 +70,7 @@ - +
@@ -83,13 +83,12 @@ + -
@@ -103,8 +102,7 @@ + parent="odoo_project_main_menu" + action="odoo_project_action"/> diff --git a/odoo_project/views/odoo_project_module.xml b/odoo_project/views/odoo_project_module.xml new file mode 100644 index 00000000..1bc5483a --- /dev/null +++ b/odoo_project/views/odoo_project_module.xml @@ -0,0 +1,75 @@ + + + + + + odoo.project.module.form.inherit + odoo.project.module + + primary + + + + + + Last Version + + + + + + odoo.project.module.tree.inherit + odoo.project.module + + primary + + + to_upgrade + pr_url + + + hide + + + + + + + Last Version + + + + show + + + + + + odoo.project.module.search.inherit + odoo.project.module + + primary + + + + + + + + + + + + Installed Modules + ir.actions.act_window + odoo.project.module + + + + + + diff --git a/odoo_project/wizards/odoo_project_import_modules.py b/odoo_project/wizards/odoo_project_import_modules.py index b68abbe3..a3cba9b7 100644 --- a/odoo_project/wizards/odoo_project_import_modules.py +++ b/odoo_project/wizards/odoo_project_import_modules.py @@ -19,8 +19,9 @@ class OdooProjectImportModules(models.TransientModel): string="Modules List", help=( "Copy/paste your list of technical module names here.\n" - "They can be separated by spaces, tabulations, comma or any other " - "special characters." + "One module per line, with an optional version number placed " + "after the module name separated by any special character " + "(space, tabulation, comma...)." ), required=True, ) @@ -28,17 +29,25 @@ class OdooProjectImportModules(models.TransientModel): def action_import(self): """Import the modules for the given Odoo project.""" self.ensure_one() - self.odoo_project_id.module_branch_ids = False - module_names = ( - re.split(r"\W+", self.modules_list) if self.modules_list else [] - ) - module_names = list(filter(None, module_names)) - module_branch_ids = [] - for module_name in module_names: + self.odoo_project_id.sudo().project_module_ids = False + module_lines = list(filter(None, self.modules_list.split("\n"))) + # module_names = ( + # re.split(r"\W+", self.modules_list) if self.modules_list else [] + # ) + # module_names = list(filter(None, module_names)) + project_module_ids = [] + for line in module_lines: + data = re.split(r"\W+", line, maxsplit=1) + if len(data) > 1: + module_name, version = data + else: + module_name, version = data[0], False + # for module_name in module_names: module = self._get_module(module_name) module_branch = self._get_module_branch(module) - module_branch_ids.append(module_branch.id) - self.odoo_project_id.module_branch_ids = module_branch_ids + project_module = self._get_project_module(module_branch, version) + project_module_ids.append(project_module.id) + self.odoo_project_id.sudo().project_module_ids = project_module_ids return True def _get_module(self, module_name): @@ -53,7 +62,7 @@ def _get_module(self, module_name): return module def _get_module_branch(self, module): - """Return a `odoo.module.branch` record for the project. + """Return a `odoo.module.branch` record. If it doesn't exist it'll be automatically created. """ @@ -64,7 +73,7 @@ def _get_module_branch(self, module): ] module_branch = module_branch_model.search(args) if not module_branch: - # Create the module to make it available for the project + # Create the module branch = self.odoo_project_id.odoo_version_id values = { "module_id": module.id, @@ -77,3 +86,26 @@ def _get_module_branch(self, module): # to identity its repository module_branch.with_delay().action_find_pr_url() return module_branch + + def _get_project_module(self, module_branch, version): + """Return a `odoo.project.module` record for the project. + + If it doesn't exist it'll be automatically created. + """ + project_module_model = self.env["odoo.project.module"] + args = [ + ("module_branch_id", "=", module_branch.id), + ("odoo_project_id", "=", self.odoo_project_id.id), + ] + project_module = project_module_model.search(args) + values = { + "module_branch_id": module_branch.id, + "odoo_project_id": self.odoo_project_id.id, + "installed_version": version, + } + if project_module: + project_module.sudo().write(values) + else: + # Create the module to make it available for the project + project_module = project_module_model.sudo().create(values) + return project_module From 1c06ae2e5b2fb30b763ef462bd178fdc28ed7436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Fri, 3 Nov 2023 11:36:16 +0100 Subject: [PATCH 03/38] odoo_project: fix computation of uninstalled modules --- odoo_project/models/odoo_project.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/odoo_project/models/odoo_project.py b/odoo_project/models/odoo_project.py index 596d0d18..fd473789 100644 --- a/odoo_project/models/odoo_project.py +++ b/odoo_project/models/odoo_project.py @@ -67,11 +67,11 @@ def _compute_modules_count(self): for rec in self: rec.modules_count = len(rec.project_module_ids) - @api.depends("repository_id.branch_ids.module_ids", "project_module_ids") + @api.depends("repository_id.branch_ids.module_ids", "project_module_ids.module_branch_id") def _compute_module_not_installed_ids(self): for rec in self: all_module_ids = set(rec.repository_id.branch_ids.module_ids.ids) - installed_module_ids = set(rec.project_module_ids.ids) + installed_module_ids = set(rec.project_module_ids.module_branch_id.ids) rec.module_not_installed_ids = list(all_module_ids - installed_module_ids) @api.depends("project_module_ids.pr_url") From 11513936fd0b58d064c2c9ad3b87f1d65a090593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Fri, 3 Nov 2023 12:13:31 +0100 Subject: [PATCH 04/38] Apply pre-commit --- odoo_project/README.rst | 61 +++ odoo_project/__manifest__.py | 2 +- odoo_project/models/odoo_module_branch.py | 3 +- odoo_project/models/odoo_project.py | 19 +- odoo_project/models/odoo_project_module.py | 1 - odoo_project/readme/CONTRIBUTORS.rst | 2 + odoo_project/readme/DESCRIPTION.rst | 1 + odoo_project/static/description/index.html | 417 ++++++++++++++++++ odoo_project/views/menu.xml | 12 +- odoo_project/views/odoo_module_branch.xml | 29 +- odoo_project/views/odoo_project.xml | 82 ++-- odoo_project/views/odoo_project_module.xml | 35 +- .../wizards/odoo_project_import_modules.py | 3 +- .../wizards/odoo_project_import_modules.xml | 15 +- 14 files changed, 601 insertions(+), 81 deletions(-) create mode 100644 odoo_project/README.rst create mode 100644 odoo_project/readme/CONTRIBUTORS.rst create mode 100644 odoo_project/readme/DESCRIPTION.rst create mode 100644 odoo_project/static/description/index.html diff --git a/odoo_project/README.rst b/odoo_project/README.rst new file mode 100644 index 00000000..e948e679 --- /dev/null +++ b/odoo_project/README.rst @@ -0,0 +1,61 @@ +============ +Odoo Project +============ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:dc350fed0f23a205eb546f1673d2c5e99b4f6cdb5be6d6ab76a010e2517e203b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-camptocamp%2Fodoo--repository-lightgray.png?logo=github + :target: https://github.com/camptocamp/odoo-repository/tree/16.0/odoo_project + :alt: camptocamp/odoo-repository + +|badge1| |badge2| |badge3| + +This module allows to declare your Odoo projects and analyze their code bases. + +**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 +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Camptocamp + * Sébastien Alix + +Maintainers +~~~~~~~~~~~ + +This module is part of the `camptocamp/odoo-repository `_ project on GitHub. + +You are welcome to contribute. diff --git a/odoo_project/__manifest__.py b/odoo_project/__manifest__.py index 89c6da71..4ff208db 100644 --- a/odoo_project/__manifest__.py +++ b/odoo_project/__manifest__.py @@ -6,7 +6,7 @@ "version": "16.0.1.0.0", "category": "Tools", "author": "Camptocamp, Odoo Community Association (OCA)", - "website": "https://github.com/OCA/TODO", + "website": "https://github.com/camptocamp/odoo-repository", "data": [ "security/ir.model.access.csv", "views/menu.xml", diff --git a/odoo_project/models/odoo_module_branch.py b/odoo_project/models/odoo_module_branch.py index d0232ef7..6f190121 100644 --- a/odoo_project/models/odoo_module_branch.py +++ b/odoo_project/models/odoo_module_branch.py @@ -15,7 +15,8 @@ class OdooModuleBranch(models.Model): odoo_project_ids = fields.Many2many( comodel_name="odoo.project", relation="odoo_project_module_branch_rel", - column1="module_branch_id", column2="odoo_project_id", + column1="module_branch_id", + column2="odoo_project_id", string="Projects", compute="_compute_module_ids", store=True, diff --git a/odoo_project/models/odoo_project.py b/odoo_project/models/odoo_project.py index fd473789..7bf51e9b 100644 --- a/odoo_project/models/odoo_project.py +++ b/odoo_project/models/odoo_project.py @@ -32,7 +32,8 @@ class OdooProject(models.Model): module_ids = fields.Many2many( comodel_name="odoo.module", relation="odoo_project_module_rel", - column1="odoo_project_id", column2="module_id", + column1="odoo_project_id", + column2="module_id", string="Modules", compute="_compute_module_ids", store=True, @@ -67,7 +68,9 @@ def _compute_modules_count(self): for rec in self: rec.modules_count = len(rec.project_module_ids) - @api.depends("repository_id.branch_ids.module_ids", "project_module_ids.module_branch_id") + @api.depends( + "repository_id.branch_ids.module_ids", "project_module_ids.module_branch_id" + ) def _compute_module_not_installed_ids(self): for rec in self: all_module_ids = set(rec.repository_id.branch_ids.module_ids.ids) @@ -77,19 +80,15 @@ def _compute_module_not_installed_ids(self): @api.depends("project_module_ids.pr_url") def _compute_unmerged_module_ids(self): for rec in self: - rec.unmerged_module_ids = ( - rec.project_module_ids.module_branch_id.filtered( - lambda module: module.pr_url - ) + rec.unmerged_module_ids = rec.project_module_ids.module_branch_id.filtered( + lambda module: module.pr_url ) @api.depends("project_module_ids.repository_id") def _compute_unknown_module_ids(self): for rec in self: - rec.unknown_module_ids = ( - rec.project_module_ids.module_branch_id.filtered( - lambda module: not module.repository_id - ) + rec.unknown_module_ids = rec.project_module_ids.module_branch_id.filtered( + lambda module: not module.repository_id ) def open_import_modules(self): diff --git a/odoo_project/models/odoo_project_module.py b/odoo_project/models/odoo_project_module.py index 031ceac4..2beb93c3 100644 --- a/odoo_project/models/odoo_project_module.py +++ b/odoo_project/models/odoo_project_module.py @@ -24,7 +24,6 @@ class OdooProjectModule(models.Model): ) installed_version = fields.Char() to_upgrade = fields.Boolean( - string="To Upgrade", compute="_compute_to_upgrade", store=True, ) diff --git a/odoo_project/readme/CONTRIBUTORS.rst b/odoo_project/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..a0c91e35 --- /dev/null +++ b/odoo_project/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Camptocamp + * Sébastien Alix diff --git a/odoo_project/readme/DESCRIPTION.rst b/odoo_project/readme/DESCRIPTION.rst new file mode 100644 index 00000000..a86173ee --- /dev/null +++ b/odoo_project/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module allows to declare your Odoo projects and analyze their code bases. diff --git a/odoo_project/static/description/index.html b/odoo_project/static/description/index.html new file mode 100644 index 00000000..747faf31 --- /dev/null +++ b/odoo_project/static/description/index.html @@ -0,0 +1,417 @@ + + + + + + +Odoo Project + + + +
+

Odoo Project

+ + +

Beta License: AGPL-3 camptocamp/odoo-repository

+

This module allows to declare your Odoo projects and analyze their code bases.

+

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

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the camptocamp/odoo-repository project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/odoo_project/views/menu.xml b/odoo_project/views/menu.xml index 07e8d4bb..60def18d 100644 --- a/odoo_project/views/menu.xml +++ b/odoo_project/views/menu.xml @@ -1,11 +1,13 @@ - + - + diff --git a/odoo_project/views/odoo_module_branch.xml b/odoo_project/views/odoo_module_branch.xml index 0bbf4cf5..570b8254 100644 --- a/odoo_project/views/odoo_module_branch.xml +++ b/odoo_project/views/odoo_module_branch.xml @@ -1,4 +1,4 @@ - + @@ -6,11 +6,11 @@ odoo.module.branch.form.inherit odoo.module.branch - + - + @@ -19,17 +19,26 @@ odoo.module.branch.search.inherit odoo.module.branch - + - - + + - + diff --git a/odoo_project/views/odoo_project.xml b/odoo_project/views/odoo_project.xml index acbbbe8f..4c28bf52 100644 --- a/odoo_project/views/odoo_project.xml +++ b/odoo_project/views/odoo_project.xml @@ -1,4 +1,4 @@ - + @@ -9,33 +9,45 @@
-
- +
-
-

+

- + - - - + + + @@ -43,17 +55,17 @@ - - - + + + - - + + @@ -68,9 +80,9 @@ odoo.project - - - + + + @@ -81,14 +93,20 @@ search - - - + + + - - + + @@ -98,11 +116,13 @@ Projects ir.actions.act_window odoo.project - + - + diff --git a/odoo_project/views/odoo_project_module.xml b/odoo_project/views/odoo_project_module.xml index 1bc5483a..6feac27a 100644 --- a/odoo_project/views/odoo_project_module.xml +++ b/odoo_project/views/odoo_project_module.xml @@ -1,4 +1,4 @@ - + @@ -6,11 +6,11 @@ odoo.project.module.form.inherit odoo.project.module - + primary - + Last Version @@ -21,7 +21,7 @@ odoo.project.module.tree.inherit odoo.project.module - + primary @@ -32,12 +32,12 @@ hide - - + + Last Version - + show @@ -48,15 +48,18 @@ odoo.project.module.search.inherit odoo.project.module - + primary - + - + @@ -65,11 +68,13 @@ Installed Modules ir.actions.act_window odoo.project.module - + - + diff --git a/odoo_project/wizards/odoo_project_import_modules.py b/odoo_project/wizards/odoo_project_import_modules.py index a3cba9b7..d7de8ac6 100644 --- a/odoo_project/wizards/odoo_project_import_modules.py +++ b/odoo_project/wizards/odoo_project_import_modules.py @@ -16,7 +16,6 @@ class OdooProjectImportModules(models.TransientModel): required=True, ) modules_list = fields.Text( - string="Modules List", help=( "Copy/paste your list of technical module names here.\n" "One module per line, with an optional version number placed " @@ -42,7 +41,7 @@ def action_import(self): module_name, version = data else: module_name, version = data[0], False - # for module_name in module_names: + # for module_name in module_names: module = self._get_module(module_name) module_branch = self._get_module_branch(module) project_module = self._get_project_module(module_branch, version) diff --git a/odoo_project/wizards/odoo_project_import_modules.xml b/odoo_project/wizards/odoo_project_import_modules.xml index 60af61ef..34a3da32 100644 --- a/odoo_project/wizards/odoo_project_import_modules.xml +++ b/odoo_project/wizards/odoo_project_import_modules.xml @@ -1,4 +1,4 @@ - + @@ -10,13 +10,18 @@ - - + +
-
From eb3f84661c68417970672429e6c03a8f9bc97c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Fri, 3 Nov 2023 12:43:00 +0100 Subject: [PATCH 05/38] Remove dead code --- odoo_project/wizards/odoo_project_import_modules.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/odoo_project/wizards/odoo_project_import_modules.py b/odoo_project/wizards/odoo_project_import_modules.py index d7de8ac6..2bf1adb0 100644 --- a/odoo_project/wizards/odoo_project_import_modules.py +++ b/odoo_project/wizards/odoo_project_import_modules.py @@ -30,10 +30,6 @@ def action_import(self): self.ensure_one() self.odoo_project_id.sudo().project_module_ids = False module_lines = list(filter(None, self.modules_list.split("\n"))) - # module_names = ( - # re.split(r"\W+", self.modules_list) if self.modules_list else [] - # ) - # module_names = list(filter(None, module_names)) project_module_ids = [] for line in module_lines: data = re.split(r"\W+", line, maxsplit=1) From ff69744ca14c40d759b18a1577c0d6d6c1d1607e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Sat, 4 Nov 2023 15:22:41 +0100 Subject: [PATCH 06/38] odoo_project: enable chatter on 'odoo.project' --- odoo_project/models/odoo_project.py | 1 + odoo_project/views/odoo_project.xml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/odoo_project/models/odoo_project.py b/odoo_project/models/odoo_project.py index 7bf51e9b..e7d816e0 100644 --- a/odoo_project/models/odoo_project.py +++ b/odoo_project/models/odoo_project.py @@ -9,6 +9,7 @@ class OdooProject(models.Model): _name = "odoo.project" _inherits = {"odoo.repository": "repository_id"} + _inherit = "mail.thread" _description = "Odoo Project" _order = "name" diff --git a/odoo_project/views/odoo_project.xml b/odoo_project/views/odoo_project.xml index 4c28bf52..e5da2fd4 100644 --- a/odoo_project/views/odoo_project.xml +++ b/odoo_project/views/odoo_project.xml @@ -71,6 +71,10 @@
+
+ + +
From b4cd7eb01fd9ede9136fe502fd3a9aa46beacc94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Sat, 4 Nov 2023 16:56:06 +0100 Subject: [PATCH 07/38] odoo_project: fix warning regarding '_inherits' --- odoo_project/models/odoo_project_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoo_project/models/odoo_project_module.py b/odoo_project/models/odoo_project_module.py index 2beb93c3..5868eb4c 100644 --- a/odoo_project/models/odoo_project_module.py +++ b/odoo_project/models/odoo_project_module.py @@ -18,7 +18,7 @@ class OdooProjectModule(models.Model): ) module_branch_id = fields.Many2one( comodel_name="odoo.module.branch", - ondelete="set null", + ondelete="cascade", string="Upstream Module", required=True, ) From cbb6564ea437f191c8dec0b69a548cd8b5a2950e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Fri, 24 Nov 2023 12:03:14 +0100 Subject: [PATCH 08/38] odoo_project: global 'Installed modules' menu opening pivot view --- odoo_project/models/odoo_project.py | 2 +- odoo_project/views/odoo_project_module.xml | 36 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/odoo_project/models/odoo_project.py b/odoo_project/models/odoo_project.py index e7d816e0..a67625fc 100644 --- a/odoo_project/models/odoo_project.py +++ b/odoo_project/models/odoo_project.py @@ -108,7 +108,7 @@ def open_import_modules(self): def open_modules(self): self.ensure_one() action = self.env["ir.actions.actions"]._for_xml_id( - "odoo_project.odoo_project_module_action" + "odoo_project.odoo_project_module_for_project_action" ) ctx = action.get("context", {}) if isinstance(ctx, str): diff --git a/odoo_project/views/odoo_project_module.xml b/odoo_project/views/odoo_project_module.xml index 6feac27a..70004833 100644 --- a/odoo_project/views/odoo_project_module.xml +++ b/odoo_project/views/odoo_project_module.xml @@ -28,10 +28,14 @@ to_upgrade pr_url + + + hide + @@ -61,6 +65,22 @@ domain="[('to_upgrade', '=', True)]" />
+ + + {'group_by': 'odoo_project_id'} + +
+ + + + odoo.project.module.pivot + odoo.project.module + + + + + + @@ -68,6 +88,22 @@ Installed Modules ir.actions.act_window odoo.project.module + tree,form,pivot + + + + + + pivot + + + + + + Installed Modules + ir.actions.act_window + odoo.project.module + tree,form,pivot From c2155f25e52baacadd8db59f343e89640a992564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Fri, 1 Dec 2023 09:41:17 +0100 Subject: [PATCH 09/38] odoo_project: add migration scripts indicator New boolean field on installed modules to tell if some migration scripts have to be played between the installed version and the latest version available. --- odoo_project/models/odoo_project_module.py | 43 ++++++++++++++++++++++ odoo_project/views/odoo_project_module.xml | 14 ++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/odoo_project/models/odoo_project_module.py b/odoo_project/models/odoo_project_module.py index 5868eb4c..71b9f3b0 100644 --- a/odoo_project/models/odoo_project_module.py +++ b/odoo_project/models/odoo_project_module.py @@ -27,6 +27,11 @@ class OdooProjectModule(models.Model): compute="_compute_to_upgrade", store=True, ) + migration_scripts = fields.Boolean( + compute="_compute_migration_scripts", + store=True, + help="Available migration scripts between installed and last version.", + ) @api.depends("version", "installed_version") def _compute_to_upgrade(self): @@ -35,3 +40,41 @@ def _compute_to_upgrade(self): installed_version = rec.installed_version or rec.version if installed_version and rec.version: rec.to_upgrade = v(installed_version) < v(rec.version) + + @api.depends( + "to_upgrade", + "installed_version", + "version_ids.name", + "version_ids.has_migration_script", + ) + def _compute_migration_scripts(self): + for rec in self: + rec.migration_scripts = False + if not rec.to_upgrade: + continue + installed_version = rec._get_installed_version() + versions_with_mig_script = rec.version_ids.filtered( + lambda v: ( + v.sequence > installed_version.sequence and v.has_migration_script + ) + ) + rec.migration_scripts = bool(versions_with_mig_script) + + def _get_installed_version(self): + self.ensure_one() + installed_version = self.version_ids.browse() + if not self.installed_version: + return installed_version + # Installed version could not be available in inventoried versions + # if it is coming from a pending-merge. In such case we take the last + # matching version as the installed one. + # - Available versions upstream = "14.0.2.0.0" & "14.0.2.1.0" + # - Installed version = "14.0.2.0.1" (in a pending-merge) + # - Computed installed version = "14.0.2.0.0" + inst_ver = [int(n) for n in self.installed_version.split(".")] + for version in self.version_ids.sorted("sequence"): + ver = [int(n) for n in version.name.split(".")] + if ver > inst_ver: + break + installed_version = version + return installed_version diff --git a/odoo_project/views/odoo_project_module.xml b/odoo_project/views/odoo_project_module.xml index 70004833..1d0da748 100644 --- a/odoo_project/views/odoo_project_module.xml +++ b/odoo_project/views/odoo_project_module.xml @@ -12,6 +12,9 @@ + + + Last Version @@ -29,16 +32,18 @@ pr_url - + hide - + + + Last Version @@ -64,6 +69,11 @@ string="To Upgrade" domain="[('to_upgrade', '=', True)]" /> + From 5a886518437b94a97f50cc5060ccfd1e9d095c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Fri, 19 Apr 2024 18:24:43 +0200 Subject: [PATCH 10/38] odoo_project: compute nearest installed version --- odoo_project/models/odoo_project_module.py | 52 +++++++++++++--------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/odoo_project/models/odoo_project_module.py b/odoo_project/models/odoo_project_module.py index 71b9f3b0..1c5af9cd 100644 --- a/odoo_project/models/odoo_project_module.py +++ b/odoo_project/models/odoo_project_module.py @@ -22,7 +22,17 @@ class OdooProjectModule(models.Model): string="Upstream Module", required=True, ) - installed_version = fields.Char() + installed_version = fields.Char(help="Installed version in project database.") + installed_version_id = fields.Many2one( + comodel_name="odoo.module.branch.version", + string="Nearest installed version", + help=( + "If the real installed version is not available in the history of " + "versions (could come from a pending merge not scanned), this field " + "computes the nearest version available." + ), + compute="_compute_installed_version_id", + ) to_upgrade = fields.Boolean( compute="_compute_to_upgrade", store=True, @@ -33,6 +43,25 @@ class OdooProjectModule(models.Model): help="Available migration scripts between installed and last version.", ) + @api.depends("installed_version") + def _compute_installed_version_id(self): + for rec in self: + rec.installed_version_id = rec.version_ids.browse() + if not rec.installed_version: + continue + # Installed version could not be available in inventoried versions + # if it is coming from a pending-merge. In such case we take the + # nearest version matching the installed one. + # - Available versions upstream = "14.0.2.0.0" & "14.0.2.1.0" + # - Installed version = "14.0.2.0.1" (in a pending-merge) + # - Computed installed version = "14.0.2.0.0" + inst_ver = [int(n) for n in rec.installed_version.split(".")] + for version in rec.version_ids.sorted("sequence"): + ver = [int(n) for n in version.name.split(".")] + if ver > inst_ver: + break + rec.installed_version_id = version + @api.depends("version", "installed_version") def _compute_to_upgrade(self): for rec in self: @@ -52,29 +81,10 @@ def _compute_migration_scripts(self): rec.migration_scripts = False if not rec.to_upgrade: continue - installed_version = rec._get_installed_version() + installed_version = rec.installed_version_id versions_with_mig_script = rec.version_ids.filtered( lambda v: ( v.sequence > installed_version.sequence and v.has_migration_script ) ) rec.migration_scripts = bool(versions_with_mig_script) - - def _get_installed_version(self): - self.ensure_one() - installed_version = self.version_ids.browse() - if not self.installed_version: - return installed_version - # Installed version could not be available in inventoried versions - # if it is coming from a pending-merge. In such case we take the last - # matching version as the installed one. - # - Available versions upstream = "14.0.2.0.0" & "14.0.2.1.0" - # - Installed version = "14.0.2.0.1" (in a pending-merge) - # - Computed installed version = "14.0.2.0.0" - inst_ver = [int(n) for n in self.installed_version.split(".")] - for version in self.version_ids.sorted("sequence"): - ver = [int(n) for n in version.name.split(".")] - if ver > inst_ver: - break - installed_version = version - return installed_version From a62a5ab585365bc053fc817db527c4d16b27e8cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Fri, 19 Apr 2024 18:49:42 +0200 Subject: [PATCH 11/38] odoo_project: add a button to recheck unknown modules --- odoo_project/models/odoo_project.py | 5 +++++ odoo_project/views/odoo_project.xml | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/odoo_project/models/odoo_project.py b/odoo_project/models/odoo_project.py index a67625fc..5b4a88ad 100644 --- a/odoo_project/models/odoo_project.py +++ b/odoo_project/models/odoo_project.py @@ -118,3 +118,8 @@ def open_modules(self): ctx["search_default_group_by_repository_id"] = 2 action["context"] = ctx return action + + def action_find_unknown_modules(self): + """Try to locate unknown modules.""" + for module in self.unknown_module_ids: + module.action_find_pr_url() diff --git a/odoo_project/views/odoo_project.xml b/odoo_project/views/odoo_project.xml index e5da2fd4..4cd80d9c 100644 --- a/odoo_project/views/odoo_project.xml +++ b/odoo_project/views/odoo_project.xml @@ -68,6 +68,12 @@ + + + + + + From 1cfaa68e95bf833ba632e20797f98875e27a276a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Fri, 18 Oct 2024 14:35:41 +0200 Subject: [PATCH 22/38] [REF] Allow same module name across different repositories This change allows to get a module `x` present in different repositories (unicity constraint on `odoo.module.branch` has changed accordingly). This is useful in case two projects have `x` installed but the code of these modules (that can be different) are hosted in their own project repository. Therefore, the scan of repositories and project modules import have been adapted to fullfil this new possibility. Features: - allows multiple instances of `` records sharing the same module technical name and branch but belonging to different repositories, - repositories have now a new `specific` flag, propagated to their modules.. By default they host generic modules, but project repositories will host specific modules that cannot be shared to other projects, - a generic module cannot depend on a specific module - when importing modules in a project, specific modules of other projects cannot be mapped, - once a specific module is scanned in a project repository, relevant orphaned modules installed in a project will be re-mapped to this newly created specific module, - detect PR only on generic modules (unmerged/pending modules are generic only, specific modules have to be found in project repositories). --- odoo_project/data/queue_job.xml | 16 ++ odoo_project/models/odoo_module_branch.py | 29 +++ odoo_project/models/odoo_project.py | 1 + odoo_project/models/odoo_project_module.py | 18 ++ odoo_project/tests/__init__.py | 1 + odoo_project/tests/common.py | 29 +++ odoo_project/tests/test_import_modules.py | 165 ++++++++++++++++++ .../wizards/odoo_project_import_modules.py | 26 ++- 8 files changed, 269 insertions(+), 16 deletions(-) create mode 100644 odoo_project/data/queue_job.xml create mode 100644 odoo_project/tests/__init__.py create mode 100644 odoo_project/tests/common.py create mode 100644 odoo_project/tests/test_import_modules.py diff --git a/odoo_project/data/queue_job.xml b/odoo_project/data/queue_job.xml new file mode 100644 index 00000000..641669a7 --- /dev/null +++ b/odoo_project/data/queue_job.xml @@ -0,0 +1,16 @@ + + + + + + + _remap_to_specific_module + + + + + diff --git a/odoo_project/models/odoo_module_branch.py b/odoo_project/models/odoo_module_branch.py index 6f190121..66969759 100644 --- a/odoo_project/models/odoo_module_branch.py +++ b/odoo_project/models/odoo_module_branch.py @@ -26,3 +26,32 @@ class OdooModuleBranch(models.Model): def _compute_module_ids(self): for rec in self: rec.odoo_project_ids = rec.odoo_project_module_ids.odoo_project_id.ids + + def _filter_module_to_update(self, repo_branch, module_branch): + # A module found in a specific repository cannot be linked to an orphaned + # module that is already listed/installed in different projects (chances + # are these projects refer to a generic version of the module). + # For such specific module we want to create a dedicated 'odoo.module.branch'. + if ( + repo_branch.repository_id.specific + # Orphaned module + and module_branch + and not module_branch.repository_id + # Installed in multiple projects + # NOTE: we could have multiple project instances using the same + # repository, so we check the underlying repo instead + and len(module_branch.odoo_project_ids.repository_id) > 1 + ): + repo_projects = repo_branch.repository_id.project_ids + if repo_projects: + # Corner case: if the current project(s) are referring to this + # orphaned module, remove them as they will now have a dedicated + # specific module created. + # Spawn a job to re-map project modules to the new (not yet created) + # specific module. + project_modules = module_branch.odoo_project_module_ids.filtered( + lambda o: o.odoo_project_id in repo_projects + ) + project_modules.with_delay()._remap_to_specific_module() + return False + return module_branch diff --git a/odoo_project/models/odoo_project.py b/odoo_project/models/odoo_project.py index c0adb013..074197f5 100644 --- a/odoo_project/models/odoo_project.py +++ b/odoo_project/models/odoo_project.py @@ -20,6 +20,7 @@ class OdooProject(models.Model): string="Repository", domain=[ ("clone_branch_id", "!=", False), + ("specific", "=", True), ("odoo_version_id", "!=", False), ], help=( diff --git a/odoo_project/models/odoo_project_module.py b/odoo_project/models/odoo_project_module.py index 988f168a..c4015aaf 100644 --- a/odoo_project/models/odoo_project_module.py +++ b/odoo_project/models/odoo_project_module.py @@ -129,3 +129,21 @@ def open_recursive_dependencies(self): ) action["domain"] = [("id", "in", project_dependencies.ids)] return action + + def _remap_to_specific_module(self): + """Re-map orphaned project modules. + + As soon as an orphaned module has been scanned as a specific module + of a project repository, all project modules that were targeting this + orphaned module will inherit from this newly specific module instead. + """ + for rec in self: + if not rec.odoo_project_id.repository_id: + continue + specific_module_branch = self.env["odoo.module.branch"]._get_module_branch( + rec.branch_id, + rec.module_id, + repo=rec.odoo_project_id.repository_id, + ) + if specific_module_branch: + rec.write({"module_branch_id": specific_module_branch.id}) diff --git a/odoo_project/tests/__init__.py b/odoo_project/tests/__init__.py new file mode 100644 index 00000000..9c82a9f7 --- /dev/null +++ b/odoo_project/tests/__init__.py @@ -0,0 +1 @@ +from . import test_import_modules diff --git a/odoo_project/tests/common.py b/odoo_project/tests/common.py new file mode 100644 index 00000000..2506670c --- /dev/null +++ b/odoo_project/tests/common.py @@ -0,0 +1,29 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.tests.common import Form + +from odoo.addons.odoo_repository.tests import common + + +class Common(common.Common): + def setUp(self): + super().setUp() + self.project = self.env["odoo.project"].create( + { + "name": "TEST", + "odoo_version_id": self.branch.id, + } + ) + self.wiz_model = self.env["odoo.project.import.modules"] + + @classmethod + def _run_import_modules(cls, project, modules_list_text, **kwargs): + wiz_model = cls.env["odoo.project.import.modules"].with_context( + default_odoo_project_id=project.id + ) + with Form(wiz_model) as form: + form.modules_list = modules_list_text + wiz = form.save() + wiz.action_import() + return wiz diff --git a/odoo_project/tests/test_import_modules.py b/odoo_project/tests/test_import_modules.py new file mode 100644 index 00000000..390fdcd4 --- /dev/null +++ b/odoo_project/tests/test_import_modules.py @@ -0,0 +1,165 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from .common import Common + + +class TestImportModules(Common): + def test_import_modules_names(self): + mod1 = "test1" + mod2 = "test2" + # Ensure these modules don't exist + existing_mods = self.module_branch_model.search( + [("module_name", "in", [mod1, mod2])] + ) + self.assertFalse(existing_mods) + # Import them through the wizard + modules_list_text = f"{mod1}\n{mod2}" + self._run_import_modules(self.project, modules_list_text) + # They now exists as orphaned modules and are linked to the project + existing_mods = self.module_branch_model.search( + [ + ("module_name", "in", [mod1, mod2]), + ("repository_id", "=", False), + ("odoo_project_ids", "=", self.project.id), + ] + ) + self.assertEqual(len(existing_mods), 2) + # Project modules are also created + self.assertEqual(len(existing_mods.odoo_project_module_ids), 2) + + def test_import_modules_names_versions(self): + mod1 = "test1" + mod2 = "test2" + # Ensure these modules don't exist + existing_mods = self.module_branch_model.search( + [("module_name", "in", [mod1, mod2])] + ) + self.assertFalse(existing_mods) + # Import them through the wizard with their installed versions + modules_list_text = f"{mod1} 1.0.0\n{mod2} {self.branch}.1.0.0" + self._run_import_modules(self.project, modules_list_text) + # They now exists as orphaned modules and are linked to the project + existing_mods = self.module_branch_model.search( + [ + ("module_name", "in", [mod1, mod2]), + ("repository_id", "=", False), + ("odoo_project_ids", "=", self.project.id), + ] + ) + self.assertEqual(len(existing_mods), 2) + # Project modules are also created + self.assertEqual(len(existing_mods.odoo_project_module_ids), 2) + pm1 = existing_mods.odoo_project_module_ids.filtered( + lambda m: m.module_name == mod1 + ) + self.assertEqual(pm1.installed_version, "1.0.0") + pm2 = existing_mods.odoo_project_module_ids.filtered( + lambda m: m.module_name == mod2 + ) + self.assertEqual(pm2.installed_version, f"{self.branch}.1.0.0") + + def test_match_blacklisted_module(self): + mod1 = "test1" + mod2 = "test2" + mod1_blacklisted = self.wiz_model._get_module(mod1) + mod1_blacklisted.blacklisted = True + # Import them through the wizard + modules_list_text = f"{mod1}\n{mod2}" + self._run_import_modules(self.project, modules_list_text) + # Only one module has been imported as an orphaned module + existing_mods = self.module_branch_model.search( + [ + ("module_name", "in", [mod1, mod2]), + ("repository_id", "=", False), + ("odoo_project_ids", "=", self.project.id), + ] + ) + self.assertEqual(len(existing_mods), 1) + # Project modules are also created + self.assertEqual(len(existing_mods.odoo_project_module_ids), 1) + + def test_match_orphaned_module(self): + mod1 = "test1" + mod2 = "test2" + mod1_orphaned = self.wiz_model._get_module(mod1) + mod1_branch_orphaned = self.module_branch_model._create_orphaned_module_branch( + self.branch, mod1_orphaned + ) + # Import them through the wizard + modules_list_text = f"{mod1}\n{mod2}" + self._run_import_modules(self.project, modules_list_text) + # They now exists as orphaned modules and are linked to the project + existing_mods = self.module_branch_model.search( + [ + ("module_name", "in", [mod1, mod2]), + ("repository_id", "=", False), + ("odoo_project_ids", "=", self.project.id), + ] + ) + self.assertEqual(len(existing_mods), 2) + # One of them matched the existing orphaned module + self.assertIn(mod1_branch_orphaned, existing_mods) + # Project modules are also created + self.assertEqual(len(existing_mods.odoo_project_module_ids), 2) + + def test_match_generic_module(self): + mod1 = "test1" + mod2 = "test2" + mod1_generic = self.wiz_model._get_module(mod1) + repo_branch = self._create_odoo_repository_branch( + self.odoo_repository, self.branch + ) + mod1_branch_generic = self._create_odoo_module_branch( + mod1_generic, + self.branch, + specific=False, + repository_branch_id=repo_branch.id, + ) + # Import them through the wizard + modules_list_text = f"{mod1}\n{mod2}" + self._run_import_modules(self.project, modules_list_text) + # They now exists and are linked to the project + existing_mods = self.module_branch_model.search( + [ + ("module_name", "in", [mod1, mod2]), + ("odoo_project_ids", "=", self.project.id), + ] + ) + self.assertEqual(len(existing_mods), 2) + # One of them matched the existing generic module + self.assertIn(mod1_branch_generic, existing_mods) + # Project modules are also created + self.assertEqual(len(existing_mods.odoo_project_module_ids), 2) + + def test_match_project_repo_module(self): + # Assign a repository to the project + self.odoo_repository.odoo_version_id = self.branch + self.project.repository_id = self.odoo_repository + mod1 = "test1" + mod2 = "test2" + mod1_in_repo = self.wiz_model._get_module(mod1) + repo_branch = self._create_odoo_repository_branch( + self.odoo_repository, self.branch + ) + mod1_branch_in_repo = self._create_odoo_module_branch( + mod1_in_repo, + self.branch, + specific=True, + repository_branch_id=repo_branch.id, + ) + # Import them through the wizard + modules_list_text = f"{mod1}\n{mod2}" + self._run_import_modules(self.project, modules_list_text) + # They now exists and are linked to the project + existing_mods = self.module_branch_model.search( + [ + ("module_name", "in", [mod1, mod2]), + ("odoo_project_ids", "=", self.project.id), + ] + ) + self.assertEqual(len(existing_mods), 2) + # One of them matched the existing module hosted in project repository + self.assertIn(mod1_branch_in_repo, existing_mods) + # Project modules are also created + self.assertEqual(len(existing_mods.odoo_project_module_ids), 2) diff --git a/odoo_project/wizards/odoo_project_import_modules.py b/odoo_project/wizards/odoo_project_import_modules.py index f3b43c72..42858cc7 100644 --- a/odoo_project/wizards/odoo_project_import_modules.py +++ b/odoo_project/wizards/odoo_project_import_modules.py @@ -67,6 +67,8 @@ def _action_import_modules_list(self): module_name, version = data[0], False # for module_name in module_names: module = self._get_module(module_name) + if module.blacklisted: + continue module_branch = self._get_module_branch(module) project_module = self._get_project_module(module_branch, version) project_module_ids.append(project_module.id) @@ -108,20 +110,12 @@ def _get_module_branch(self, module): If it doesn't exist it'll be automatically created. """ module_branch_model = self.env["odoo.module.branch"] - args = [ - ("module_id", "=", module.id), - ("branch_id", "=", self.odoo_project_id.odoo_version_id.id), - ] - module_branch = module_branch_model.search(args) - if not module_branch: - # Create the module - branch = self.odoo_project_id.odoo_version_id - values = { - "module_id": module.id, - "branch_id": branch.id, - } - module_branch = module_branch_model.sudo().create(values) - if not module.blacklisted and not module_branch.repository_branch_id: + module_branch = False + branch = self.odoo_project_id.odoo_version_id + module_branch = module_branch_model._find_or_create( + branch, module, self.odoo_project_id.repository_id + ) + if not module_branch.repository_branch_id and not module_branch.specific: # If the module hasn't been found in existing repositories content, # it could be available somewhere on GitHub as a PR that could help # to identity its repository @@ -134,11 +128,11 @@ def _get_project_module(self, module_branch, version): If it doesn't exist it'll be automatically created. """ project_module_model = self.env["odoo.project.module"] - args = [ + domain = [ ("module_branch_id", "=", module_branch.id), ("odoo_project_id", "=", self.odoo_project_id.id), ] - project_module = project_module_model.search(args) + project_module = project_module_model.search(domain) values = { "module_branch_id": module_branch.id, "odoo_project_id": self.odoo_project_id.id, From 82afe0d532a96ee045fe03867ab37edea444b08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Thu, 31 Oct 2024 09:05:28 +0100 Subject: [PATCH 23/38] odoo_repository_*: rename some filters --- odoo_project/views/odoo_module_branch.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/odoo_project/views/odoo_module_branch.xml b/odoo_project/views/odoo_module_branch.xml index 570b8254..47fc3c52 100644 --- a/odoo_project/views/odoo_module_branch.xml +++ b/odoo_project/views/odoo_module_branch.xml @@ -21,14 +21,14 @@ odoo.module.branch - + From 699b7c185d2e183b718d9f2bb455b0f4d3683808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Tue, 26 Nov 2024 11:25:17 +0100 Subject: [PATCH 24/38] odoo_project: add notebook in form view --- odoo_project/views/odoo_project.xml | 72 +++++++++++++++-------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/odoo_project/views/odoo_project.xml b/odoo_project/views/odoo_project.xml index 7b5b4e7d..59502512 100644 --- a/odoo_project/views/odoo_project.xml +++ b/odoo_project/views/odoo_project.xml @@ -54,40 +54,44 @@ options="{'no_open': True}" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

-
- - - + + + + Modules inst. + + + +
+

+ +

+
+ + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - + + + + + diff --git a/odoo_project/wizards/odoo_project_import_modules.xml b/odoo_project/wizards/odoo_project_import_modules.xml index e6f10598..2e8f7ec5 100644 --- a/odoo_project/wizards/odoo_project_import_modules.xml +++ b/odoo_project/wizards/odoo_project_import_modules.xml @@ -2,58 +2,59 @@ - - - odoo.project.import.modules.form - odoo.project.import.modules - -
- - - - - -
+ odoo.project.import.modules.form + odoo.project.import.modules + + + + + + + + - - - -
+ + + - The list can be copied-pasted from columns 'Technical Name' and 'Latest Version' put side by side from a standard Odoo export of modules installed.
+ -
-
- - - -
-
-
- -
- - - - Import Project Modules - ir.actions.act_window - odoo.project.import.modules - form - new - +
@@ -434,7 +434,7 @@

Maintainers

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.

+

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_project/tests/common.py b/odoo_project/tests/common.py index 2506670c..b387a9d5 100644 --- a/odoo_project/tests/common.py +++ b/odoo_project/tests/common.py @@ -1,7 +1,7 @@ # Copyright 2024 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) -from odoo.tests.common import Form +from odoo.tests import Form from odoo.addons.odoo_repository.tests import common @@ -26,4 +26,5 @@ def _run_import_modules(cls, project, modules_list_text, **kwargs): form.modules_list = modules_list_text wiz = form.save() wiz.action_import() + cls.env.flush_all() # Force fields computation return wiz diff --git a/odoo_project/views/odoo_project.xml b/odoo_project/views/odoo_project.xml index f9db14a8..ac0e1556 100644 --- a/odoo_project/views/odoo_project.xml +++ b/odoo_project/views/odoo_project.xml @@ -51,14 +51,14 @@
@@ -69,16 +69,14 @@ nolabel="1" colspan="2" > - + - +
- + - + - +
@@ -101,10 +99,10 @@ nolabel="1" colspan="2" > - + - +