[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 <las@odoo.com>
This commit is contained in:
aliya
2022-06-15 11:09:49 +02:00
parent 4272374505
commit 9d2875492b
4 changed files with 181 additions and 32 deletions
+87 -14
View File
@@ -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).
@@ -40,26 +40,32 @@
<td t-if="!line.is_exchange">
<i class="o_field_widget text-left o_payment_label">Paid on <t t-esc="line.date"></t></i>
</td>
<td t-if="line.is_exchange">
<i class="o_field_widget text-left o_payment_label text-muted">Exchange Difference</i>
<td t-if="line.is_exchange" colspan="2">
<i class="o_field_widget text-left text-muted text-left">
<span class="oe_form_field oe_form_field_float oe_form_field_monetary font-weight-bold">
<t t-if="line.position === 'before'">
<t t-esc="line.currency"/>
</t>
<t t-esc="line.amount"/>
<t t-if="line.position === 'after'">
<t t-esc="line.currency"/>
</t>
</span>
<span>Exchange Difference</span>
</i>
</td>
</t>
<td style="text-align:right;">
<span class="oe_form_field oe_form_field_float oe_form_field_monetary" style="margin-left: -10px;">
<t t-if="line.position === 'before'">
<t t-esc="line.currency"/>
</t>
<t t-if="!line.is_exchange">
<t t-esc="line.amount"/>
</t>
<t t-if="line.is_exchange">
<i class="text-muted"><t t-esc="line.amount"/></i>
</t>
<t t-if="line.position === 'after'">
<t t-esc="line.currency"/>
</t>
</span>
</td>
<td t-if="!line.is_exchange" style="text-align:right;">
<span class="oe_form_field oe_form_field_float oe_form_field_monetary" style="margin-left: -10px;">
<t t-if="line.position === 'before'">
<t t-esc="line.currency"/>
</t>
<t t-esc="line.amount"/>
<t t-if="line.position === 'after'">
<t t-esc="line.currency"/>
</t>
</span>
</td>
</tr>
</t>
</table>
@@ -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)
@@ -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, {})