erplibre/script/odoo/migration/neutralize_cow_views.py

238 lines
7.8 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Neutralize the website COW views that would break a version bump.
How it works
------------
``key`` is the only thing that pairs a website copy with the module view it
came from. A copy whose key matches nothing is never paired, so it never
receives the new ``inherit_id``, never changes shape, and never takes part in
any view combination. Renaming the key is therefore enough to take a copy out
of the way::
UPDATE ir_ui_view SET key = '<prefix>.' || key, active = false WHERE id = ?
Setting ``active = false`` alone would NOT work: an inactive copy that keeps
the same key still shadows the module view.
Nothing is deleted, so ``inherit_id ondelete='restrict'`` and the
``website_page`` foreign keys are never touched, and the 12.0 arch stays in
database as a readable archive. ``--restore`` puts everything back.
Plain psql on purpose: this must run on a database that has not been migrated
yet, where starting an Odoo shell of the target version is not guaranteed.
"""
import argparse
import os
import subprocess
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from check_cow_views import analyse, t # noqa: E402
DEFAULT_PREFIX = "zz_cow_archive"
def run_psql(database, sql):
"""Run a statement and return stdout, raising on failure."""
result = subprocess.run(
[ADD] migration: look at a COW copy before agreeing to give it up The prompt asked whether to neutralize copies without showing what they hold. Answering meant giving up a customization sight unseen — often three lines, an id and a container width, sometimes a whole page, and nothing told them apart. « v » now shows both halves of the question. What the copy changed, diffed against the module view it shadows: on the database at hand, 5 lines added and 3 removed, two CSS anchors and a container width. And why it breaks, by showing the declaration in each version — portal.frontend_layout is a standalone template in 12.0 and inheritance specs in 13.0, which is the whole explanation. « w » is the same two, full screen, space to switch. The current version comes from ir_module_module, not .odoo-version: the checkout is switched to the target before this runs, so reading the file compared the target with itself and printed the same declaration twice. That is what it did until it was run against a real migration. --list says which copies a past neutralization put aside. A renamed key is invisible in the interface, so without it the only trace was remembering. Checked on the VM mid-migration: both views on view 2670, the screen builds headless and toggles, --list reports and reports nothing when there is nothing. A missing database now exits 2 with a message instead of a traceback. --- FR --- L'invite demandait de neutraliser des copies sans montrer ce qu'elles contiennent. Répondre revenait à renoncer à une personnalisation sans l'avoir vue — souvent trois lignes, un id et une largeur de conteneur, parfois une page entière, et rien ne les distinguait. « v » montre désormais les deux moitiés de la question. Ce que la copie a changé, comparé à la vue de module qu'elle masque : sur la base en cours, 5 lignes ajoutées et 3 retirées, deux ancres CSS et une largeur. Et pourquoi ça casse, en affichant la déclaration dans chaque version — portal.frontend_layout est un gabarit autonome en 12.0 et des consignes d'héritage en 13.0, ce qui est toute l'explication. « w » donne les deux en plein écran, espace pour basculer. La version courante vient d'ir_module_module, pas de .odoo-version : le checkout est basculé sur la cible avant cette étape, donc lire le fichier comparait la cible avec elle-même et affichait deux fois la même déclaration. C'est ce qu'il faisait jusqu'à l'essai sur une vraie migration. --list dit quelles copies une neutralisation passée a mises de côté. Une clé renommée est invisible dans l'interface ; sans cela, la seule trace était de s'en souvenir. Vérifié sur la VM en cours de migration : les deux vues sur la vue 2670, l'écran se construit sans terminal et bascule, --list rapporte et ne rapporte rien quand il n'y a rien. Une base absente sort en 2 avec un message plutôt qu'une trace d'appel. Assisted-by: Claude Opus 5
2026-08-11 23:49:05 -04:00
["psql", "-X", "-w", "-d", database, "-tAF", "|", "-c", sql],
capture_output=True,
text=True,
)
if result.returncode:
raise RuntimeError(
f"Query failed on '{database}': {result.stderr.strip()}"
)
return result.stdout.strip()
def neutralize(database, lst_view_id, prefix):
"""Rename the key of the given views and deactivate them."""
if not lst_view_id:
return 0
ids = ",".join(str(view_id) for view_id in lst_view_id)
output = run_psql(
database,
"WITH updated AS ("
f" UPDATE ir_ui_view SET key = '{prefix}.' || key, active = false"
f" WHERE id IN ({ids}) AND key NOT LIKE '{prefix}.%'"
" RETURNING 1) SELECT count(*) FROM updated;",
)
return int(output or 0)
[ADD] migration: look at a COW copy before agreeing to give it up The prompt asked whether to neutralize copies without showing what they hold. Answering meant giving up a customization sight unseen — often three lines, an id and a container width, sometimes a whole page, and nothing told them apart. « v » now shows both halves of the question. What the copy changed, diffed against the module view it shadows: on the database at hand, 5 lines added and 3 removed, two CSS anchors and a container width. And why it breaks, by showing the declaration in each version — portal.frontend_layout is a standalone template in 12.0 and inheritance specs in 13.0, which is the whole explanation. « w » is the same two, full screen, space to switch. The current version comes from ir_module_module, not .odoo-version: the checkout is switched to the target before this runs, so reading the file compared the target with itself and printed the same declaration twice. That is what it did until it was run against a real migration. --list says which copies a past neutralization put aside. A renamed key is invisible in the interface, so without it the only trace was remembering. Checked on the VM mid-migration: both views on view 2670, the screen builds headless and toggles, --list reports and reports nothing when there is nothing. A missing database now exits 2 with a message instead of a traceback. --- FR --- L'invite demandait de neutraliser des copies sans montrer ce qu'elles contiennent. Répondre revenait à renoncer à une personnalisation sans l'avoir vue — souvent trois lignes, un id et une largeur de conteneur, parfois une page entière, et rien ne les distinguait. « v » montre désormais les deux moitiés de la question. Ce que la copie a changé, comparé à la vue de module qu'elle masque : sur la base en cours, 5 lignes ajoutées et 3 retirées, deux ancres CSS et une largeur. Et pourquoi ça casse, en affichant la déclaration dans chaque version — portal.frontend_layout est un gabarit autonome en 12.0 et des consignes d'héritage en 13.0, ce qui est toute l'explication. « w » donne les deux en plein écran, espace pour basculer. La version courante vient d'ir_module_module, pas de .odoo-version : le checkout est basculé sur la cible avant cette étape, donc lire le fichier comparait la cible avec elle-même et affichait deux fois la même déclaration. C'est ce qu'il faisait jusqu'à l'essai sur une vraie migration. --list dit quelles copies une neutralisation passée a mises de côté. Une clé renommée est invisible dans l'interface ; sans cela, la seule trace était de s'en souvenir. Vérifié sur la VM en cours de migration : les deux vues sur la vue 2670, l'écran se construit sans terminal et bascule, --list rapporte et ne rapporte rien quand il n'y a rien. Une base absente sort en 2 avec un message plutôt qu'une trace d'appel. Assisted-by: Claude Opus 5
2026-08-11 23:49:05 -04:00
def list_archived(database, prefix):
"""The copies a previous neutralization put aside.
Knowing what was archived is the other half of --restore: a key renamed
months ago is invisible in the interface — the copy is inactive and no
longer pairs with anything — so without this listing the only trace is
someone's memory of having run --apply.
"""
output = run_psql(
database,
"SELECT id, substring(key from " + str(len(prefix) + 2) + "),"
" COALESCE(website_id::text, ''), active,"
" octet_length(arch_db::text)"
f" FROM ir_ui_view WHERE key LIKE '{prefix}.%' ORDER BY id;",
)
lst_row = []
for line in output.splitlines():
if not line.strip():
continue
parts = line.split("|")
if len(parts) >= 5:
lst_row.append(
{
"id": int(parts[0]),
"key": parts[1],
"website_id": parts[2] or None,
"active": parts[3] == "t",
"arch_bytes": int(parts[4] or 0),
}
)
return lst_row
def restore(database, prefix):
"""Undo a neutralization: strip the prefix and reactivate."""
output = run_psql(
database,
"WITH updated AS ("
f" UPDATE ir_ui_view SET key = substring(key from {len(prefix) + 2}),"
" active = true"
f" WHERE key LIKE '{prefix}.%'"
" RETURNING 1) SELECT count(*) FROM updated;",
)
return int(output or 0)
def main():
parser = argparse.ArgumentParser(
description=(
"Neutralize the website COW views that would break a version"
" bump, by renaming their key. Dry-run unless --apply."
)
)
parser.add_argument("-d", "--database", required=True)
parser.add_argument(
"-t",
"--target_version",
help="target Odoo source directory, e.g. odoo13.0",
)
parser.add_argument(
"--prefix",
default=DEFAULT_PREFIX,
help=f"archive prefix for the key (default: {DEFAULT_PREFIX})",
)
parser.add_argument(
"--apply", action="store_true", help="actually write to the database"
)
parser.add_argument(
"--restore",
action="store_true",
help="undo a previous neutralization and reactivate the copies",
)
[ADD] migration: look at a COW copy before agreeing to give it up The prompt asked whether to neutralize copies without showing what they hold. Answering meant giving up a customization sight unseen — often three lines, an id and a container width, sometimes a whole page, and nothing told them apart. « v » now shows both halves of the question. What the copy changed, diffed against the module view it shadows: on the database at hand, 5 lines added and 3 removed, two CSS anchors and a container width. And why it breaks, by showing the declaration in each version — portal.frontend_layout is a standalone template in 12.0 and inheritance specs in 13.0, which is the whole explanation. « w » is the same two, full screen, space to switch. The current version comes from ir_module_module, not .odoo-version: the checkout is switched to the target before this runs, so reading the file compared the target with itself and printed the same declaration twice. That is what it did until it was run against a real migration. --list says which copies a past neutralization put aside. A renamed key is invisible in the interface, so without it the only trace was remembering. Checked on the VM mid-migration: both views on view 2670, the screen builds headless and toggles, --list reports and reports nothing when there is nothing. A missing database now exits 2 with a message instead of a traceback. --- FR --- L'invite demandait de neutraliser des copies sans montrer ce qu'elles contiennent. Répondre revenait à renoncer à une personnalisation sans l'avoir vue — souvent trois lignes, un id et une largeur de conteneur, parfois une page entière, et rien ne les distinguait. « v » montre désormais les deux moitiés de la question. Ce que la copie a changé, comparé à la vue de module qu'elle masque : sur la base en cours, 5 lignes ajoutées et 3 retirées, deux ancres CSS et une largeur. Et pourquoi ça casse, en affichant la déclaration dans chaque version — portal.frontend_layout est un gabarit autonome en 12.0 et des consignes d'héritage en 13.0, ce qui est toute l'explication. « w » donne les deux en plein écran, espace pour basculer. La version courante vient d'ir_module_module, pas de .odoo-version : le checkout est basculé sur la cible avant cette étape, donc lire le fichier comparait la cible avec elle-même et affichait deux fois la même déclaration. C'est ce qu'il faisait jusqu'à l'essai sur une vraie migration. --list dit quelles copies une neutralisation passée a mises de côté. Une clé renommée est invisible dans l'interface ; sans cela, la seule trace était de s'en souvenir. Vérifié sur la VM en cours de migration : les deux vues sur la vue 2670, l'écran se construit sans terminal et bascule, --list rapporte et ne rapporte rien quand il n'y a rien. Une base absente sort en 2 avec un message plutôt qu'une trace d'appel. Assisted-by: Claude Opus 5
2026-08-11 23:49:05 -04:00
parser.add_argument(
"--list",
dest="list_archived",
action="store_true",
help="list the copies a previous neutralization put aside",
)
config = parser.parse_args()
[ADD] migration: look at a COW copy before agreeing to give it up The prompt asked whether to neutralize copies without showing what they hold. Answering meant giving up a customization sight unseen — often three lines, an id and a container width, sometimes a whole page, and nothing told them apart. « v » now shows both halves of the question. What the copy changed, diffed against the module view it shadows: on the database at hand, 5 lines added and 3 removed, two CSS anchors and a container width. And why it breaks, by showing the declaration in each version — portal.frontend_layout is a standalone template in 12.0 and inheritance specs in 13.0, which is the whole explanation. « w » is the same two, full screen, space to switch. The current version comes from ir_module_module, not .odoo-version: the checkout is switched to the target before this runs, so reading the file compared the target with itself and printed the same declaration twice. That is what it did until it was run against a real migration. --list says which copies a past neutralization put aside. A renamed key is invisible in the interface, so without it the only trace was remembering. Checked on the VM mid-migration: both views on view 2670, the screen builds headless and toggles, --list reports and reports nothing when there is nothing. A missing database now exits 2 with a message instead of a traceback. --- FR --- L'invite demandait de neutraliser des copies sans montrer ce qu'elles contiennent. Répondre revenait à renoncer à une personnalisation sans l'avoir vue — souvent trois lignes, un id et une largeur de conteneur, parfois une page entière, et rien ne les distinguait. « v » montre désormais les deux moitiés de la question. Ce que la copie a changé, comparé à la vue de module qu'elle masque : sur la base en cours, 5 lignes ajoutées et 3 retirées, deux ancres CSS et une largeur. Et pourquoi ça casse, en affichant la déclaration dans chaque version — portal.frontend_layout est un gabarit autonome en 12.0 et des consignes d'héritage en 13.0, ce qui est toute l'explication. « w » donne les deux en plein écran, espace pour basculer. La version courante vient d'ir_module_module, pas de .odoo-version : le checkout est basculé sur la cible avant cette étape, donc lire le fichier comparait la cible avec elle-même et affichait deux fois la même déclaration. C'est ce qu'il faisait jusqu'à l'essai sur une vraie migration. --list dit quelles copies une neutralisation passée a mises de côté. Une clé renommée est invisible dans l'interface ; sans cela, la seule trace était de s'en souvenir. Vérifié sur la VM en cours de migration : les deux vues sur la vue 2670, l'écran se construit sans terminal et bascule, --list rapporte et ne rapporte rien quand il n'y a rien. Une base absente sort en 2 avec un message plutôt qu'une trace d'appel. Assisted-by: Claude Opus 5
2026-08-11 23:49:05 -04:00
try:
return _run(config, parser)
except RuntimeError as exc:
# Une base absente ou un PostgreSQL arrêté est une erreur d'usage, pas
# un défaut de l'outil : une trace d'appel ferait chercher le bogue au
# mauvais endroit.
print(f"❌ {exc}")
return 2
def _run(config, parser):
if config.list_archived:
lst_row = list_archived(config.database, config.prefix)
if not lst_row:
print(
f"✅ -> {t('No view with this prefix on')}"
f" '{config.database}' : '{config.prefix}.'"
)
[ADD] migration: look at a COW copy before agreeing to give it up The prompt asked whether to neutralize copies without showing what they hold. Answering meant giving up a customization sight unseen — often three lines, an id and a container width, sometimes a whole page, and nothing told them apart. « v » now shows both halves of the question. What the copy changed, diffed against the module view it shadows: on the database at hand, 5 lines added and 3 removed, two CSS anchors and a container width. And why it breaks, by showing the declaration in each version — portal.frontend_layout is a standalone template in 12.0 and inheritance specs in 13.0, which is the whole explanation. « w » is the same two, full screen, space to switch. The current version comes from ir_module_module, not .odoo-version: the checkout is switched to the target before this runs, so reading the file compared the target with itself and printed the same declaration twice. That is what it did until it was run against a real migration. --list says which copies a past neutralization put aside. A renamed key is invisible in the interface, so without it the only trace was remembering. Checked on the VM mid-migration: both views on view 2670, the screen builds headless and toggles, --list reports and reports nothing when there is nothing. A missing database now exits 2 with a message instead of a traceback. --- FR --- L'invite demandait de neutraliser des copies sans montrer ce qu'elles contiennent. Répondre revenait à renoncer à une personnalisation sans l'avoir vue — souvent trois lignes, un id et une largeur de conteneur, parfois une page entière, et rien ne les distinguait. « v » montre désormais les deux moitiés de la question. Ce que la copie a changé, comparé à la vue de module qu'elle masque : sur la base en cours, 5 lignes ajoutées et 3 retirées, deux ancres CSS et une largeur. Et pourquoi ça casse, en affichant la déclaration dans chaque version — portal.frontend_layout est un gabarit autonome en 12.0 et des consignes d'héritage en 13.0, ce qui est toute l'explication. « w » donne les deux en plein écran, espace pour basculer. La version courante vient d'ir_module_module, pas de .odoo-version : le checkout est basculé sur la cible avant cette étape, donc lire le fichier comparait la cible avec elle-même et affichait deux fois la même déclaration. C'est ce qu'il faisait jusqu'à l'essai sur une vraie migration. --list dit quelles copies une neutralisation passée a mises de côté. Une clé renommée est invisible dans l'interface ; sans cela, la seule trace était de s'en souvenir. Vérifié sur la VM en cours de migration : les deux vues sur la vue 2670, l'écran se construit sans terminal et bascule, --list rapporte et ne rapporte rien quand il n'y a rien. Une base absente sort en 2 avec un message plutôt qu'une trace d'appel. Assisted-by: Claude Opus 5
2026-08-11 23:49:05 -04:00
return 0
print(
f"ℹ {len(lst_row)} {t('archived COW view(s) on')}"
f" '{config.database}' :"
)
[ADD] migration: look at a COW copy before agreeing to give it up The prompt asked whether to neutralize copies without showing what they hold. Answering meant giving up a customization sight unseen — often three lines, an id and a container width, sometimes a whole page, and nothing told them apart. « v » now shows both halves of the question. What the copy changed, diffed against the module view it shadows: on the database at hand, 5 lines added and 3 removed, two CSS anchors and a container width. And why it breaks, by showing the declaration in each version — portal.frontend_layout is a standalone template in 12.0 and inheritance specs in 13.0, which is the whole explanation. « w » is the same two, full screen, space to switch. The current version comes from ir_module_module, not .odoo-version: the checkout is switched to the target before this runs, so reading the file compared the target with itself and printed the same declaration twice. That is what it did until it was run against a real migration. --list says which copies a past neutralization put aside. A renamed key is invisible in the interface, so without it the only trace was remembering. Checked on the VM mid-migration: both views on view 2670, the screen builds headless and toggles, --list reports and reports nothing when there is nothing. A missing database now exits 2 with a message instead of a traceback. --- FR --- L'invite demandait de neutraliser des copies sans montrer ce qu'elles contiennent. Répondre revenait à renoncer à une personnalisation sans l'avoir vue — souvent trois lignes, un id et une largeur de conteneur, parfois une page entière, et rien ne les distinguait. « v » montre désormais les deux moitiés de la question. Ce que la copie a changé, comparé à la vue de module qu'elle masque : sur la base en cours, 5 lignes ajoutées et 3 retirées, deux ancres CSS et une largeur. Et pourquoi ça casse, en affichant la déclaration dans chaque version — portal.frontend_layout est un gabarit autonome en 12.0 et des consignes d'héritage en 13.0, ce qui est toute l'explication. « w » donne les deux en plein écran, espace pour basculer. La version courante vient d'ir_module_module, pas de .odoo-version : le checkout est basculé sur la cible avant cette étape, donc lire le fichier comparait la cible avec elle-même et affichait deux fois la même déclaration. C'est ce qu'il faisait jusqu'à l'essai sur une vraie migration. --list dit quelles copies une neutralisation passée a mises de côté. Une clé renommée est invisible dans l'interface ; sans cela, la seule trace était de s'en souvenir. Vérifié sur la VM en cours de migration : les deux vues sur la vue 2670, l'écran se construit sans terminal et bascule, --list rapporte et ne rapporte rien quand il n'y a rien. Une base absente sort en 2 avec un message plutôt qu'une trace d'appel. Assisted-by: Claude Opus 5
2026-08-11 23:49:05 -04:00
for row in lst_row:
state = "active" if row["active"] else "inactive"
print(
f" - id={row['id']} website={row['website_id'] or '-'}"
f" {row['key']} ({state}, {row['arch_bytes']} B)"
)
print(
f" {t('Their arch is intact. Restore them all with --restore,')}"
f" {t('once the module view they shadow has the right shape.')}"
[ADD] migration: look at a COW copy before agreeing to give it up The prompt asked whether to neutralize copies without showing what they hold. Answering meant giving up a customization sight unseen — often three lines, an id and a container width, sometimes a whole page, and nothing told them apart. « v » now shows both halves of the question. What the copy changed, diffed against the module view it shadows: on the database at hand, 5 lines added and 3 removed, two CSS anchors and a container width. And why it breaks, by showing the declaration in each version — portal.frontend_layout is a standalone template in 12.0 and inheritance specs in 13.0, which is the whole explanation. « w » is the same two, full screen, space to switch. The current version comes from ir_module_module, not .odoo-version: the checkout is switched to the target before this runs, so reading the file compared the target with itself and printed the same declaration twice. That is what it did until it was run against a real migration. --list says which copies a past neutralization put aside. A renamed key is invisible in the interface, so without it the only trace was remembering. Checked on the VM mid-migration: both views on view 2670, the screen builds headless and toggles, --list reports and reports nothing when there is nothing. A missing database now exits 2 with a message instead of a traceback. --- FR --- L'invite demandait de neutraliser des copies sans montrer ce qu'elles contiennent. Répondre revenait à renoncer à une personnalisation sans l'avoir vue — souvent trois lignes, un id et une largeur de conteneur, parfois une page entière, et rien ne les distinguait. « v » montre désormais les deux moitiés de la question. Ce que la copie a changé, comparé à la vue de module qu'elle masque : sur la base en cours, 5 lignes ajoutées et 3 retirées, deux ancres CSS et une largeur. Et pourquoi ça casse, en affichant la déclaration dans chaque version — portal.frontend_layout est un gabarit autonome en 12.0 et des consignes d'héritage en 13.0, ce qui est toute l'explication. « w » donne les deux en plein écran, espace pour basculer. La version courante vient d'ir_module_module, pas de .odoo-version : le checkout est basculé sur la cible avant cette étape, donc lire le fichier comparait la cible avec elle-même et affichait deux fois la même déclaration. C'est ce qu'il faisait jusqu'à l'essai sur une vraie migration. --list dit quelles copies une neutralisation passée a mises de côté. Une clé renommée est invisible dans l'interface ; sans cela, la seule trace était de s'en souvenir. Vérifié sur la VM en cours de migration : les deux vues sur la vue 2670, l'écran se construit sans terminal et bascule, --list rapporte et ne rapporte rien quand il n'y a rien. Une base absente sort en 2 avec un message plutôt qu'une trace d'appel. Assisted-by: Claude Opus 5
2026-08-11 23:49:05 -04:00
)
return 0
if config.restore:
count = restore(config.database, config.prefix)
print(
f"✅ -> {count} {t('COW view(s) restored on')} '{config.database}'."
)
return 0
if not config.target_version:
parser.error("--target_version is required unless --restore is used")
if not os.path.isdir(config.target_version):
print(
f"❌ {t('Target version directory not found')} :"
f" '{config.target_version}'"
)
return 2
lst_at_risk, _, _ = analyse(config.database, config.target_version)
if not lst_at_risk:
print(f"✅ -> {t('No website COW view to neutralize.')}")
return 0
print(
f"⚠️ {len(lst_at_risk)} {t('website COW view(s) would break the bump')}"
f" {t('to')} {config.target_version} :"
)
lst_view_id = []
for view_id, key, mode, target_mode, website_id, reason in lst_at_risk:
lst_view_id.append(view_id)
print(
f" - id={view_id} website={website_id} {key}"
f" : {mode} -> {target_mode} ({t(reason)})"
)
if not config.apply:
print(
f"ℹ {t('Dry-run. Add --apply to rename their key to')}"
f" '{config.prefix}.<key>' {t('and deactivate them. Reversible')}"
f" {t('with --restore; the arch stays in database.')}"
)
# 1 = des copies sont à neutraliser. Le pilote lisait une phrase
# anglaise pour le savoir ; il devenait aveugle une fois traduite.
return 1
count = neutralize(config.database, lst_view_id, config.prefix)
print(
f"✅ -> {count} {t('COW view(s) neutralized (key prefixed with')}"
f" '{config.prefix}.', {t('deactivated). The arch is kept as an')}"
f" {t('archive; use --restore to undo.')}"
)
return 0
if __name__ == "__main__":
sys.exit(main())