[FIX] account: mark invoice as paid when paying all in foreign currency

Activate multicurrency:
* EUR with rate 1.0
* USD with rate 100.0
Have a company in currency USD
Create an invoice (in USD)
* invoice line of 12.15, no tax
Save, post and register payment
In the payment widget change currency to EUR.
The amount will be automatically converted in 0.12
Register payment

Invoice will be still marked to paid as the amount paid is 12.0.
This occur because the system does not check if the amount to be paid,
expressed in payment currency, is what has been paid.

opw-2376336

closes odoo/odoo#62238

X-original-commit: 967a2ab323e273c853aee0edd5ce404f4d14e6ab
Signed-off-by: Laurent Smet <smetl@users.noreply.github.com>
This commit is contained in:
Laurent Smet
2020-11-24 11:56:10 +00:00
parent 3a918898e8
commit f5c1e09772
2 changed files with 310 additions and 4 deletions
@@ -10,6 +10,13 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.currency_data_3 = cls.setup_multi_currency_data({
'name': "Umbrella",
'symbol': '☂',
'currency_unit_label': "Umbrella",
'currency_subunit_label': "Broken Umbrella",
}, rate2017=0.01)
cls.payment_debit_account_id = cls.company_data['default_journal_bank'].payment_debit_account_id.copy()
cls.payment_credit_account_id = cls.company_data['default_journal_bank'].payment_credit_account_id.copy()
@@ -51,7 +58,6 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 1000.0})],
})
cls.out_invoice_1.action_post()
cls.out_invoice_2 = cls.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2017-01-01',
@@ -60,7 +66,21 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 2000.0})],
})
(cls.out_invoice_1 + cls.out_invoice_2).action_post()
cls.out_invoice_3 = cls.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': cls.partner_a.id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 12.01})],
})
cls.out_invoice_4 = cls.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': cls.partner_a.id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 11.99})],
})
(cls.out_invoice_1 + cls.out_invoice_2 + cls.out_invoice_3 + cls.out_invoice_4).action_post()
# Vendor bills, in_invoice_1 + in_invoice_2 are sharing the same batch but not in_invoice_3.
cls.in_invoice_1 = cls.env['account.move'].create({
@@ -504,3 +524,250 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=self.out_invoice_2.ids)\
.create({})
def test_register_payment_multi_currency_rounding_issue_positive_delta(self):
''' When registering a payment using a different currency than the invoice one, the invoice must be fully paid
at the end whatever the currency rate.
'''
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=self.out_invoice_3.ids)\
.create({
'currency_id': self.currency_data_3['currency'].id,
'amount': 0.12,
})\
._create_payments()
self.assertRecordValues(payment.line_ids.sorted('balance'), [
# Receivable line:
{
'debit': 0.0,
'credit': 12.01,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': -0.12,
'reconciled': True,
},
# Liquidity line:
{
'debit': 12.01,
'credit': 0.0,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': 0.12,
'reconciled': False,
},
])
def test_register_payment_multi_currency_rounding_issue_negative_delta(self):
''' When registering a payment using a different currency than the invoice one, the invoice must be fully paid
at the end whatever the currency rate.
'''
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=self.out_invoice_4.ids)\
.create({
'currency_id': self.currency_data_3['currency'].id,
'amount': 0.12,
})\
._create_payments()
self.assertRecordValues(payment.line_ids.sorted('balance'), [
# Receivable line:
{
'debit': 0.0,
'credit': 11.99,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': -0.12,
'reconciled': True,
},
# Liquidity line:
{
'debit': 11.99,
'credit': 0.0,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': 0.12,
'reconciled': False,
},
])
def test_register_payment_multi_currency_rounding_issue_writeoff_lower_amount_keep_open(self):
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=self.out_invoice_3.ids)\
.create({
'currency_id': self.currency_data_3['currency'].id,
'amount': 0.08,
'payment_difference_handling': 'open',
})\
._create_payments()
self.assertRecordValues(payment.line_ids.sorted('balance'), [
# Receivable line:
{
'debit': 0.0,
'credit': 8.0,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': -0.08,
'reconciled': True,
},
# Liquidity line:
{
'debit': 8.0,
'credit': 0.0,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': 0.08,
'reconciled': False,
},
])
def test_register_payment_multi_currency_rounding_issue_writeoff_lower_amount_reconcile_positive_delta(self):
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=self.out_invoice_3.ids)\
.create({
'currency_id': self.currency_data_3['currency'].id,
'amount': 0.08,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.company_data['default_account_revenue'].id,
'writeoff_label': 'writeoff',
})\
._create_payments()
self.assertRecordValues(payment.line_ids.sorted('balance'), [
# Receivable line:
{
'debit': 0.0,
'credit': 12.01,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': -0.12,
'reconciled': True,
},
# Write-off line:
{
'debit': 4.0,
'credit': 0.0,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': 0.04,
'reconciled': False,
},
# Liquidity line:
{
'debit': 8.01,
'credit': 0.0,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': 0.08,
'reconciled': False,
},
])
def test_register_payment_multi_currency_rounding_issue_writeoff_lower_amount_reconcile_negative_delta(self):
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=self.out_invoice_4.ids)\
.create({
'currency_id': self.currency_data_3['currency'].id,
'amount': 0.08,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.company_data['default_account_revenue'].id,
'writeoff_label': 'writeoff',
})\
._create_payments()
self.assertRecordValues(payment.line_ids.sorted('balance'), [
# Receivable line:
{
'debit': 0.0,
'credit': 11.99,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': -0.12,
'reconciled': True,
},
# Write-off line:
{
'debit': 4.0,
'credit': 0.0,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': 0.04,
'reconciled': False,
},
# Liquidity line:
{
'debit': 7.99,
'credit': 0.0,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': 0.08,
'reconciled': False,
},
])
def test_register_payment_multi_currency_rounding_issue_writeoff_higher_amount_reconcile_positive_delta(self):
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=self.out_invoice_3.ids)\
.create({
'currency_id': self.currency_data_3['currency'].id,
'amount': 0.16,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.company_data['default_account_revenue'].id,
'writeoff_label': 'writeoff',
})\
._create_payments()
self.assertRecordValues(payment.line_ids.sorted('balance'), [
# Receivable line:
{
'debit': 0.0,
'credit': 12.01,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': -0.12,
'reconciled': True,
},
# Write-off line:
{
'debit': 0.0,
'credit': 4.0,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': -0.04,
'reconciled': False,
},
# Liquidity line:
{
'debit': 16.01,
'credit': 0.0,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': 0.16,
'reconciled': False,
},
])
def test_register_payment_multi_currency_rounding_issue_writeoff_higher_amount_reconcile_negative_delta(self):
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=self.out_invoice_4.ids)\
.create({
'currency_id': self.currency_data_3['currency'].id,
'amount': 0.16,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.company_data['default_account_revenue'].id,
'writeoff_label': 'writeoff',
})\
._create_payments()
self.assertRecordValues(payment.line_ids.sorted('balance'), [
# Receivable line:
{
'debit': 0.0,
'credit': 11.99,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': -0.12,
'reconciled': True,
},
# Write-off line:
{
'debit': 0.0,
'credit': 4.0,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': -0.04,
'reconciled': False,
},
# Liquidity line:
{
'debit': 15.99,
'credit': 0.0,
'currency_id': self.currency_data_3['currency'].id,
'amount_currency': 0.16,
'reconciled': False,
},
])
@@ -417,7 +417,7 @@ class AccountPaymentRegister(models.TransientModel):
'destination_account_id': self.line_ids[0].account_id.id
}
if self.payment_difference and self.payment_difference_handling == 'reconcile':
if not self.currency_id.is_zero(self.payment_difference) and self.payment_difference_handling == 'reconcile':
payment_vals['write_off_line_vals'] = {
'name': self.writeoff_label,
'amount': self.payment_difference,
@@ -444,9 +444,10 @@ class AccountPaymentRegister(models.TransientModel):
def _create_payments(self):
self.ensure_one()
batches = self._get_batches()
edit_mode = self.can_edit_wizard and (len(batches[0]['lines']) == 1 or self.group_payment)
to_reconcile = []
if self.can_edit_wizard and (len(batches[0]['lines']) == 1 or self.group_payment):
if edit_mode:
payment_vals = self._create_payment_vals_from_wizard()
payment_vals_list = [payment_vals]
to_reconcile.append(batches[0]['lines'])
@@ -468,6 +469,44 @@ class AccountPaymentRegister(models.TransientModel):
to_reconcile.append(batch_result['lines'])
payments = self.env['account.payment'].create(payment_vals_list)
# If payments are made using a currency different than the source one, ensure the balance match exactly in
# order to fully paid the source journal items.
# For example, suppose a new currency B having a rate 100:1 regarding the company currency A.
# If you try to pay 12.15A using 0.12B, the computed balance will be 12.00A for the payment instead of 12.15A.
if edit_mode:
for payment, lines in zip(payments, to_reconcile):
# Batches are made using the same currency so making 'lines.currency_id' is ok.
if payment.currency_id != lines.currency_id:
liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
source_balance = abs(sum(lines.mapped('amount_residual')))
payment_rate = liquidity_lines[0].amount_currency / liquidity_lines[0].balance
source_balance_converted = abs(source_balance) * payment_rate
# Translate the balance into the payment currency is order to be able to compare them.
# In case in both have the same value (12.15 * 0.01 ~= 0.12 in our example), it means the user
# attempt to fully paid the source lines and then, we need to manually fix them to get a perfect
# match.
payment_balance = abs(sum(counterpart_lines.mapped('balance')))
payment_amount_currency = abs(sum(counterpart_lines.mapped('amount_currency')))
if not payment.currency_id.is_zero(source_balance_converted - payment_amount_currency):
continue
delta_balance = source_balance - payment_balance
# Balance are already the same.
if self.company_currency_id.is_zero(delta_balance):
continue
# Fix the balance but make sure to peek the liquidity and counterpart lines first.
debit_lines = (liquidity_lines + counterpart_lines).filtered('debit')
credit_lines = (liquidity_lines + counterpart_lines).filtered('credit')
payment.move_id.write({'line_ids': [
(1, debit_lines[0].id, {'debit': debit_lines[0].debit + delta_balance}),
(1, credit_lines[0].id, {'credit': credit_lines[0].credit + delta_balance}),
]})
payments.action_post()
domain = [('account_internal_type', 'in', ('receivable', 'payable')), ('reconciled', '=', False)]