[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 <las@odoo.com>
Co-authored-by: Habib Ayob <ayh@odoo.com> & Laurent Smet <las@odoo.com>
This commit is contained in:
Guillaume (guva)
2023-05-10 08:23:47 +02:00
committed by Laurent Smet
co-authored by Habib Ayob <ayh@odoo.com> & Laurent Smet <las@odoo.com>
parent 5ddd39b5b7
commit e0e7e3b832
2 changed files with 165 additions and 24 deletions
+25 -24
View File
@@ -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')
@@ -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)