diff --git a/web_pwa_customize/models/res_config_settings.py b/web_pwa_customize/models/res_config_settings.py index 2f4226dcbd5c..c8e77cc60618 100644 --- a/web_pwa_customize/models/res_config_settings.py +++ b/web_pwa_customize/models/res_config_settings.py @@ -32,7 +32,7 @@ def get_values(self): pwa_icon_attachment = ( self.env["ir.attachment"] .sudo() - .search([("url", "like", self._pwa_icon_url_base + ".")]) + .search([("url", "like", self._pwa_icon_url_base + ".")], limit=1) ) res["pwa_icon"] = pwa_icon_attachment.datas if pwa_icon_attachment else False return res diff --git a/web_pwa_customize/tests/test_web_pwa_customize.py b/web_pwa_customize/tests/test_web_pwa_customize.py index 5d6935a2119b..af503cdaa78d 100644 --- a/web_pwa_customize/tests/test_web_pwa_customize.py +++ b/web_pwa_customize/tests/test_web_pwa_customize.py @@ -1,7 +1,7 @@ # Copyright 2024 Tecnativa - Víctor Martínez # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo.tests.common import tagged +from odoo.tests.common import Form, tagged from odoo.addons.base.tests.common import HttpCaseWithUserDemo @@ -24,3 +24,41 @@ def test_webmanifest_customize(self): self.assertEqual(data["short_name"], "SHORT-NAME") self.assertEqual(data["background_color"], "#2E69B5") self.assertEqual(data["theme_color"], "#2E69B4") + + def test_default_get_pwa_icon_singleton(self): + """Test default_get doesn't crash with multiple PWA icon attachments. + + Reproduces the real scenario: duplicate attachments with the same URL + accumulate due to race conditions or repeated saves. The search in + get_values() matches all of them, and .datas triggers ensure_one(). + """ + attachment_model = self.env["ir.attachment"].sudo() + # Simulate production scenario: 2 attachments with the same URL + # (the actual bug — duplicate base icon) + for _ in range(2): + attachment_model.create( + { + "name": "/web_pwa_customize/icon.svg", + "url": "/web_pwa_customize/icon.svg", + "type": "binary", + "datas": "dGVzdA==", + "mimetype": "image/svg+xml", + } + ) + # Also create resized PNG variants (normal scenario) + for size in ["128x128", "144x144", "152x152"]: + attachment_model.create( + { + "name": f"/web_pwa_customize/icon{size}.png", + "url": f"/web_pwa_customize/icon{size}.png", + "type": "binary", + "datas": "dGVzdA==", + "mimetype": "image/png", + } + ) + # Form() triggers default_get() which calls get_values() via super chain + # This is the exact flow that crashes in production + settings_form = Form(self.env["res.config.settings"]) + # If the bug exists, this line raises: + # ValueError: Expected singleton: ir.attachment(137, 123) + settings_form.save()