From 7bcb69521f90e53b6639b541f2aa43c8d9c13a6a Mon Sep 17 00:00:00 2001 From: Guido Galetto Date: Thu, 2 Jul 2026 17:28:16 +0000 Subject: [PATCH] [ADD] portal_ux: grant portal access to all contacts in one click Adds a "Grant Access to All" button to the portal access wizard (portal.wizard) that grants portal access to every contact listed in the wizard with a valid email in a single action, instead of having to click "Grant Access" row by row. --- portal_ux/README.rst | 80 +++++++++++++++++++++++ portal_ux/__init__.py | 5 ++ portal_ux/__manifest__.py | 41 ++++++++++++ portal_ux/tests/__init__.py | 5 ++ portal_ux/tests/test_portal_wizard.py | 46 +++++++++++++ portal_ux/wizards/__init__.py | 5 ++ portal_ux/wizards/portal_wizard.py | 15 +++++ portal_ux/wizards/portal_wizard_views.xml | 14 ++++ 8 files changed, 211 insertions(+) create mode 100644 portal_ux/README.rst create mode 100644 portal_ux/__init__.py create mode 100644 portal_ux/__manifest__.py create mode 100644 portal_ux/tests/__init__.py create mode 100644 portal_ux/tests/test_portal_wizard.py create mode 100644 portal_ux/wizards/__init__.py create mode 100644 portal_ux/wizards/portal_wizard.py create mode 100644 portal_ux/wizards/portal_wizard_views.xml diff --git a/portal_ux/README.rst b/portal_ux/README.rst new file mode 100644 index 00000000..1a8fb0dd --- /dev/null +++ b/portal_ux/README.rst @@ -0,0 +1,80 @@ +.. |company| replace:: ADHOC SA + +.. |company_logo| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-logo.png + :alt: ADHOC SA + :target: https://www.adhoc.com.ar + +.. |icon| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-icon.png + +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +========= +Portal UX +========= + +Usability improvements for the portal access wizard. + +* Adds a **"Grant Access to All"** button to the portal access wizard + (``portal.wizard``), granting portal access to every contact loaded in the + wizard in a single pass. +* Applies only to contacts with a valid email (``email_state == 'ok'``) that + don't already have portal access and are not internal users + (``is_portal`` / ``is_internal``); the rest are skipped without raising an + error. +* Reuses the native per-row logic (``action_grant_access``) of + ``portal.wizard.user``, so any override of that method added by other + modules keeps applying. + + +Installation +============ + +Only install the module. + +Configuration +============= + +There is nothing to configure. + +Usage +===== + +#. Go to **Contacts**, select one or more contacts and run the **"Grant + portal access"** action (``portal.wizard``). +#. In the wizard, click **"Grant Access to All"**. +#. Every listed contact with a valid, unused email gets portal access in a + single operation. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: http://runbot.adhoc.com.ar/ + +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 smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* |company| |icon| + +Contributors +------------ + +Maintainer +---------- + +|company_logo| + +This module is maintained by the |company|. + +To contribute to this module, please visit https://www.adhoc.com.ar. diff --git a/portal_ux/__init__.py b/portal_ux/__init__.py new file mode 100644 index 00000000..c72130af --- /dev/null +++ b/portal_ux/__init__.py @@ -0,0 +1,5 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from . import wizards diff --git a/portal_ux/__manifest__.py b/portal_ux/__manifest__.py new file mode 100644 index 00000000..e8605a92 --- /dev/null +++ b/portal_ux/__manifest__.py @@ -0,0 +1,41 @@ +############################################################################## +# +# Copyright (C) 2026 ADHOC SA (http://www.adhoc.com.ar) +# All Rights Reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + "name": "Portal UX", + "version": "19.0.1.0.0", + "category": "Tools", + "sequence": 14, + "summary": "", + "author": "ADHOC SA", + "website": "www.adhoc.com.ar", + "license": "AGPL-3", + "images": [], + "depends": [ + "portal", + ], + "data": [ + "wizards/portal_wizard_views.xml", + ], + "demo": [], + "test": [], + "installable": True, + "auto_install": False, + "application": False, +} diff --git a/portal_ux/tests/__init__.py b/portal_ux/tests/__init__.py new file mode 100644 index 00000000..7c663517 --- /dev/null +++ b/portal_ux/tests/__init__.py @@ -0,0 +1,5 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from . import test_portal_wizard diff --git a/portal_ux/tests/test_portal_wizard.py b/portal_ux/tests/test_portal_wizard.py new file mode 100644 index 00000000..8d9f825c --- /dev/null +++ b/portal_ux/tests/test_portal_wizard.py @@ -0,0 +1,46 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo.tests import TransactionCase, tagged + + +@tagged("-at_install", "post_install") +class TestPortalWizard(TransactionCase): + def test_action_grant_access_all(self): + partner_model = self.env["res.partner"].sudo() + valid_partner_1 = partner_model.create({"name": "Valid Contact 1", "email": "valid1@test.example.com"}) + valid_partner_2 = partner_model.create({"name": "Valid Contact 2", "email": "valid2@test.example.com"}) + invalid_partner = partner_model.create({"name": "Invalid Contact", "email": "not-an-email"}) + + partners = valid_partner_1 + valid_partner_2 + invalid_partner + wizard = self.env["portal.wizard"].with_context(active_ids=partners.ids).create({}) + self.assertEqual(len(wizard.user_ids), 3) + + wizard.action_grant_access_all() + + group_portal = self.env.ref("base.group_portal") + for partner in (valid_partner_1, valid_partner_2): + self.assertTrue(partner.user_ids, "A portal user should have been created") + self.assertIn(group_portal, partner.user_ids.group_ids) + + self.assertFalse( + invalid_partner.user_ids, + "No user should be created for a contact with an invalid email", + ) + + def test_action_grant_access_all_skips_existing_portal_user(self): + partner_model = self.env["res.partner"].sudo() + already_portal_partner = partner_model.create( + {"name": "Already Portal Contact", "email": "already@test.example.com"} + ) + wizard = self.env["portal.wizard"].with_context(active_ids=already_portal_partner.ids).create({}) + wizard.user_ids.action_grant_access() + user = already_portal_partner.user_ids + self.assertTrue(user, "Precondition: partner should already have a portal user") + + # Re-open the wizard: the existing portal user must be skipped, not re-processed. + wizard_2 = self.env["portal.wizard"].with_context(active_ids=already_portal_partner.ids).create({}) + wizard_2.action_grant_access_all() + + self.assertEqual(already_portal_partner.user_ids, user) diff --git a/portal_ux/wizards/__init__.py b/portal_ux/wizards/__init__.py new file mode 100644 index 00000000..bad0004e --- /dev/null +++ b/portal_ux/wizards/__init__.py @@ -0,0 +1,5 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from . import portal_wizard diff --git a/portal_ux/wizards/portal_wizard.py b/portal_ux/wizards/portal_wizard.py new file mode 100644 index 00000000..2d9a2c3c --- /dev/null +++ b/portal_ux/wizards/portal_wizard.py @@ -0,0 +1,15 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import models + + +class PortalWizard(models.TransientModel): + _inherit = "portal.wizard" + + def action_grant_access_all(self): + for wizard_user in self.user_ids: + if not wizard_user.is_portal and not wizard_user.is_internal and wizard_user.email_state == "ok": + wizard_user.action_grant_access() + return self._action_open_modal() diff --git a/portal_ux/wizards/portal_wizard_views.xml b/portal_ux/wizards/portal_wizard_views.xml new file mode 100644 index 00000000..c4d6d9a8 --- /dev/null +++ b/portal_ux/wizards/portal_wizard_views.xml @@ -0,0 +1,14 @@ + + + portal.wizard.form (in portal_ux) + portal.wizard + + + +