diff --git a/addons/account/tests/test_account_payment_register.py b/addons/account/tests/test_account_payment_register.py index 6a04bdb0bd5..023d4c243a5 100644 --- a/addons/account/tests/test_account_payment_register.py +++ b/addons/account/tests/test_account_payment_register.py @@ -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, + }, + ]) diff --git a/addons/account/wizard/account_payment_register.py b/addons/account/wizard/account_payment_register.py index df9f6875b81..2d19473ca2e 100644 --- a/addons/account/wizard/account_payment_register.py +++ b/addons/account/wizard/account_payment_register.py @@ -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)]