From bb759eb8ca867a2545e150c48afb2a7a4dc40abc Mon Sep 17 00:00:00 2001 From: Laetitia Da Costa Date: Mon, 10 Aug 2026 16:04:48 +0200 Subject: [PATCH] [IMP]hide_portal_module_by_user: fix invoices access in v18 --- hide_portal_module_by_user/__manifest__.py | 2 +- .../controllers/main.py | 23 +++++++++++- .../models/res_users.py | 37 +++++++++++++++++-- .../views/portal_home.xml | 14 +++++-- 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/hide_portal_module_by_user/__manifest__.py b/hide_portal_module_by_user/__manifest__.py index 83abff7..4424573 100644 --- a/hide_portal_module_by_user/__manifest__.py +++ b/hide_portal_module_by_user/__manifest__.py @@ -5,7 +5,7 @@ 'name': 'Hide Portal Module By User', 'description': """ Show / Hide Specific Portal Docs on res.users""", - 'version': "18.0.1.0.0", + 'version': "18.0.1.1.0", 'license': 'AGPL-3', 'author': 'Munin', 'website': 'https://github.com/AxeldelosReyes/odoo_web_modules', diff --git a/hide_portal_module_by_user/controllers/main.py b/hide_portal_module_by_user/controllers/main.py index d5c2d04..cebdb3c 100644 --- a/hide_portal_module_by_user/controllers/main.py +++ b/hide_portal_module_by_user/controllers/main.py @@ -5,14 +5,33 @@ from odoo.http import request from odoo.addons.portal.controllers.portal import CustomerPortal from werkzeug.exceptions import NotFound -WHITELISTED_ROUTES = ['/my/invoices','/my/home', '/my', '/my/account', '/my/security', '/my/payment_method'] +from ..models.res_users import match_portal_url + +# Routes always reachable, whatever the portal groups of the user. +# +# Split in two because of how each route is matched, not because of how +# important it is: both lists are equally "always reachable". +# +# Exact match, for routes that have no sub-route of their own. '/my' has to +# stay here in any case: as a prefix it would match every portal page and +# make this module a no-op. +WHITELISTED_ROUTES = ['/my', '/my/home', '/my/account', '/my/security', '/my/payment_method'] + +# Prefix match, for routes that also serve detail and pager sub-pages +# ('/my/invoices/', '/my/invoices/page/', '/my/invoices/overdue'), +# which go through _prepare_portal_layout_values too. +WHITELISTED_ROUTE_PREFIXES = ['/my/invoices'] class CustomerPortalPolicy(CustomerPortal): def _prepare_portal_layout_values(self): vals = super()._prepare_portal_layout_values() current_path = request.httprequest.path - if request.env.user.validate_portal_url(current_path) or current_path in WHITELISTED_ROUTES: + if current_path in WHITELISTED_ROUTES: + return vals + if any(match_portal_url(route, current_path) for route in WHITELISTED_ROUTE_PREFIXES): + return vals + if request.env.user.validate_portal_url(current_path): return vals if '/my/' not in current_path: return vals diff --git a/hide_portal_module_by_user/models/res_users.py b/hide_portal_module_by_user/models/res_users.py index 70b5316..71ccad8 100644 --- a/hide_portal_module_by_user/models/res_users.py +++ b/hide_portal_module_by_user/models/res_users.py @@ -1,16 +1,47 @@ # Copyright 2026 Munin # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from urllib.parse import parse_qsl, urlsplit + from odoo import fields, models +def match_portal_url(portal_url, target): + """Whether ``target`` is covered by the ``portal_url`` of a portal group. + + A group url is the one read from the portal.portal_docs_entry t-call it + was generated from, and it covers: + + - the listing itself and its sub-pages, since the portal serves detail + and pager pages below it ('/my/projects/135', '/my/projects/page/2'), + which go through _prepare_portal_layout_values as well; + - the entries narrowing that listing down with query arguments. Odoo 18 + splits some listings in several entries this way, eg. account has + '/my/invoices?filterby=invoices' and '/my/invoices?filterby=bills' + where 16.0 had a single '/my/invoices'. + + A group url carrying query arguments only matches targets carrying them + all, so a group can be restricted to one such entry; a group url without + any matches every entry of the listing. + """ + if not portal_url or not target: + return False + ref = urlsplit(portal_url) + got = urlsplit(target) + root = ref.path.rstrip('/') + if not (got.path == root or got.path.startswith(root + '/')): + return False + return set(parse_qsl(ref.query)) <= set(parse_qsl(got.query)) + + class ResUsers(models.Model): _inherit = "res.users" portal_url = fields.Char(string='Portal URL') def validate_portal_url(self, url): - group = self.sudo().env['res.groups'].search([('portal_url', '=', url)]) - if self.env.user in group.users: - return True + groups = self.sudo().env['res.groups'].search([('portal_url', '!=', False)]) + for group in groups.filtered(lambda g: match_portal_url(g.portal_url, url)): + if self.env.user in group.users: + return True return False diff --git a/hide_portal_module_by_user/views/portal_home.xml b/hide_portal_module_by_user/views/portal_home.xml index eec831e..29dbe8f 100644 --- a/hide_portal_module_by_user/views/portal_home.xml +++ b/hide_portal_module_by_user/views/portal_home.xml @@ -2,9 +2,17 @@ -- 2.49.1