diff --git a/odoo/addons/base/models/assetsbundle.py b/odoo/addons/base/models/assetsbundle.py index f28aa3539bf..63dc107c635 100644 --- a/odoo/addons/base/models/assetsbundle.py +++ b/odoo/addons/base/models/assetsbundle.py @@ -222,6 +222,18 @@ class AssetsBundle(object): def get_debug_asset_url(self, extra='', name='%', extension='%'): return f"/web/assets/debug/{extra}{name}{extension}" + def _unlink_attachments(self, attachments): + """ Unlinks attachments without actually calling unlink, so that the ORM cache is not cleared. + + Specifically, if an attachment is generated while a view is rendered, clearing the ORM cache + could unload fields loaded with a sudo(), and expected to be readable by the view. + Such a view would be website.layout when main_object is an ir.ui.view. + """ + to_delete = set(attach.store_fname for attach in attachments if attach.store_fname) + self.env.cr.execute(f"DELETE FROM {attachments._table} WHERE id IN %s", [tuple(attachments.ids)]) + for file_path in to_delete: + attachments._file_delete(file_path) + def clean_attachments(self, extension): """ Takes care of deleting any outdated ir.attachment records associated to a bundle before saving a fresh one. @@ -248,7 +260,7 @@ class AssetsBundle(object): # avoid to invalidate cache if it's already empty (mainly useful for test) if attachments: - attachments.unlink() + self._unlink_attachments(attachments) # force bundle invalidation on other workers self.env['ir.qweb'].clear_caches() @@ -591,7 +603,7 @@ class AssetsBundle(object): compiled = self.run_rtlcss(compiled) if not self.css_errors and old_attachments: - old_attachments.unlink() + self._unlink_attachments(old_attachments) old_attachments = None fragments = self.rx_css_split.split(compiled) diff --git a/odoo/addons/base/models/ir_attachment.py b/odoo/addons/base/models/ir_attachment.py index 815d0781ee4..50003009992 100644 --- a/odoo/addons/base/models/ir_attachment.py +++ b/odoo/addons/base/models/ir_attachment.py @@ -142,6 +142,7 @@ class IrAttachment(models.Model): def _mark_for_gc(self, fname): """ Add ``fname`` in a checklist for the filestore garbage collection. """ + fname = re.sub('[.]', '', fname).strip('/\\') # we use a spooldir: add an empty file in the subdirectory 'checklist' full_path = os.path.join(self._full_path('checklist'), fname) if not os.path.exists(full_path): diff --git a/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py b/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py index 6ee0c60acad..35f43462db5 100644 --- a/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py +++ b/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py @@ -736,19 +736,19 @@ class TestAssetsBundleWithIRAMock(FileTouchable): # patch methods 'create' and 'unlink' of model 'ir.attachment' origin_create = IrAttachment.create - origin_unlink = IrAttachment.unlink + origin_unlink = AssetsBundle._unlink_attachments @api.model_create_multi def create(self, vals_list): counter.update(['create'] * len(vals_list)) return origin_create(self, vals_list) - def unlink(self): + def unlink(self, attachments): counter.update(['unlink']) - return origin_unlink(self) + return origin_unlink(self, attachments) self.patch(IrAttachment, 'create', create) - self.patch(IrAttachment, 'unlink', unlink) + self.patch(AssetsBundle, '_unlink_attachments', unlink) def _get_asset(self): files, _ = self.env['ir.qweb']._get_asset_content(self.stylebundle_name)