From 0863af4de88d1ac7d27e1a74794039e0a1c05e10 Mon Sep 17 00:00:00 2001 From: hugues de keyzer Date: Wed, 2 Oct 2024 17:43:42 +0200 Subject: [PATCH 1/4] [OCA-CHERRY-PICK OCA#4577] [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 ec52eb320fc2b694b7cafccd34c9a3475d020c48 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 2/4] [OCA-CHERRY-PICK OCA#4577] [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 8ecca7f644df09f90338cf69729bb9f622bb8fbc Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Mon, 6 Jul 2026 09:59:08 +0200 Subject: [PATCH 3/4] [OCA-CANDIDATE] [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 9cdb7a689758a5a3e64cba8d1879261299692bca Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Mon, 6 Jul 2026 13:57:18 +0200 Subject: [PATCH 4/4] [OCA-CANDIDATE] [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()