From 1f0b5c021fdc420972f9a11c5becb2ec6d27556c Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Sat, 1 Aug 2026 04:21:06 -0400 Subject: [PATCH] [FIX] migration 13->14: hand group_fiscal_year over to om_account_accountant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 13.0 -> 14.0 data migration died on: duplicate key value violates unique constraint "res_groups_name_uniq" Key (category_id, name)=(9, Allow to define fiscal years of more or less than a year) already exists In 13.0 that security group is declared by the core « account » module, so the database holds it as account.group_fiscal_year. In 14.0 core account no longer declares it and om_account_accountant (odoomates) does. Owning no XML id of its own, the module tries to CREATE the group and collides with the existing row. Renaming the XML id makes Odoo UPDATE the existing record instead. The record id is untouched, so user assignments, access rights and record rules pointing at the group survive -- on the reference database it holds none, but the fix must not depend on that. The fix hook also learns a « .sql » flavour, run through psql. The « .py » flavour is piped into « odoo-bin shell », which requires loading a not-yet-migrated database with the TARGET version's registry -- precisely what is failing at that point. A pure SQL fix needs no ORM and no registry. Verified end to end on a throwaway copy of the stuck database: the statement renames one row, a second run changes nothing, and the full 13->14 OpenUpgrade then completes (« Modules loaded. », base at 14.0.1.3, zero res_groups_name_uniq error). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../fix_migration_odoo130_to_odoo140.sql | 39 +++++++++++++++++++ script/todo/todo_upgrade.py | 32 +++++++++++---- 2 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 script/odoo/migration/fix_migration_odoo130_to_odoo140.sql diff --git a/script/odoo/migration/fix_migration_odoo130_to_odoo140.sql b/script/odoo/migration/fix_migration_odoo130_to_odoo140.sql new file mode 100644 index 0000000..cde9856 --- /dev/null +++ b/script/odoo/migration/fix_migration_odoo130_to_odoo140.sql @@ -0,0 +1,39 @@ +-- © 2021-2026 TechnoLibre (http://www.technolibre.ca) +-- License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +-- +-- Odoo 13.0 -> 14.0 : hand the « group_fiscal_year » security group over to +-- the module that owns it in 14.0. +-- +-- In 13.0 the group « Allow to define fiscal years of more or less than a +-- year » is declared by the core « account » module, so the database holds it +-- as account.group_fiscal_year. In 14.0 core account no longer declares it and +-- om_account_accountant (odoomates) does. During the upgrade that module finds +-- no XML id of its own, tries to CREATE the group, and hits: +-- +-- duplicate key value violates unique constraint "res_groups_name_uniq" +-- Key (category_id, name)=(9, Allow to define fiscal years ...) already exists +-- +-- Renaming the XML id makes Odoo UPDATE the existing row instead of creating a +-- duplicate. The record id is untouched, so any user assignment, access right +-- or record rule pointing at the group survives. +-- +-- Runs through psql, not the Odoo shell: at this point the database is still +-- 13.0 and loading it with the 14.0 registry is exactly what fails. + +UPDATE ir_model_data + SET module = 'om_account_accountant' + WHERE model = 'res.groups' + AND module = 'account' + AND name = 'group_fiscal_year' + -- Only when that module is actually part of this database. + AND EXISTS ( + SELECT 1 FROM ir_module_module + WHERE name = 'om_account_accountant' + AND state IN ('installed', 'to upgrade', 'to install') + ) + -- Idempotent: do nothing if the target XML id already exists. + AND NOT EXISTS ( + SELECT 1 FROM ir_model_data + WHERE module = 'om_account_accountant' + AND name = 'group_fiscal_year' + ); diff --git a/script/todo/todo_upgrade.py b/script/todo/todo_upgrade.py index b351903..cded711 100755 --- a/script/todo/todo_upgrade.py +++ b/script/todo/todo_upgrade.py @@ -1622,15 +1622,33 @@ class TodoUpgrade: if not lst_fix_migration_odoo[index]: print("") - file_path_fix_migration = os.path.join( - "script", - "odoo", - "migration", - f"fix_migration_odoo{(next_version-1)*10}_to_odoo{next_version*10}.py", + stem = os.path.join( + PATH_MIGRATION_GLOBAL, + f"fix_migration_odoo{(next_version - 1) * 10}" + f"_to_odoo{next_version * 10}", ) - if os.path.exists(file_path_fix_migration): + # Two flavours. « .sql » runs through psql: no Odoo registry, + # so it works on a database not yet migrated -- exactly when + # loading it with the TARGET version's code would fail. « .py » + # is piped into the Odoo shell when the ORM is really needed. + file_path_fix_migration = "" + cmd_fix_migration = "" + if os.path.exists(f"{stem}.sql"): + file_path_fix_migration = f"{stem}.sql" + cmd_fix_migration = ( + f"psql -v ON_ERROR_STOP=1 -d {database_name_upgrade}" + f" -f ./{file_path_fix_migration}" + ) + elif os.path.exists(f"{stem}.py"): + file_path_fix_migration = f"{stem}.py" + cmd_fix_migration = ( + f"cat ./{file_path_fix_migration} |" + f" ./odoo{next_version}.0/odoo/odoo-bin shell" + f" -d {database_name_upgrade}" + ) + if file_path_fix_migration: status, cmd_executed = self.todo_upgrade_execute( - f"cat ./{file_path_fix_migration} | ./odoo{next_version}.0/odoo/odoo-bin shell -d {database_name_upgrade}", + cmd_fix_migration, single_source_odoo=True, )