summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/account/tests/test_account_payment_register.py271
-rw-r--r--addons/account/wizard/account_payment_register.py43
2 files changed, 310 insertions, 4 deletions
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 f5fc12b11d2..3fc91f716ad 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)]