Skip to content

[17.0][OU-FIX] openupgrade_framework: cascade inactive child views on force unlink#19

Closed
eantones wants to merge 1 commit into
17.0from
17.0-ou-fix-view-unlink-inactive-children
Closed

[17.0][OU-FIX] openupgrade_framework: cascade inactive child views on force unlink#19
eantones wants to merge 1 commit into
17.0from
17.0-ou-fix-view-unlink-inactive-children

Conversation

@eantones

Copy link
Copy Markdown
Member

Patches a core defect that silently leaves obsolete parent views in the database after a migration hop.

The bug (in Odoo core, not in OpenUpgrade)

View.unlink() emulates an ondelete cascade over inherit_children_ids when _force_unlink is set — the context ir.model.data._process_end() uses while removing records that became obsolete after a module update:

def unlink(self):
    # if in uninstall mode and has children views, emulate an ondelete cascade
    if self.env.context.get('_force_unlink', False) and self.inherit_children_ids:
        self.inherit_children_ids.unlink()

inherit_children_ids is a plain One2many, so it is read with active_test enabled and inactive child views are not returned. The cascade skips them, but they still hold the inherit_id RESTRICT foreign key at database level, so the parent's DELETE is rejected, logged as a benign bad query, and the obsolete parent view survives orphaned. _process_end() walks its obsolete records once and never revisits the parent.

Why this matters for OpenUpgrade

The common assumption is that obsolete views are naturally removed by the next real -u all after the migration. For this class of view they are not: every -u all re-runs the same _process_end → same cascade → same skipped inactive child → same rejected DELETE. The orphan is permanent unless something deletes the child first.

Hit at 16.0 → 17.0: website_sale.payment_footer (dropped from core in 17.0) survives, because its child website_sale.payment_sale_note is active="False":

Deleting 6740@ir.ui.view (website_sale.payment_footer)      <- parent first
bad query: DELETE FROM "ir_ui_view" WHERE id IN (6740)      <- rejected by the FK
Deleting 5500@ir.ui.view (website_sale.payment_sale_note)   <- child, 77s later

Two conditions must coincide, which is why it is rarely seen: the parent must be processed before the child (ORDER BY id DESC on ir_model_data.id), and the child must be inactive.

The fix

Wrap View.unlink so the cascade reads its children with active_test=False. The child is then removed before the parent's DELETE, which succeeds, and no orphan is left.

Why here and not only in core

This should be fixed in core, and the same one-line change applies there. It is patched in openupgrade_framework because:

  • migrations must not depend on the fix being accepted and released upstream;
  • Odoo 16.0 and older are EOL and will never receive it, yet OpenUpgrade still has to migrate through them;
  • this is exactly what odoo_patch exists for — correcting core behaviour that misbehaves during a migration.

The patch is a no-op once core reads inactive children itself, so it is safe to keep across versions.

Scope

The cascade was added to core in 2018 (odoo/odoo PR 25499), which fixed the active child case and left the inactive one. The defect is present unchanged from 10.0 through 18.0.

… 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.
@eantones

Copy link
Copy Markdown
Member Author

Superseded: fixing this in Odoo core instead (nuobit/odoo#9 + an 18.0 counterpart). The defect is not migration-specific — _force_unlink is also set on every module update (ir.model.data._process_end, called from odoo/modules/loading.py on every -u/-i) and on every module uninstall (_module_data_uninstall), so it belongs in core, and the core fix is deployed through repos.yaml on the versions we run.

@eantones eantones closed this Jul 15, 2026
@eantones
eantones deleted the 17.0-ou-fix-view-unlink-inactive-children branch July 15, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant