From 9d2875492b607693f0a0493ee15eda4fbdfd04ef Mon Sep 17 00:00:00 2001 From: aliya Date: Fri, 6 May 2022 17:05:46 +0000 Subject: [PATCH] [FIX] account: fix payment widget (exchange difference) Task 2843455 When an exchange difference is linked to a payment, not an invoice, it is not displayed in the payment widget even though an exchange difference move is created and matched with the invoice. Exchange difference should be displayed when it's linked to the payment as well. closes odoo/odoo#92069 Signed-off-by: Laurent Smet --- addons/account/models/account_move.py | 101 +++++++++++++++--- .../static/src/xml/account_payment.xml | 42 ++++---- .../test_account_move_payments_widget.py | 56 ++++++++++ .../tests/test_account_move_reconcile.py | 14 +++ 4 files changed, 181 insertions(+), 32 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 402e8d203f1..7a374f04dd7 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1648,35 +1648,30 @@ class AccountMove(models.Model): def _get_reconciled_info_JSON_values(self): self.ensure_one() reconciled_vals = [] - reconciled_partials, exchange_diff_moves = self._get_reconciled_invoices_partials() - for partial, amount, counterpart_line in reconciled_partials: + reconciled_partials = self._get_all_reconciled_invoice_partials() + for reconciled_partial in reconciled_partials: + counterpart_line = reconciled_partial['aml'] if counterpart_line.move_id.ref: reconciliation_ref = '%s (%s)' % (counterpart_line.move_id.name, counterpart_line.move_id.ref) else: reconciliation_ref = counterpart_line.move_id.name - currency = self.currency_id - is_exchange = counterpart_line.move_id.id in exchange_diff_moves - if is_exchange: - amount = counterpart_line.move_id.amount_total_signed - currency = self.company_id.currency_id - reconciled_vals.append({ 'name': counterpart_line.name, 'journal_name': counterpart_line.journal_id.name, - 'amount': amount, - 'currency': currency.symbol, - 'digits': [69, currency.decimal_places], - 'position': currency.position, + 'amount': reconciled_partial['amount'], + 'currency': reconciled_partial['currency'].symbol, + 'digits': [69, reconciled_partial['currency'].decimal_places], + 'position': reconciled_partial['currency'].position, 'date': counterpart_line.date, 'payment_id': counterpart_line.id, - 'partial_id': partial.id, + 'partial_id': reconciled_partial['partial_id'], 'account_payment_id': counterpart_line.payment_id.id, 'payment_method_name': counterpart_line.payment_id.payment_method_line_id.name, 'move_id': counterpart_line.move_id.id, 'ref': reconciliation_ref, # these are necessary for the views to change depending on the values - 'is_exchange': is_exchange, + 'is_exchange': reconciled_partial['is_exchange'], 'amount_company_currency': formatLang(self.env, abs(counterpart_line.balance), currency_obj=counterpart_line.company_id.currency_id), 'amount_foreign_currency': formatLang(self.env, abs(counterpart_line.amount_currency), currency_obj=counterpart_line.currency_id) if counterpart_line.currency_id != counterpart_line.company_id.currency_id else False }) @@ -2466,6 +2461,84 @@ class AccountMove(models.Model): reconciled_lines.mapped('matched_credit_ids.credit_move_id') return reconciled_amls.move_id.filtered(lambda move: move.is_invoice(include_receipts=True)) + def _get_all_reconciled_invoice_partials(self): + self.ensure_one() + reconciled_lines = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable')) + if not reconciled_lines: + return {} + + query = ''' + SELECT + part.id, + part.exchange_move_id, + part.debit_amount_currency AS amount, + part.credit_move_id AS counterpart_line_id + FROM account_partial_reconcile part + WHERE part.debit_move_id IN %s + + UNION ALL + + SELECT + part.id, + part.exchange_move_id, + part.credit_amount_currency AS amount, + part.debit_move_id AS counterpart_line_id + FROM account_partial_reconcile part + WHERE part.credit_move_id IN %s + ''' + self._cr.execute(query, [tuple(reconciled_lines.ids)] * 2) + + partial_values_list = [] + counterpart_line_ids = set() + exchange_move_ids = set() + for values in self._cr.dictfetchall(): + partial_values_list.append({ + 'aml_id': values['counterpart_line_id'], + 'partial_id': values['id'], + 'amount': values['amount'], + 'currency': self.currency_id, + }) + counterpart_line_ids.add(values['counterpart_line_id']) + if values['exchange_move_id']: + exchange_move_ids.add(values['exchange_move_id']) + + if exchange_move_ids: + query = ''' + SELECT + part.id, + part.credit_move_id AS counterpart_line_id + FROM account_partial_reconcile part + JOIN account_move_line credit_line ON credit_line.id = part.credit_move_id + WHERE credit_line.move_id IN %s AND part.debit_move_id IN %s + + UNION ALL + + SELECT + part.id, + part.debit_move_id AS counterpart_line_id + FROM account_partial_reconcile part + JOIN account_move_line debit_line ON debit_line.id = part.debit_move_id + WHERE debit_line.move_id IN %s AND part.credit_move_id IN %s + ''' + self._cr.execute(query, [tuple(exchange_move_ids), tuple(counterpart_line_ids)] * 2) + + for values in self._cr.dictfetchall(): + counterpart_line_ids.add(values['counterpart_line_id']) + partial_values_list.append({ + 'aml_id': values['counterpart_line_id'], + 'partial_id': values['id'], + 'currency': self.company_id.currency_id, + }) + + counterpart_lines = {x.id: x for x in self.env['account.move.line'].browse(counterpart_line_ids)} + for partial_values in partial_values_list: + partial_values['aml'] = counterpart_lines[partial_values['aml_id']] + partial_values['is_exchange'] = partial_values['aml'].move_id.id in exchange_move_ids + if partial_values['is_exchange']: + partial_values['amount'] = abs(partial_values['aml'].balance) + + return partial_values_list + def _get_reconciled_invoices_partials(self): ''' Helper to retrieve the details about reconciled invoices. :return A list of tuple (partial, amount, invoice_line). diff --git a/addons/account/static/src/xml/account_payment.xml b/addons/account/static/src/xml/account_payment.xml index 89b255858c7..17ceeb7c5e1 100644 --- a/addons/account/static/src/xml/account_payment.xml +++ b/addons/account/static/src/xml/account_payment.xml @@ -40,26 +40,32 @@ Paid on - - Exchange Difference + + + + + + + + + + + + Exchange Difference + - - - - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/addons/account/tests/test_account_move_payments_widget.py b/addons/account/tests/test_account_move_payments_widget.py index 06584e26cc8..a7ddc101fc6 100644 --- a/addons/account/tests/test_account_move_payments_widget.py +++ b/addons/account/tests/test_account_move_payments_widget.py @@ -146,3 +146,59 @@ class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon): self.assert_invoice_outstanding_to_reconcile_widget(out_invoice, expected_amounts) self.assert_invoice_outstanding_to_reconcile_widget(in_invoice, expected_amounts) + + def test_payments_with_exchange_difference_payment(self): + ''' Test the payments widget on invoices having a foreign currency that triggers an exchange difference on the payment. ''' + + # Customer invoice of 300 in GOL at exchage rate 3:1. 300 GOL -> 100 USD + out_invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'date': '2016-01-01', + 'invoice_date': '2016-01-01', + 'partner_id': self.partner_a.id, + 'currency_id': self.currency_data['currency'].id, + 'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 300})], + }) + out_invoice.action_post() + + # Payment at exchange rate 2:1. 300 GOL -> 150 USD + payment = self.env['account.payment.register']\ + .with_context(active_model='account.move', active_ids=out_invoice.ids)\ + .create({'payment_date': '2017-01-01'})\ + ._create_payments() + + expected_amounts = {payment.move_id.id: 300.0} + # Get the exchange difference move. + for ln in out_invoice.line_ids: + if ln.matched_credit_ids.exchange_move_id: + expected_amounts[ln.matched_credit_ids.exchange_move_id.id] = 50.0 + + self.assert_invoice_outstanding_reconciled_widget(out_invoice, expected_amounts) + + def test_payments_with_exchange_difference_invoice(self): + ''' Test the payments widget on invoices having a foreign currency that triggers an exchange difference on the invoice. ''' + + # Customer invoice of 300 in GOL at exchage rate 2:1. 300 GOL -> 150 USD + out_invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'date': '2017-01-01', + 'invoice_date': '2017-01-01', + 'partner_id': self.partner_a.id, + 'currency_id': self.currency_data['currency'].id, + 'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 300})], + }) + out_invoice.action_post() + + # Payment at exchange rate 3:1. 300 GOL -> 100 USD + payment = self.env['account.payment.register']\ + .with_context(active_model='account.move', active_ids=out_invoice.ids)\ + .create({'payment_date': '2016-01-01'})\ + ._create_payments() + + expected_amounts = {payment.move_id.id: 300.0} + # Get the exchange difference move. + for ln in out_invoice.line_ids: + if ln.matched_credit_ids.exchange_move_id: + expected_amounts[ln.matched_credit_ids.exchange_move_id.id] = 50.0 + + self.assert_invoice_outstanding_reconciled_widget(out_invoice, expected_amounts) diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index 19a9c0ca0ae..9209a44a8f8 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -1896,9 +1896,13 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): {'amount_residual': 1.71, 'amount_residual_currency': 0.09, 'reconciled': False}, {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, ]) + payment_exchange_id = inv2_rec_line.matched_credit_ids.filtered(lambda x: x.id != res['partials'].id) + self.assert_invoice_outstanding_reconciled_widget(inv2, { refund1.id: 28.46, pay1.move_id.id: 1907.17, + res['partials'].exchange_move_id.id: 312.54, + payment_exchange_id[0].exchange_move_id.id: 6.34, }) # 4th reconciliation inv2 + pay2 @@ -1940,10 +1944,15 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, ]) + payment_exchange_id = inv2_rec_line.matched_credit_ids.filtered(lambda x: x.id != res['partials'].id) + self.assert_invoice_outstanding_reconciled_widget(inv2, { refund1.id: 28.46, pay1.move_id.id: 1907.17, pay2.move_id.id: 0.09, + res['partials'].exchange_move_id.id: 0.06, + payment_exchange_id[0].exchange_move_id.id: 6.34, + payment_exchange_id[1].exchange_move_id.id: 312.54, }) self.assert_invoice_outstanding_to_reconcile_widget(inv2, {}) @@ -2113,6 +2122,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): ]) self.assert_invoice_outstanding_reconciled_widget(inv2, { pay1.move_id.id: 1907.17, + res['partials'].exchange_move_id.id: 312.54, }) self.assert_invoice_outstanding_to_reconcile_widget(inv2, { refund1.id: 28.46, @@ -2205,11 +2215,15 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, ]) + payment_exchange_id = inv2_rec_line.matched_credit_ids.filtered(lambda x: x.id != res['partials'].id) self.assert_invoice_outstanding_reconciled_widget(inv2, { refund1.id: 28.46, pay1.move_id.id: 1907.17, pay2.move_id.id: 0.09, + res['partials'].exchange_move_id.id: 0.06, + payment_exchange_id[0].exchange_move_id.id: 312.54, + payment_exchange_id[1].exchange_move_id.id: 6.34, }) self.assert_invoice_outstanding_to_reconcile_widget(inv2, {})