[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:
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user