From e0e7e3b832f3f92090ca698308bf95d3a877cf4e Mon Sep 17 00:00:00 2001 From: "Guillaume (guva)" Date: Tue, 4 Apr 2023 08:24:13 +0000 Subject: [PATCH] [FIX] account: payment_state reversed/paid bill When partially reversing a bill and paying the difference, the payment state should be 'paid'. In the particular case of having the bank account as manual outstanding payment account on the bank journal, payment state was 'reversed'. opw-3235107 closes odoo/odoo#120967 X-original-commit: eb0baf273980957b522e3e41b5d371ba081c915d Signed-off-by: Laurent Smet Co-authored-by: Habib Ayob & Laurent Smet --- addons/account/models/account_move.py | 49 +++--- .../tests/test_account_move_in_invoice.py | 140 ++++++++++++++++++ 2 files changed, 165 insertions(+), 24 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index eb1e5d55588..f431a621845 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -44,8 +44,8 @@ TYPE_REVERSE_MAP = { 'out_refund': 'entry', 'in_invoice': 'in_refund', 'in_refund': 'entry', - 'out_receipt': 'entry', - 'in_receipt': 'entry', + 'out_receipt': 'out_refund', + 'in_receipt': 'in_refund', } EMPTY = object() @@ -939,12 +939,10 @@ class AccountMove(models.Model): source_line.id AS source_line_id, source_line.move_id AS source_move_id, account.account_type AS source_line_account_type, - ARRAY_AGG(counterpart_move.reversed_entry_id) - FILTER (WHERE counterpart_move.reversed_entry_id IS NOT NULL) AS counterpart_reversed_entry_ids, - ARRAY_AGG(counterpart_move.move_type) - FILTER (WHERE counterpart_move.reversed_entry_id IS NOT NULL) AS counterpart_move_types, + ARRAY_AGG(counterpart_move.move_type) AS counterpart_move_types, COALESCE(BOOL_AND(COALESCE(pay.is_matched, FALSE)) - FILTER (WHERE counterpart_move.payment_id IS NOT NULL), TRUE) AS all_payments_matched + FILTER (WHERE counterpart_move.payment_id IS NOT NULL), TRUE) AS all_payments_matched, + BOOL_OR(COALESCE(BOOL(pay.id), FALSE)) as has_payment FROM account_partial_reconcile part JOIN account_move_line source_line ON source_line.id = part.{source_field}_move_id JOIN account_account account ON account.id = source_line.account_id @@ -987,27 +985,30 @@ class AccountMove(models.Model): if payment_state_matters: if currency.is_zero(invoice.amount_residual): - # Check if the invoice/expense entry is fully paid or 'in_payment'. - if all(x['all_payments_matched'] for x in reconciliation_vals): - new_pmt_state = 'paid' + if any(x['has_payment'] for x in reconciliation_vals): + + # Check if the invoice/expense entry is fully paid or 'in_payment'. + if all(x['all_payments_matched'] for x in reconciliation_vals): + new_pmt_state = 'paid' + else: + new_pmt_state = invoice._get_invoice_in_payment_state() + else: - new_pmt_state = invoice._get_invoice_in_payment_state() + new_pmt_state = 'paid' + + reverse_move_types = set() + for x in reconciliation_vals: + for move_type in x['counterpart_move_types']: + reverse_move_types.add(move_type) + + if (invoice.move_type in ('in_invoice', 'in_receipt') and reverse_move_types == {'in_refund'}) \ + or (invoice.move_type in ('out_invoice', 'out_receipt') and reverse_move_types == {'out_refund'}) \ + or (invoice.move_type in ('entry', 'out_refund', 'in_refund') and reverse_move_types == {'entry'}): + new_pmt_state = 'reversed' + elif reconciliation_vals: new_pmt_state = 'partial' - # Check if the journal entry is 'reversed' (1 on 1 full reconciliation with entries being of the opposite types) - if new_pmt_state == 'paid': - reverse_move_types = [] - for x in reconciliation_vals: - for rec_move_type, rec_reversed_entry_id in zip(x['counterpart_move_types'] or [], x['counterpart_reversed_entry_ids'] or []): - if rec_reversed_entry_id == invoice.id: - reverse_move_types.append(rec_move_type) - - if (invoice.move_type in ('in_invoice', 'in_receipt') and reverse_move_types == ['in_refund']) \ - or (invoice.move_type in ('out_invoice', 'out_receipt') and reverse_move_types == ['out_refund']) \ - or (invoice.move_type in ('entry', 'out_refund', 'in_refund') and reverse_move_types == ['entry']): - new_pmt_state = 'reversed' - invoice.payment_state = new_pmt_state @api.depends('invoice_payment_term_id', 'invoice_date', 'currency_id', 'amount_total_in_currency_signed', 'invoice_date_due') diff --git a/addons/account/tests/test_account_move_in_invoice.py b/addons/account/tests/test_account_move_in_invoice.py index a832ebfe351..d129c300381 100644 --- a/addons/account/tests/test_account_move_in_invoice.py +++ b/addons/account/tests/test_account_move_in_invoice.py @@ -2131,3 +2131,143 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): move_form.invoice_date = fields.Date.from_string('2022-05-06') move = move_form.save() self.assertEqual(move.invoice_date.strftime('%Y-%m-%d'), '2022-05-06') + + def _assert_payment_move_state(self, move_type, amount, counterpart_values_list, payment_state): + def create_move(move_type, amount, account=None): + move_vals = { + 'move_type': move_type, + 'date': '2020-01-10', + } + if move_type in self.env['account.move'].get_sale_types(include_receipts=True) + self.env['account.move'].get_purchase_types(include_receipts=True): + move_vals.update({ + 'partner_id': self.partner_a.id, + 'invoice_date': '2020-01-10', + 'invoice_line_ids': [Command.create({'product_id': self.product_a.id, 'price_unit': amount, 'tax_ids': []})], + }) + else: + if amount > 0.0: + debit_account = account or self.company_data['default_account_receivable'] + credit_account = self.company_data['default_account_revenue'] + debit_balance = amount + else: + credit_account = account or self.company_data['default_account_receivable'] + debit_account = self.company_data['default_account_revenue'] + debit_balance = -amount + move_vals['line_ids'] = [ + Command.create({ + 'name': "line1", + 'account_id': debit_account.id, + 'balance': debit_balance, + }), + Command.create({ + 'name': "line2", + 'account_id': credit_account.id, + 'balance': -debit_balance, + }), + ] + move = self.env['account.move'].create(move_vals) + move.action_post() + return move + + def create_payment(move, amount): + self.env['account.payment.register']\ + .with_context(active_ids=move.ids, active_model='account.move')\ + .create({ + 'amount': amount, + })\ + ._create_payments() + + def create_reverse(move, amount): + move_reversal = self.env['account.move.reversal']\ + .with_context(active_model='account.move', active_ids=move.ids)\ + .create({ + 'reason': 'no reason', + 'refund_method': 'refund', + 'journal_id': move.journal_id.id, + }) + reversal = move_reversal.reverse_moves() + reverse_move = self.env['account.move'].browse(reversal['res_id']) + if reverse_move.move_type in ('out_refund', 'in_refund'): + reverse_move.write({ + 'invoice_line_ids': [ + Command.update(reverse_move.invoice_line_ids.id, {'price_unit': amount}), + ], + }) + else: + line = move.line_ids.filtered(lambda line: line.account_type in ('asset_receivable', 'liability_payable')) + reverse_move.write({ + 'line_ids': [ + Command.update(line.id, {'balance': amount}), + ], + }) + + reverse_move.action_post() + (move + reverse_move).line_ids\ + .filtered(lambda line: line.account_type in ('asset_receivable', 'liability_payable'))\ + .reconcile() + + move = create_move(move_type, amount) + line = move.line_ids.filtered(lambda line: line.account_type in ('asset_receivable', 'liability_payable')) + for counterpart_move_type, counterpart_amount in counterpart_values_list: + if counterpart_move_type == 'payment': + create_payment(move, counterpart_amount) + elif counterpart_move_type == 'reverse': + create_reverse(move, counterpart_amount) + else: + counterpart_move = create_move(counterpart_move_type, counterpart_amount, account=line.account_id) + counterpart_line = counterpart_move.line_ids.filtered(lambda x: x.account_id == line.account_id) + (line + counterpart_line).reconcile() + + if payment_state == 'in_payment' and move._get_invoice_in_payment_state() == 'paid': + payment_state = 'paid' + + self.assertRecordValues(move, [{'payment_state': payment_state}]) + + def test_payment_move_state(self): + for move_type, amount, counterpart_values_list, payment_state in ( + ('out_invoice', 1000.0, [('out_refund', 1000.0)], 'reversed'), + ('out_invoice', 1000.0, [('out_refund', 500.0), ('out_refund', 500.0)], 'reversed'), + ('out_invoice', 1000.0, [('reverse', 1000.0)], 'reversed'), + ('out_receipt', 1000.0, [('out_refund', 1000.0)], 'reversed'), + ('out_receipt', 1000.0, [('out_refund', 500.0), ('out_refund', 500.0)], 'reversed'), + ('out_receipt', 1000.0, [('reverse', 1000.0)], 'reversed'), + ('out_refund', 1000.0, [('reverse', -1000.0)], 'reversed'), + ('in_invoice', 1000.0, [('in_refund', 1000.0)], 'reversed'), + ('in_invoice', 1000.0, [('in_refund', 500.0), ('in_refund', 500.0)], 'reversed'), + ('in_invoice', 1000.0, [('reverse', 1000.0)], 'reversed'), + ('in_receipt', 1000.0, [('in_refund', 1000.0)], 'reversed'), + ('in_receipt', 1000.0, [('in_refund', 500.0), ('in_refund', 500.0)], 'reversed'), + ('in_receipt', 1000.0, [('reverse', 1000.0)], 'reversed'), + ('in_refund', 1000.0, [('reverse', 1000.0)], 'reversed'), + ('entry', 1000.0, [('entry', -1000.0)], 'not_paid'), + ('entry', 1000.0, [('reverse', 1000.0)], 'not_paid'), + + ('out_invoice', 1000.0, [('payment', 500.0)], 'partial'), + ('out_invoice', 1000.0, [('payment', 1000.0)], 'in_payment'), + ('out_receipt', 1000.0, [('payment', 500.0)], 'partial'), + ('out_receipt', 1000.0, [('payment', 1000.0)], 'in_payment'), + ('out_refund', 1000.0, [('payment', 500.0)], 'partial'), + ('out_refund', 1000.0, [('payment', 1000.0)], 'in_payment'), + ('in_invoice', 1000.0, [('payment', 500.0)], 'partial'), + ('in_invoice', 1000.0, [('payment', 1000.0)], 'in_payment'), + ('in_receipt', 1000.0, [('payment', 500.0)], 'partial'), + ('in_receipt', 1000.0, [('payment', 1000.0)], 'in_payment'), + ('in_refund', 1000.0, [('payment', 500.0)], 'partial'), + ('in_refund', 1000.0, [('payment', 1000.0)], 'in_payment'), + ('entry', 1000.0, [('payment', 500.0)], 'not_paid'), + ('entry', 1000.0, [('payment', 1000.0)], 'not_paid'), + + ('out_invoice', 1000.0, [('out_refund', 500.0), ('payment', 500.0)], 'in_payment'), + ('out_invoice', 1000.0, [('out_refund', 500.0), ('payment', 400.0)], 'partial'), + ('out_invoice', 1000.0, [('entry', -1000.0)], 'paid'), + ('in_invoice', 1000.0, [('in_refund', 500.0), ('payment', 500.0)], 'in_payment'), + ('in_invoice', 1000.0, [('in_refund', 500.0), ('payment', 400.0)], 'partial'), + ('in_invoice', 1000.0, [('entry', 1000.0)], 'paid'), + ): + with self.subTest( + move_type=move_type, + amount=amount, + counterpart_values_list=counterpart_values_list, + payment_state=payment_state, + ): + self._assert_payment_move_state(move_type, amount, counterpart_values_list, payment_state)