From 9fe04750f7f9620d79eb07c58951346b6bbddffb Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Wed, 15 Jul 2026 19:11:06 +0200 Subject: [PATCH] [17.0][OU-FIX] openupgrade_framework: cascade inactive child views on force unlink Odoo's View.unlink() emulates an ondelete cascade on inherit_children_ids when _force_unlink is set (as ir.model.data._process_end does), but reads that One2many with active_test enabled, so INACTIVE child views are skipped. They still hold the inherit_id RESTRICT foreign key, so the parent's DELETE is rejected (benign bad query) and the obsolete parent view survives ORPHANED -- contrary to the assumption that a later -u all sweeps obsolete views, it never does. Hit at 16.0 -> 17.0: website_sale.payment_footer survives because its child payment_sale_note is active="False". Patch the cascade to read the children with active_test=False. This is a core defect (present 10.0 -> 18.0, since the cascade was added by odoo/odoo#25499); patched here so migrations do not depend on it being fixed upstream. No-op once core reads inactive children itself. --- .../odoo/addons/base/models/ir_ui_view.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openupgrade_framework/odoo_patch/odoo/addons/base/models/ir_ui_view.py b/openupgrade_framework/odoo_patch/odoo/addons/base/models/ir_ui_view.py index 402362253db..9390ffe6242 100644 --- a/openupgrade_framework/odoo_patch/odoo/addons/base/models/ir_ui_view.py +++ b/openupgrade_framework/odoo_patch/odoo/addons/base/models/ir_ui_view.py @@ -102,6 +102,27 @@ def _inverse_arch(self): return _inverse_arch._original_method(self) +def unlink(self): + """Let the ``_force_unlink`` child cascade see INACTIVE inherited views. + + Core's ``View.unlink`` reads ``inherit_children_ids`` with ``active_test`` + enabled, so an inactive child view is never cascaded away. The parent's + DELETE then hits the ``inherit_id`` RESTRICT foreign key, is logged as a + benign "bad query", and the parent survives ORPHANED -- the obsolete view + is never swept, not even by a later ``-u all``. + + Hit at 16.0->17.0: ``website_sale.payment_footer`` (dropped from core in + 17.0) survived because its child ``website_sale.payment_sale_note`` is + ``active="False"`` and so was skipped by the cascade. + """ + if self.env.context.get("_force_unlink", False): + # Needs to override the context so the cascade sees inactive children + self = self.with_context( # pylint: disable=context-overridden + active_test=False + ) + return View.unlink._original_method(self) + + _check_xml._original_method = View._check_xml View._check_xml = _check_xml check._original_method = NameManager.check @@ -112,3 +133,5 @@ def _inverse_arch(self): View._check_field_paths = _check_field_paths _inverse_arch._original_method = View._inverse_arch View._inverse_arch = _inverse_arch +unlink._original_method = View.unlink +View.unlink = unlink