From 05da4f34db852e4b43dfb10b5e30908429436851 Mon Sep 17 00:00:00 2001 From: Sylvain LE GAL Date: Thu, 18 Jun 2026 21:32:58 +0200 Subject: [PATCH 01/15] [ADD] product_sale_tax_price_included: merged into 'account'. product_sale_tax_price_included is an OCA module that basically provide the new native feature 'tax_string', on product models. https://github.com/OCA/product-attribute/tree/12.0/product_sale_tax_price_included#product-sale-tax-price-included --- openupgrade_scripts/apriori.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openupgrade_scripts/apriori.py b/openupgrade_scripts/apriori.py index 84d625356b4f..687139ad5243 100644 --- a/openupgrade_scripts/apriori.py +++ b/openupgrade_scripts/apriori.py @@ -98,6 +98,7 @@ "pos_sale_order_load": "pos_sale", # OCA/product-attribute "stock_account_product_cost_security": "product_cost_security", + "product_sale_tax_price_included": "account", # OCA/server-tools "base_jsonify": "jsonifier", # OCA/server-ux From 48f9b6918cdf25def4b41210993c0e6a0ad2e50a Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Wed, 18 Mar 2026 14:07:33 +0100 Subject: [PATCH 02/15] [OU-ADD] stock_putaway_method: Merged into stock_putaway_hook stock_putaway_method (OCA/stock-logistics-warehouse) was deprecated in 15.0 and replaced by stock_putaway_hook (OCA/stock-logistics-workflow). The migration PR OCA/stock-logistics-warehouse#1545 was closed with the comment "Deprecated. Replaced by stock_putaway_hook" by an OCA member. Without this entry, upgrading from 14.0 to 15.0 leaves stock_putaway_method as an orphaned installed module in ir_module_module. --- openupgrade_scripts/apriori.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openupgrade_scripts/apriori.py b/openupgrade_scripts/apriori.py index 687139ad5243..37466ececae2 100644 --- a/openupgrade_scripts/apriori.py +++ b/openupgrade_scripts/apriori.py @@ -108,6 +108,7 @@ # OCA/stock-logistics-warehouse "stock_inventory_exclude_sublocation": "stock", "stock_orderpoint_manual_procurement": "stock", + "stock_putaway_method": "stock_putaway_hook", # OCA/stock-logistics-workflow "stock_deferred_assign": "stock", "stock_move_assign_picking_hook": "stock", From 7ae825949ba285dcf43780c06938cfcdc538e827 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Mon, 22 Jun 2026 22:38:58 +0200 Subject: [PATCH 03/15] [OU-ADD] queue_job_context: Merged into queue_job queue_job_context (OCA/queue) exists in 14.0 but was removed in 15.0; its context-key feature is provided natively by the queue_job base module from 16.0 (queue_job._job_prepare_context_before_enqueue_keys). The 15.0 migration PR OCA/queue#725 was closed with the note "not needed in v16+". Upstream OpenUpgrade only records the merge in the 16.0 apriori, so upgrading from 14.0 to 15.0 leaves queue_job_context as an orphaned installed module in ir_module_module. Add the entry to 15.0 so it is merged into queue_job at the hop where it disappears. --- openupgrade_scripts/apriori.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openupgrade_scripts/apriori.py b/openupgrade_scripts/apriori.py index 37466ececae2..564c313c43d2 100644 --- a/openupgrade_scripts/apriori.py +++ b/openupgrade_scripts/apriori.py @@ -99,6 +99,8 @@ # OCA/product-attribute "stock_account_product_cost_security": "product_cost_security", "product_sale_tax_price_included": "account", + # OCA/queue + "queue_job_context": "queue_job", # OCA/server-tools "base_jsonify": "jsonifier", # OCA/server-ux From 327f11ea786436c0d0d459ecc4f1e809fab8e0f6 Mon Sep 17 00:00:00 2001 From: hugues de keyzer Date: Wed, 2 Oct 2024 17:43:42 +0200 Subject: [PATCH 04/15] [OU-FIX] fix hr.expense account.move and lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix the value of several fields on account.move and account.move.line records linked to hr.expense records. Co-authored-by: Miquel Raïch --- .../hr_expense/15.0.2.0/post-migration.py | 54 +++++++------------ 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py b/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py index c9b5b354fe41..8f232ddf831a 100644 --- a/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py +++ b/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py @@ -14,44 +14,26 @@ def _fill_payment_state(env): openupgrade.logged_query( env.cr, """ - UPDATE account_move_line - SET exclude_from_invoice_tab=(coalesce(quantity, 0) = 0) - WHERE expense_id IS NOT NULL + UPDATE account_move_line AS aml + SET exclude_from_invoice_tab = true + FROM account_account AS aa + INNER JOIN account_account_type AS aat ON + aa.user_type_id = aat.id + WHERE + aml.account_id = aa.id AND + aml.expense_id IS NOT NULL AND + aat.type = 'payable' """, ) - # Recompute payment_state for the moves associated to the expenses, as on - # v14 these ones were not computed being of type `entry`, which changes now - # on v15 if the method `_payment_state_matters` returns True, which is the - # case for the expense moves - for move in env["hr.expense.sheet"].search([]).account_move_id: - # Extracted and adapted from _compute_amount() in account.move - new_pmt_state = "not_paid" if move.move_type != "entry" else False - total_to_pay = total_residual = 0.0 - for line in move.line_ids: - if line.account_id.user_type_id.type in ("receivable", "payable"): - total_to_pay += line.balance - total_residual += line.amount_residual - currencies = move._get_lines_onchange_currency().currency_id - currency = currencies if len(currencies) == 1 else move.company_id.currency_id - if currency.is_zero(move.amount_residual): - reconciled_payments = move._get_reconciled_payments() - if not reconciled_payments or all( - payment.is_matched for payment in reconciled_payments - ): - new_pmt_state = "paid" - else: - new_pmt_state = move._get_invoice_in_payment_state() - elif currency.compare_amounts(total_to_pay, total_residual) != 0: - new_pmt_state = "partial" - openupgrade.logged_query( - env.cr, - """ - UPDATE account_move - SET payment_state = %s - WHERE id = %s - """, - (new_pmt_state, move.id), - ) + # Recompute several fields (always_tax_exigible, amount_residual, + # amount_residual_signed, amount_untaxed, amount_untaxed_signed, + # payment_state) for the moves associated to the expenses, as on v14 these + # ones were not computed being of type `entry`, which changes now on v15 + # if the method `_payment_state_matters` returns True, which is the case + # for the expense moves + env["account.move"].with_context(active_test=False, tracking_disable=True).search( + [("line_ids.expense_id", "!=", False)] + )._compute_amount() @openupgrade.migrate() From f5c0e32728f343d9d0ea115c54a55f4fb37a7520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Taymans?= Date: Wed, 9 Oct 2024 14:44:10 +0200 Subject: [PATCH 05/15] [OU-FIX] disable fiscalyear_lock_date check fiscalyear_lock_date check prevent modifying old move, but some field should be recomputed to be correct in 15.0. --- .../scripts/hr_expense/15.0.2.0/post-migration.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py b/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py index 8f232ddf831a..35f1c12ace23 100644 --- a/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py +++ b/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py @@ -25,6 +25,11 @@ def _fill_payment_state(env): aat.type = 'payable' """, ) + # Disable fiscalyear_lock_date check + _check_fiscalyear_lock_date = env[ + "account.move" + ].__class__._check_fiscalyear_lock_date + env["account.move"].__class__._check_fiscalyear_lock_date = lambda self: None # Recompute several fields (always_tax_exigible, amount_residual, # amount_residual_signed, amount_untaxed, amount_untaxed_signed, # payment_state) for the moves associated to the expenses, as on v14 these @@ -34,6 +39,10 @@ def _fill_payment_state(env): env["account.move"].with_context(active_test=False, tracking_disable=True).search( [("line_ids.expense_id", "!=", False)] )._compute_amount() + # Enable fiscalyear_lock_date check + env["account.move"].__class__._check_fiscalyear_lock_date = ( + _check_fiscalyear_lock_date + ) @openupgrade.migrate() From db272522ab6f867baf3d63220ab7860d6dd61069 Mon Sep 17 00:00:00 2001 From: sergiocorato Date: Mon, 18 May 2026 07:09:55 +0200 Subject: [PATCH 06/15] [15.0][OU-FIX] revert merge into stock of stock_orderpoint_manual_procurement --- openupgrade_scripts/apriori.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openupgrade_scripts/apriori.py b/openupgrade_scripts/apriori.py index 564c313c43d2..f5dc9a322aad 100644 --- a/openupgrade_scripts/apriori.py +++ b/openupgrade_scripts/apriori.py @@ -109,7 +109,6 @@ "stock_inventory_valuation_pivot": "stock_account", # OCA/stock-logistics-warehouse "stock_inventory_exclude_sublocation": "stock", - "stock_orderpoint_manual_procurement": "stock", "stock_putaway_method": "stock_putaway_hook", # OCA/stock-logistics-workflow "stock_deferred_assign": "stock", From 00451f1cb02b1f8bd09387fc4c16316717fbe830 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Mon, 6 Jul 2026 09:59:08 +0200 Subject: [PATCH 07/15] [OU-FIX] hr_expense: also disable reconciliation check during amount recompute The payment_state rework picked from OCA#4577 rebuilt _fill_payment_state keeping only the fiscalyear_lock_date bypass, dropping the reconciliation bypass that 4ac3a169 had introduced for the same _compute_amount() pass. On databases where a reconciled expense move's recomputed amount_total disagrees with its stored line balances, the recompute fires _inverse_amount_total -> write() on reconciled lines -> UserError, aborting the whole 14->15 upgrade. Restore the upstream-merged double bypass. --- .../scripts/hr_expense/15.0.2.0/post-migration.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py b/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py index 35f1c12ace23..88639dd2408a 100644 --- a/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py +++ b/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py @@ -25,11 +25,16 @@ def _fill_payment_state(env): aat.type = 'payable' """, ) - # Disable fiscalyear_lock_date check + # Disable fiscalyear_lock_date and reconciliation checks: the recompute + # below writes on posted expense moves, and on reconciled ones the + # amount_total inverse would otherwise raise "You cannot do this + # modification on a reconciled journal entry" _check_fiscalyear_lock_date = env[ "account.move" ].__class__._check_fiscalyear_lock_date + _check_reconciliation = env["account.move.line"].__class__._check_reconciliation env["account.move"].__class__._check_fiscalyear_lock_date = lambda self: None + env["account.move.line"].__class__._check_reconciliation = lambda self: None # Recompute several fields (always_tax_exigible, amount_residual, # amount_residual_signed, amount_untaxed, amount_untaxed_signed, # payment_state) for the moves associated to the expenses, as on v14 these @@ -39,10 +44,11 @@ def _fill_payment_state(env): env["account.move"].with_context(active_test=False, tracking_disable=True).search( [("line_ids.expense_id", "!=", False)] )._compute_amount() - # Enable fiscalyear_lock_date check + # Enable fiscalyear_lock_date and reconciliation checks env["account.move"].__class__._check_fiscalyear_lock_date = ( _check_fiscalyear_lock_date ) + env["account.move.line"].__class__._check_reconciliation = _check_reconciliation @openupgrade.migrate() From 7ad0cc83aa4a821e201c11664ea1abc57b30bb33 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Mon, 6 Jul 2026 12:17:34 +0200 Subject: [PATCH 08/15] [OU-ADD] ptplus_accounting: pre-create l10n_pt_account_expected_balance The 15.0.5.0.0 vendor upgrade adds this stored computed field on account.move.line. On a large database the ORM's column-creation mass compute ("Storing computed values") loads millions of rows into the cache on top of ~300 modules' worth of upgrade state and the process is OOM-killed. Pre-creating the column in a pre-migration makes _auto_init skip the mass init (the openupgradelib.add_fields mechanism); the field is recomputed in controlled batches at the target version. --- .../15.0.5.0.0/pre-migration.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/pre-migration.py diff --git a/openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/pre-migration.py b/openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/pre-migration.py new file mode 100644 index 000000000000..b3816f5895bd --- /dev/null +++ b/openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/pre-migration.py @@ -0,0 +1,17 @@ +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + # The 15.0.5.0.0 upgrade adds the stored computed field + # account_move_line.l10n_pt_account_expected_balance. Creating the column + # here makes the ORM skip its whole-table "Storing computed values" pass + # (millions of rows), which cannot fit in memory during `-u all`. The + # field is recomputed in controlled batches at the target version. + openupgrade.logged_query( + env.cr, + """ + ALTER TABLE account_move_line + ADD COLUMN IF NOT EXISTS l10n_pt_account_expected_balance numeric + """, + ) From e4b717062d84e98c6938aeb19d81eb869d5e7a42 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Mon, 6 Jul 2026 12:17:34 +0200 Subject: [PATCH 09/15] [OU-ADD] ptplus_stock: pre-create l10n_pt_is_national The 15.0.5.0.0 vendor upgrade adds this stored computed field on stock.picking. Same mass-compute OOM as ptplus_accounting's l10n_pt_account_expected_balance: pre-creating the column makes the ORM skip the whole-table init; the field is recomputed in controlled batches at the target version. --- .../ptplus_stock/15.0.5.0.0/pre-migration.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 openupgrade_scripts/scripts/ptplus_stock/15.0.5.0.0/pre-migration.py diff --git a/openupgrade_scripts/scripts/ptplus_stock/15.0.5.0.0/pre-migration.py b/openupgrade_scripts/scripts/ptplus_stock/15.0.5.0.0/pre-migration.py new file mode 100644 index 000000000000..39f18b6e7f2a --- /dev/null +++ b/openupgrade_scripts/scripts/ptplus_stock/15.0.5.0.0/pre-migration.py @@ -0,0 +1,17 @@ +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + # The 15.0.5.0.0 upgrade adds the stored computed field + # stock_picking.l10n_pt_is_national. Creating the column here makes the + # ORM skip its whole-table "Storing computed values" pass (millions of + # rows), which cannot fit in memory during `-u all`. The field is + # recomputed in controlled batches at the target version. + openupgrade.logged_query( + env.cr, + """ + ALTER TABLE stock_picking + ADD COLUMN IF NOT EXISTS l10n_pt_is_national boolean + """, + ) From e6a85dfa86be3b4b282d5f232112d28dd052c92d Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Mon, 6 Jul 2026 12:24:16 +0200 Subject: [PATCH 10/15] [OU-FIX] ptplus_accounting: drop stale vat_adjustment_norm_id duplicate before rename Databases that already carry l10n_pt_vat_adjustment_norm_id alongside the old vat_adjustment_norm_id duplicate crash the module's own pre-migration: its openupgrade.rename_fields() finds the target column already present. Drop the old duplicate first, guarded to abort if it holds any data. The pre-00- basename makes it run before the module's own pre-migration.py (same-version stage scripts execute in basename order). --- .../pre-00-dedup-vat-adjustment-norm.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/pre-00-dedup-vat-adjustment-norm.py diff --git a/openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/pre-00-dedup-vat-adjustment-norm.py b/openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/pre-00-dedup-vat-adjustment-norm.py new file mode 100644 index 000000000000..22fd3a7dcea9 --- /dev/null +++ b/openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/pre-00-dedup-vat-adjustment-norm.py @@ -0,0 +1,53 @@ +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + # Some 14.0 databases already carry l10n_pt_vat_adjustment_norm_id (the + # 15.0 target name) while the old vat_adjustment_norm_id duplicate also + # exists on account_move. The module's own pre-migration then calls + # openupgrade.rename_fields() old -> new and fails because the target + # exists. Drop the old duplicate first, but only when it is provably + # empty. Named pre-00-* so it sorts — and therefore runs — before the + # module's own pre-migration.py (same-version stage scripts execute in + # basename order). + cr = env.cr + if not ( + openupgrade.column_exists(cr, "account_move", "vat_adjustment_norm_id") + and openupgrade.column_exists( + cr, "account_move", "l10n_pt_vat_adjustment_norm_id" + ) + ): + return + cr.execute( + "SELECT count(*) FROM account_move WHERE vat_adjustment_norm_id IS NOT NULL" + ) + old_non_null = cr.fetchone()[0] + if old_non_null: + raise RuntimeError( + "Refusing to drop account_move.vat_adjustment_norm_id: " + "%s non-null rows" % old_non_null + ) + openupgrade.logged_query( + cr, + """ + DELETE FROM ir_model_data + WHERE model = 'ir.model.fields' + AND res_id IN ( + SELECT id FROM ir_model_fields + WHERE model = 'account.move' + AND name = 'vat_adjustment_norm_id' + ) + """, + ) + openupgrade.logged_query( + cr, + """ + DELETE FROM ir_model_fields + WHERE model = 'account.move' + AND name = 'vat_adjustment_norm_id' + """, + ) + openupgrade.logged_query( + cr, "ALTER TABLE account_move DROP COLUMN vat_adjustment_norm_id" + ) From 2bf6b8a958bc01df92f78c5b1acfe8b6e05a37ab Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Mon, 6 Jul 2026 12:58:47 +0200 Subject: [PATCH 11/15] [OU-ADD] ptplus_accounting: batch-recompute l10n_pt_account_expected_balance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fill the field pre-created empty by pre-migration.py, in memory-safe chunks using the model's own compute via the ORM (add_to_compute + flush), so the vendor's real compute logic runs without ever reading the packaged code. Runs in post-migration at 15.0.5.0.0, so the field is created AND filled within this same hop — no cross-version dependency, each hop self-contained. --- ...ompute-l10n-pt-account-expected-balance.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/post-recompute-l10n-pt-account-expected-balance.py diff --git a/openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/post-recompute-l10n-pt-account-expected-balance.py b/openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/post-recompute-l10n-pt-account-expected-balance.py new file mode 100644 index 000000000000..96641d3c8034 --- /dev/null +++ b/openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/post-recompute-l10n-pt-account-expected-balance.py @@ -0,0 +1,44 @@ +import logging + +from openupgradelib import openupgrade + +_logger = logging.getLogger(__name__) + +# Kept small so each browse+compute+flush cycle stays within the container +# memory budget: the cache is dropped between chunks (invalidate_cache), so +# peak memory is one chunk, not the whole table. +CHUNK = 5000 + + +def _batched_recompute(env, model_name, fname): + """Recompute one stored computed field over the whole table, in + memory-safe chunks, using the model's OWN compute method via the ORM. + + The column was pre-created empty in pre-migration to skip the ORM's + whole-table mass init (which OOM-kills the upgrade). Here we fill it: the + compute is triggered through the ORM (add_to_compute + flush), so the + vendor's real compute logic runs — we never need to read or reimplement + it. Self-contained: the field is created AND filled within this same hop. + """ + model = env[model_name].with_context(active_test=False, prefetch_fields=False) + field = model._fields[fname] + env.cr.execute('SELECT id FROM "%s" ORDER BY id' % model._table) + ids = [row[0] for row in env.cr.fetchall()] + total = len(ids) + _logger.info( + "recompute %s.%s over %s rows (chunk %s)", model_name, fname, total, CHUNK + ) + for start in range(0, total, CHUNK): + recs = model.browse(ids[start : start + CHUNK]) + env.add_to_compute(field, recs) + recs.flush([fname], recs) + env.cr.commit() + recs.invalidate_cache() + if start and start % (CHUNK * 20) == 0: + _logger.info(" ... %s/%s", start, total) + _logger.info("recompute %s.%s done", model_name, fname) + + +@openupgrade.migrate() +def migrate(env, version): + _batched_recompute(env, "account.move.line", "l10n_pt_account_expected_balance") From b30a1dda2b38612b543ff31777d653b5618556dd Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Mon, 6 Jul 2026 12:58:47 +0200 Subject: [PATCH 12/15] [OU-ADD] ptplus_stock: batch-recompute l10n_pt_is_national MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fill the field pre-created empty by pre-migration.py, in memory-safe chunks using the model's own compute via the ORM (add_to_compute + flush), so the vendor's real compute logic runs without ever reading the packaged code. Runs in post-migration at 15.0.5.0.0, so the field is created AND filled within this same hop — no cross-version dependency, each hop self-contained. --- .../post-recompute-l10n-pt-is-national.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 openupgrade_scripts/scripts/ptplus_stock/15.0.5.0.0/post-recompute-l10n-pt-is-national.py diff --git a/openupgrade_scripts/scripts/ptplus_stock/15.0.5.0.0/post-recompute-l10n-pt-is-national.py b/openupgrade_scripts/scripts/ptplus_stock/15.0.5.0.0/post-recompute-l10n-pt-is-national.py new file mode 100644 index 000000000000..fefe2fb32bd3 --- /dev/null +++ b/openupgrade_scripts/scripts/ptplus_stock/15.0.5.0.0/post-recompute-l10n-pt-is-national.py @@ -0,0 +1,44 @@ +import logging + +from openupgradelib import openupgrade + +_logger = logging.getLogger(__name__) + +# Kept small so each browse+compute+flush cycle stays within the container +# memory budget: the cache is dropped between chunks (invalidate_cache), so +# peak memory is one chunk, not the whole table. +CHUNK = 5000 + + +def _batched_recompute(env, model_name, fname): + """Recompute one stored computed field over the whole table, in + memory-safe chunks, using the model's OWN compute method via the ORM. + + The column was pre-created empty in pre-migration to skip the ORM's + whole-table mass init (which OOM-kills the upgrade). Here we fill it: the + compute is triggered through the ORM (add_to_compute + flush), so the + vendor's real compute logic runs — we never need to read or reimplement + it. Self-contained: the field is created AND filled within this same hop. + """ + model = env[model_name].with_context(active_test=False, prefetch_fields=False) + field = model._fields[fname] + env.cr.execute('SELECT id FROM "%s" ORDER BY id' % model._table) + ids = [row[0] for row in env.cr.fetchall()] + total = len(ids) + _logger.info( + "recompute %s.%s over %s rows (chunk %s)", model_name, fname, total, CHUNK + ) + for start in range(0, total, CHUNK): + recs = model.browse(ids[start : start + CHUNK]) + env.add_to_compute(field, recs) + recs.flush([fname], recs) + env.cr.commit() + recs.invalidate_cache() + if start and start % (CHUNK * 20) == 0: + _logger.info(" ... %s/%s", start, total) + _logger.info("recompute %s.%s done", model_name, fname) + + +@openupgrade.migrate() +def migrate(env, version): + _batched_recompute(env, "stock.picking", "l10n_pt_is_national") From 31e1cd9230a9e6f87d66e2aac834c638fe1c1742 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Mon, 6 Jul 2026 13:57:18 +0200 Subject: [PATCH 13/15] [OU-FIX] hr_expense: recompute amounts under env.protecting, drop reconciliation bypass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The double-bypass approach (4ac3a169, restored in 00451f1c) lets the bare _compute_amount() call fire _inverse_amount_total through Field.__set__ -> write(): on reconciled 2-line expense moves whose recomputed amount_total is 0, the inverse rewrites both lines' debit/credit to zero — silent data corruption on reconciled entries (caught by a pre/post CSV audit: 828 mutated lines on a real 14.0 database). The reconciliation check that 4ac3a169 disables was the guard refusing exactly that write. Run the compute under env.protecting() instead: assignments land in the cache, flush() persists them by SQL, the inverse never fires, and no lock-date or reconciliation check is triggered — so the fiscalyear bypass and the reconciliation bypass both become unnecessary. Amounts follow the lines, never the other way around. --- .../hr_expense/15.0.2.0/post-migration.py | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py b/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py index 88639dd2408a..39417f359722 100644 --- a/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py +++ b/openupgrade_scripts/scripts/hr_expense/15.0.2.0/post-migration.py @@ -25,30 +25,32 @@ def _fill_payment_state(env): aat.type = 'payable' """, ) - # Disable fiscalyear_lock_date and reconciliation checks: the recompute - # below writes on posted expense moves, and on reconciled ones the - # amount_total inverse would otherwise raise "You cannot do this - # modification on a reconciled journal entry" - _check_fiscalyear_lock_date = env[ - "account.move" - ].__class__._check_fiscalyear_lock_date - _check_reconciliation = env["account.move.line"].__class__._check_reconciliation - env["account.move"].__class__._check_fiscalyear_lock_date = lambda self: None - env["account.move.line"].__class__._check_reconciliation = lambda self: None # Recompute several fields (always_tax_exigible, amount_residual, # amount_residual_signed, amount_untaxed, amount_untaxed_signed, # payment_state) for the moves associated to the expenses, as on v14 these # ones were not computed being of type `entry`, which changes now on v15 # if the method `_payment_state_matters` returns True, which is the case - # for the expense moves - env["account.move"].with_context(active_test=False, tracking_disable=True).search( + # for the expense moves. + # + # The compute MUST run under env.protecting(): called bare, every + # assignment inside _compute_amount() goes through Field.__set__ -> + # write(), which triggers _inverse_amount_total and REWRITES the + # debit/credit of reconciled 2-line expense moves (zeroing them when the + # recomputed amount_total is 0). Disabling the reconciliation check (the + # 4ac3a169 approach) silently persists that corruption instead of + # crashing on it. Protected, the assignments land in the cache and + # flush() persists them by SQL: amounts follow the lines, never the + # other way around, and no lock-date or reconciliation check fires. + AccountMove = env["account.move"] + moves = AccountMove.with_context(active_test=False, tracking_disable=True).search( [("line_ids.expense_id", "!=", False)] - )._compute_amount() - # Enable fiscalyear_lock_date and reconciliation checks - env["account.move"].__class__._check_fiscalyear_lock_date = ( - _check_fiscalyear_lock_date ) - env["account.move.line"].__class__._check_reconciliation = _check_reconciliation + fields_amount = [ + f for f in AccountMove._fields.values() if f.compute == "_compute_amount" + ] + with env.protecting(fields_amount, moves): + moves._compute_amount() + AccountMove.flush([f.name for f in fields_amount], moves) @openupgrade.migrate() From 9deb976beb16e4d2810f05dc6aa27702fe960e00 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Mon, 6 Jul 2026 14:30:20 +0200 Subject: [PATCH 14/15] [OU-FIX] ptplus_accounting: no commit inside the batched recompute @openupgrade.migrate() wraps the script in a savepoint: a cr.commit() per chunk destroys it (RELEASE SAVEPOINT crashes on script exit, aborting the whole upgrade at the finish line) and breaks per-module transactionality. Memory stays bounded by the per-chunk cache invalidation alone; the recompute measured ~3 minutes for 2.28M rows. --- .../post-recompute-l10n-pt-account-expected-balance.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/post-recompute-l10n-pt-account-expected-balance.py b/openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/post-recompute-l10n-pt-account-expected-balance.py index 96641d3c8034..a9c848b10082 100644 --- a/openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/post-recompute-l10n-pt-account-expected-balance.py +++ b/openupgrade_scripts/scripts/ptplus_accounting/15.0.5.0.0/post-recompute-l10n-pt-account-expected-balance.py @@ -6,7 +6,10 @@ # Kept small so each browse+compute+flush cycle stays within the container # memory budget: the cache is dropped between chunks (invalidate_cache), so -# peak memory is one chunk, not the whole table. +# peak memory is one chunk, not the whole table. No commit inside the loop: +# @openupgrade.migrate() wraps the script in a savepoint (a commit would +# destroy it -> RELEASE SAVEPOINT crashes on exit) and the module upgrade +# must stay transactional; flushed rows live in the module's transaction. CHUNK = 5000 @@ -32,7 +35,6 @@ def _batched_recompute(env, model_name, fname): recs = model.browse(ids[start : start + CHUNK]) env.add_to_compute(field, recs) recs.flush([fname], recs) - env.cr.commit() recs.invalidate_cache() if start and start % (CHUNK * 20) == 0: _logger.info(" ... %s/%s", start, total) From ed1e1ae2c7863d508c50dedcffd1ae695446f5cb Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Mon, 6 Jul 2026 14:30:20 +0200 Subject: [PATCH 15/15] [OU-FIX] ptplus_stock: no commit inside the batched recompute Same savepoint/transactionality fix as ptplus_accounting: the decorator wraps the script in a savepoint that a mid-loop commit destroys. Cache invalidation per chunk alone keeps memory bounded. --- .../15.0.5.0.0/post-recompute-l10n-pt-is-national.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openupgrade_scripts/scripts/ptplus_stock/15.0.5.0.0/post-recompute-l10n-pt-is-national.py b/openupgrade_scripts/scripts/ptplus_stock/15.0.5.0.0/post-recompute-l10n-pt-is-national.py index fefe2fb32bd3..4cca928993c4 100644 --- a/openupgrade_scripts/scripts/ptplus_stock/15.0.5.0.0/post-recompute-l10n-pt-is-national.py +++ b/openupgrade_scripts/scripts/ptplus_stock/15.0.5.0.0/post-recompute-l10n-pt-is-national.py @@ -6,7 +6,10 @@ # Kept small so each browse+compute+flush cycle stays within the container # memory budget: the cache is dropped between chunks (invalidate_cache), so -# peak memory is one chunk, not the whole table. +# peak memory is one chunk, not the whole table. No commit inside the loop: +# @openupgrade.migrate() wraps the script in a savepoint (a commit would +# destroy it -> RELEASE SAVEPOINT crashes on exit) and the module upgrade +# must stay transactional; flushed rows live in the module's transaction. CHUNK = 5000 @@ -32,7 +35,6 @@ def _batched_recompute(env, model_name, fname): recs = model.browse(ids[start : start + CHUNK]) env.add_to_compute(field, recs) recs.flush([fname], recs) - env.cr.commit() recs.invalidate_cache() if start and start % (CHUNK * 20) == 0: _logger.info(" ... %s/%s", start, total)