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, {})