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)