diff --git a/mass_module_updater/README.rst b/mass_module_updater/README.rst new file mode 100644 index 00000000000..c62e894266b --- /dev/null +++ b/mass_module_updater/README.rst @@ -0,0 +1,37 @@ +=================== +Mass Module Updater +=================== + +This module provides a wizard to update multiple Odoo modules at once by their technical names. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module, you need to: + +1. Go to Apps > Mass Module Updater. +2. Enter the technical names of the modules you want to update, separated by commas. +3. Click on "Update Modules". + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. + +Credits +======= + +Authors +------- + +* Pol Reig + +Maintainers +----------- + +This module is maintained by QubiQ. diff --git a/mass_module_updater/__init__.py b/mass_module_updater/__init__.py new file mode 100644 index 00000000000..88607ea28d1 --- /dev/null +++ b/mass_module_updater/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Pol Reig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import wizards diff --git a/mass_module_updater/__manifest__.py b/mass_module_updater/__manifest__.py new file mode 100644 index 00000000000..0e10ae9078e --- /dev/null +++ b/mass_module_updater/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2026 Pol Reig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Mass Module Updater", + "summary": "Update multiple Odoo modules at once from a single wizard", + "version": "18.0.1.0.0", + "category": "Extra Tools", + "license": "AGPL-3", + "author": "Pol Reig, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/server-tools", + "depends": ["base"], + "data": [ + "security/ir.model.access.csv", + "views/mass_module_updater_wizard_views.xml", + ], + "installable": True, +} diff --git a/mass_module_updater/pyproject.toml b/mass_module_updater/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/mass_module_updater/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/mass_module_updater/security/ir.model.access.csv b/mass_module_updater/security/ir.model.access.csv new file mode 100644 index 00000000000..9e7dd9c348b --- /dev/null +++ b/mass_module_updater/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mass_module_updater_wizard,mass.module.updater.wizard,model_mass_module_updater_wizard,base.group_system,1,1,1,1 diff --git a/mass_module_updater/views/mass_module_updater_wizard_views.xml b/mass_module_updater/views/mass_module_updater_wizard_views.xml new file mode 100644 index 00000000000..bbf4c70c0bb --- /dev/null +++ b/mass_module_updater/views/mass_module_updater_wizard_views.xml @@ -0,0 +1,44 @@ + + + + mass.module.updater.wizard.view.form + mass.module.updater.wizard + +
+ + + +
+
+
+
+
+ + + Mass Module Updater + mass.module.updater.wizard + form + new + + + +
diff --git a/mass_module_updater/wizards/__init__.py b/mass_module_updater/wizards/__init__.py new file mode 100644 index 00000000000..828faa3cd18 --- /dev/null +++ b/mass_module_updater/wizards/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Pol Reig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import mass_module_updater_wizard diff --git a/mass_module_updater/wizards/mass_module_updater_wizard.py b/mass_module_updater/wizards/mass_module_updater_wizard.py new file mode 100644 index 00000000000..ffa248262d1 --- /dev/null +++ b/mass_module_updater/wizards/mass_module_updater_wizard.py @@ -0,0 +1,60 @@ +# Copyright 2026 Pol Reig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models +from odoo.exceptions import UserError + + +class MassModuleUpdaterWizard(models.TransientModel): + _name = "mass.module.updater.wizard" + _description = "Mass Module Updater Wizard" + + module_names = fields.Char( + required=True, help="Comma-separated list of module names." + ) + + def _get_modules(self): + raw_names = self.module_names.split(",") + module_names = [name.strip() for name in raw_names if name.strip()] + + if not module_names: + raise UserError(_("Please provide at least one valid module name.")) + + modules = self.env["ir.module.module"].search([("name", "in", module_names)]) + + found_names = set(modules.mapped("name")) + missing_names = set(module_names) - found_names + if missing_names: + raise UserError( + _( + "The following modules were not found: %s", + ", ".join(sorted(missing_names)), + ) + ) + return modules + + def action_install_modules(self): + self.ensure_one() + modules = self._get_modules() + already_installed = modules.filtered(lambda m: m.state == "installed") + if already_installed: + raise UserError( + _( + "The following modules are already installed: %s", + ", ".join(sorted(already_installed.mapped("name"))), + ) + ) + return modules.button_immediate_install() + + def action_update_modules(self): + self.ensure_one() + modules = self._get_modules() + uninstalled = modules.filtered(lambda m: m.state != "installed") + if uninstalled: + raise UserError( + _( + "The following modules are not installed: %s", + ", ".join(sorted(uninstalled.mapped("name"))), + ) + ) + return modules.button_immediate_upgrade()