[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:
@@ -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, {})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user