From 275d4880dac923aad29999378068d6a0fc262d42 Mon Sep 17 00:00:00 2001 From: Franco Leyes Date: Wed, 23 Sep 2026 13:22:03 -0300 Subject: [PATCH 1/4] [FIX] portal_backend: let the chatter load followers and attachments The core /mail/thread/data controller empties request_list when the user is not internal, so the response carries neither followersCount nor attachments. The chatter keeps its spinner while followersCount is undefined and the attachments are not loaded, so both counters spin forever for a portal backend user. The cut happens before any permission check, so it is not an ACL issue. Override the controller to set portal_bypass for portal backend users, which is the context this module already uses to make _is_internal return True. Core still checks read access on the record before answering, so a portal backend user without access to the record gets nothing, and plain portal users are unaffected. Also drop the _get_mail_thread_data override: that method no longer exists in Odoo 18, where _thread_to_store replaced it. It was dead code left over from the migration, and it used to strip "followers" to dodge an error on records followed by an internal user. That error does not show up anymore. closes ingadhoc/miscellaneous#449 Signed-off-by: Guido Galetto --- portal_backend/controllers/__init__.py | 1 + portal_backend/controllers/thread.py | 18 ++++++++++++++++++ portal_backend/models/mail_thread.py | 6 ------ 3 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 portal_backend/controllers/thread.py diff --git a/portal_backend/controllers/__init__.py b/portal_backend/controllers/__init__.py index 5cfd7701..99cf36ef 100644 --- a/portal_backend/controllers/__init__.py +++ b/portal_backend/controllers/__init__.py @@ -5,3 +5,4 @@ from . import patch_web_controller_utils from . import discuss +from . import thread diff --git a/portal_backend/controllers/thread.py b/portal_backend/controllers/thread.py new file mode 100644 index 00000000..c9053e90 --- /dev/null +++ b/portal_backend/controllers/thread.py @@ -0,0 +1,18 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import http +from odoo.addons.mail.controllers.thread import ThreadController +from odoo.http import request + + +class PortalBackendThreadController(ThreadController): + @http.route("/mail/thread/data", methods=["POST"], type="json", auth="user") + def mail_thread_data(self, thread_model, thread_id, request_list, **kwargs): + """Core empties request_list for non internal users, so the chatter never gets + followersCount nor attachments and its counters spin forever. Let portal backend users + through: core still checks read access on the record before answering.""" + if request.env.user.has_group("portal_backend.group_portal_backend"): + request.update_context(portal_bypass=True) + return super().mail_thread_data(thread_model, thread_id, request_list, **kwargs) diff --git a/portal_backend/models/mail_thread.py b/portal_backend/models/mail_thread.py index dd46a9da..4c9ab9cc 100644 --- a/portal_backend/models/mail_thread.py +++ b/portal_backend/models/mail_thread.py @@ -6,9 +6,3 @@ class MailThread(models.AbstractModel): message_attachment_count = fields.Integer(groups="base.group_user, portal_backend.group_portal_backend") message_follower_ids = fields.One2many(groups="base.group_user, portal_backend.group_portal_backend") - - def _get_mail_thread_data(self, request_list): - """This avoids to get an error when an internal user is follower of a record that a portal_backend has access""" - if "followers" in request_list and self.env.user.has_group("portal_backend.group_portal_backend"): - request_list.remove("followers") - return super()._get_mail_thread_data(request_list) From bc14d41504013973525f81fe70c7b41ea6dc433f Mon Sep 17 00:00:00 2001 From: roboadhoc Date: Fri, 25 Sep 2026 11:34:00 +0000 Subject: [PATCH 2/4] [BOT] Bump version: portal_backend 18.0.1.2.0 Merged: ingadhoc/miscellaneous#449 --- portal_backend/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portal_backend/__manifest__.py b/portal_backend/__manifest__.py index 2c87c48f..616110a9 100644 --- a/portal_backend/__manifest__.py +++ b/portal_backend/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Portal Backend", - "version": "18.0.1.1.0", + "version": "18.0.1.2.0", "category": "Base", "sequence": 14, "summary": "", From dcab508f0ca307ff3e4db7b18dd70a4afc454423 Mon Sep 17 00:00:00 2001 From: Franco Leyes Date: Mon, 28 Sep 2026 10:59:41 -0300 Subject: [PATCH 3/4] [ADD] base_exception_compat: keep the pre-3590 exception flow for every model The replacement of base.exception.method.detect_exceptions() used to live in sale_exception_compat. The method is defined on the abstract model every exception module inherits from, so stock_exception was covered only while a sale module happened to be installed, with nothing declaring that dependency. Move it to its own module that auto installs with base_exception, so the flow is restored for stock.picking, stock.move and anything else that inherits base.exception without depending on sales. The override delegates to super() when the context carries test_base_exception, the key upstream sets to exercise the second cursor on purpose in its own tests. Nothing sets it outside tests, so the behaviour is unchanged elsewhere and those tests keep meaning something once this module is loaded. Without this module, on a database with stock_exception and no sale_exception, confirming a transfer that matches an exception rule raises BaseExceptionError from detect_exceptions(). The web client swallows it and asks the record for its popup, but stock.picking does not override _must_popup_exception(), so the default False turns it into a soft reload: the transfer stays in draft and the user is told nothing. stock_exception's own test suite errors on 2 of its 4 tests for the same reason. closes ingadhoc/miscellaneous#453 Related: ingadhoc/sale#1871 Signed-off-by: Filoquin adhoc --- base_exception_compat/README.rst | 88 +++++++++++++++++++ base_exception_compat/__init__.py | 1 + base_exception_compat/__manifest__.py | 12 +++ base_exception_compat/models/__init__.py | 1 + .../models/base_exception_method.py | 24 +++++ 5 files changed, 126 insertions(+) create mode 100644 base_exception_compat/README.rst create mode 100644 base_exception_compat/__init__.py create mode 100644 base_exception_compat/__manifest__.py create mode 100644 base_exception_compat/models/__init__.py create mode 100644 base_exception_compat/models/base_exception_method.py diff --git a/base_exception_compat/README.rst b/base_exception_compat/README.rst new file mode 100644 index 00000000..4069b88c --- /dev/null +++ b/base_exception_compat/README.rst @@ -0,0 +1,88 @@ +.. |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 + +===================== +Base Exception Compat +===================== + +Keeps the exception flow that `base_exception` had before OCA/server-tools#3590, +where `detect_exceptions()` writes in the ongoing transaction and returns the +rules instead of writing through a second cursor and raising. + +The upstream mechanism opens a second database connection to store the +exceptions. That connection writes the same rows the ongoing transaction is +about to write, so the operation either fails with a serialization error or +blocks until the worker is killed. It also turns the exception into an error +for any caller outside the backend web client (portal, e-commerce, API, cron), +because the handler that translates it into a popup is only registered in +`web.assets_backend`. + +The replacement belongs here and not in a module of a specific application: +`detect_exceptions()` is defined on `base.exception.method`, the abstract model +every exception module inherits from, so `sale_exception`, `stock_exception` +and any other one are all affected. Keeping it in `sale_exception_compat` left +stock covered only as long as a sale module happened to be installed, with +nothing declaring that dependency. + +Installation +============ + +To install this module, you need to: + +#. Only need to install the module. It is auto installed with `base_exception`. + +Configuration +============= + +To configure this module, you need to: + +#. Nothing to configure. + +Usage +===== + +To use this module, you need to: + +#. Confirm or validate a record that matches an exception rule: the exception + popup shows up instead of an error. + +.. 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/base_exception_compat/__init__.py b/base_exception_compat/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/base_exception_compat/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/base_exception_compat/__manifest__.py b/base_exception_compat/__manifest__.py new file mode 100644 index 00000000..83a36154 --- /dev/null +++ b/base_exception_compat/__manifest__.py @@ -0,0 +1,12 @@ +{ + "name": "Base Exception Compat", + "version": "18.0.1.0.0", + "category": "Tools", + "author": "ADHOC SA", + "website": "www.adhoc.com.ar", + "license": "AGPL-3", + "summary": "Keeps the pre-3590 exception flow while OCA fixes the second cursor", + "depends": ["base_exception"], + "installable": True, + "auto_install": True, +} diff --git a/base_exception_compat/models/__init__.py b/base_exception_compat/models/__init__.py new file mode 100644 index 00000000..3109b19e --- /dev/null +++ b/base_exception_compat/models/__init__.py @@ -0,0 +1 @@ +from . import base_exception_method diff --git a/base_exception_compat/models/base_exception_method.py b/base_exception_compat/models/base_exception_method.py new file mode 100644 index 00000000..91c620f0 --- /dev/null +++ b/base_exception_compat/models/base_exception_method.py @@ -0,0 +1,24 @@ +from odoo import models +from odoo.fields import Command + + +class BaseExceptionMethod(models.AbstractModel): + _inherit = "base.exception.method" + + def detect_exceptions(self): + # Full replacement of the upstream method: it writes the exceptions + # through a second cursor that commits on the same rows the ongoing + # transaction is about to write, which breaks with a serialization + # failure (or blocks until the worker dies). Write in the current + # transaction and return the rules, as before OCA/server-tools#3590. + if self.env.context.get("test_base_exception"): + # Upstream sets this key to exercise the second cursor on purpose. + # Nothing sets it outside its own tests, so keeping that path alive + # leaves them meaningful instead of red. + return super().detect_exceptions() + all_exception_ids, rules_to_remove, rules_to_add = self._get_exceptions() + for rule_id, records in rules_to_remove.items(): + records.write({"exception_ids": [Command.unlink(rule_id)]}) + for rule_id, records in rules_to_add.items(): + records.write({"exception_ids": [Command.link(rule_id)]}) + return all_exception_ids From da8117e525fa1dd3f3fff5f544153ccfd8eb5aa7 Mon Sep 17 00:00:00 2001 From: roboadhoc Date: Wed, 30 Sep 2026 19:52:33 +0000 Subject: [PATCH 4/4] [BOT] Bump version: base_exception_compat 18.0.1.1.0 Merged: ingadhoc/miscellaneous#453 --- base_exception_compat/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_exception_compat/__manifest__.py b/base_exception_compat/__manifest__.py index 83a36154..ba40f67a 100644 --- a/base_exception_compat/__manifest__.py +++ b/base_exception_compat/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Base Exception Compat", - "version": "18.0.1.0.0", + "version": "18.0.1.1.0", "category": "Tools", "author": "ADHOC SA", "website": "www.adhoc.com.ar",