diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py
index b810e17cfed..852cb43fa42 100644
--- a/addons/account/models/account_move_line.py
+++ b/addons/account/models/account_move_line.py
@@ -1776,6 +1776,8 @@ class AccountMoveLine(models.Model):
return aml.move_id.payment_id or aml.move_id.statement_line_id
def get_odoo_rate(aml, other_aml, currency):
+ if forced_rate := self._context.get('forced_rate_from_register_payment'):
+ return forced_rate
if other_aml and not is_payment(aml) and is_payment(other_aml):
return get_accounting_rate(other_aml, currency)
if aml.move_id.is_invoice(include_receipts=True):
@@ -1785,6 +1787,8 @@ class AccountMoveLine(models.Model):
return currency._get_conversion_rate(aml.company_currency_id, currency, aml.company_id, exchange_rate_date)
def get_accounting_rate(aml, currency):
+ if forced_rate := self._context.get('forced_rate_from_register_payment'):
+ return forced_rate
balance = aml._get_reconciliation_aml_field_value('balance', shadowed_aml_values)
amount_currency = aml._get_reconciliation_aml_field_value('amount_currency', shadowed_aml_values)
if not aml.company_currency_id.is_zero(balance) and not currency.is_zero(amount_currency):
diff --git a/addons/account/models/account_partial_reconcile.py b/addons/account/models/account_partial_reconcile.py
index 171aa60af15..f9dad7ab28a 100644
--- a/addons/account/models/account_partial_reconcile.py
+++ b/addons/account/models/account_partial_reconcile.py
@@ -271,12 +271,15 @@ class AccountPartialReconcile(models.Model):
if source_line.currency_id != counterpart_line.currency_id:
# When the invoice and the payment are not sharing the same foreign currency, the rate is computed
# on-the-fly using the payment date.
- payment_rate = self.env['res.currency']._get_conversion_rate(
- counterpart_line.company_currency_id,
- source_line.currency_id,
- counterpart_line.company_id,
- payment_date,
- )
+ if 'forced_rate_from_register_payment' in self._context:
+ payment_rate = self._context['forced_rate_from_register_payment']
+ else:
+ payment_rate = self.env['res.currency']._get_conversion_rate(
+ counterpart_line.company_currency_id,
+ source_line.currency_id,
+ counterpart_line.company_id,
+ payment_date,
+ )
elif rate_amount:
payment_rate = rate_amount_currency / rate_amount
else:
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index 7ba636edbbc..66d543535c9 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -282,12 +282,13 @@ class AccountPayment(models.Model):
else:
return self._get_aml_default_display_name_list()
- def _prepare_move_line_default_vals(self, write_off_line_vals=None):
+ def _prepare_move_line_default_vals(self, write_off_line_vals=None, force_balance=None):
''' Prepare the dictionary to create the default account.move.lines for the current payment.
:param write_off_line_vals: Optional list of dictionaries to create a write-off account.move.line easily containing:
* amount: The amount to be added to the counterpart amount.
* name: The label to set on the line.
* account_id: The account on which create the write-off.
+ :param force_balance: Optional balance.
:return: A list of python dictionary to be passed to the account.move.line's 'create' method.
'''
self.ensure_one()
@@ -312,12 +313,16 @@ class AccountPayment(models.Model):
else:
liquidity_amount_currency = 0.0
- liquidity_balance = self.currency_id._convert(
- liquidity_amount_currency,
- self.company_id.currency_id,
- self.company_id,
- self.date,
- )
+ if not write_off_line_vals and force_balance is not None:
+ sign = 1 if liquidity_amount_currency > 0 else -1
+ liquidity_balance = sign * abs(force_balance)
+ else:
+ liquidity_balance = self.currency_id._convert(
+ liquidity_amount_currency,
+ self.company_id.currency_id,
+ self.company_id,
+ self.date,
+ )
counterpart_amount_currency = -liquidity_amount_currency - write_off_amount_currency
counterpart_balance = -liquidity_balance - write_off_balance
currency_id = self.currency_id.id
@@ -701,12 +706,16 @@ class AccountPayment(models.Model):
def create(self, vals_list):
# OVERRIDE
write_off_line_vals_list = []
+ force_balance_vals_list = []
for vals in vals_list:
# Hack to add a custom write-off line.
write_off_line_vals_list.append(vals.pop('write_off_line_vals', None))
+ # Hack to force a custom balance.
+ force_balance_vals_list.append(vals.pop('force_balance', None))
+
# Force the move_type to avoid inconsistency with residual 'default_move_type' inside the context.
vals['move_type'] = 'entry'
vals['journal_id'] = vals.get('journal_id') or self.move_id.with_context(is_payment=True)._search_default_journal().id
@@ -717,8 +726,6 @@ class AccountPayment(models.Model):
} for vals in vals_list])
for i, pay in enumerate(payments):
- write_off_line_vals = write_off_line_vals_list[i]
-
# Write payment_id on the journal entry plus the fields being stored in both models but having the same
# name, e.g. partner_bank_id. The ORM is currently not able to perform such synchronization and make things
# more difficult by creating related fields on the fly to handle the _inherits.
@@ -730,7 +737,13 @@ class AccountPayment(models.Model):
to_write[k] = v
if 'line_ids' not in vals_list[i]:
- to_write['line_ids'] = [(0, 0, line_vals) for line_vals in pay._prepare_move_line_default_vals(write_off_line_vals=write_off_line_vals)]
+ to_write['line_ids'] = [
+ Command.create(line_vals)
+ for line_vals in pay._prepare_move_line_default_vals(
+ write_off_line_vals=write_off_line_vals_list[i],
+ force_balance=force_balance_vals_list[i],
+ )
+ ]
pay.move_id.write(to_write)
self.env.add_to_compute(self.env['account.move']._fields['name'], pay.move_id)
diff --git a/addons/account/tests/test_account_payment_register.py b/addons/account/tests/test_account_payment_register.py
index 4b504348046..7cf59f13746 100644
--- a/addons/account/tests/test_account_payment_register.py
+++ b/addons/account/tests/test_account_payment_register.py
@@ -1007,6 +1007,143 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
},
])
+ def test_register_foreign_currency_on_payment_exchange_writeoff_account(self):
+ 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,
+ 'invoice_line_ids': [Command.create({'product_id': self.product_a.id, 'price_unit': 1000.0, 'tax_ids': []})],
+ })
+ invoice.action_post()
+ # 1998 GOL = 999 USD
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=invoice.ids)\
+ .create({
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount': 1998,
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.env.company.expense_currency_exchange_account_id.id,
+ })\
+ ._create_payments()
+
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 1000.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1998.0,
+ 'reconciled': True,
+ },
+ # Liquidity line:
+ {
+ 'debit': 1000.0,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1998.0,
+ 'reconciled': False,
+ },
+ ])
+
+ def test_register_foreign_currency_on_invoice_exchange_writeoff_account(self):
+ self.env.company.tax_exigibility = True
+ self.env.company.account_cash_basis_base_account_id = self.env['account.account'].create({
+ 'code': 'cash.basis.base.account',
+ 'name': 'cash_basis_base_account',
+ 'account_type': 'income',
+ })
+
+ default_tax = self.company_data['default_tax_sale']
+ default_tax.cash_basis_transition_account_id = self.env['account.account'].create({
+ 'code': 'cash.basis.transfer.account',
+ 'name': 'cash_basis_transfer_account',
+ 'account_type': 'income',
+ 'reconcile': True,
+ })
+ default_tax.tax_exigibility = 'on_payment'
+
+ # 1150 GOL = 575 USD
+ 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': [Command.create({
+ 'product_id': self.product_a.id,
+ 'price_unit': 1000.0,
+ 'tax_ids': [Command.set(default_tax.ids)],
+ })],
+ })
+ invoice.action_post()
+
+ # 1110 GOL = 370 USD
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=invoice.ids)\
+ .create({
+ 'currency_id': self.env.company.currency_id.id,
+ 'amount': 370.0,
+ 'payment_date': '2016-01-01',
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.env.company.expense_currency_exchange_account_id.id,
+ })\
+ ._create_payments()
+
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ # Receivable line:
+ {
+ 'balance': -370.0,
+ 'currency_id': self.env.company.currency_id.id,
+ 'amount_currency': -370.0,
+ 'reconciled': True,
+ },
+ # Liquidity line:
+ {
+ 'balance': 370.0,
+ 'currency_id': self.env.company.currency_id.id,
+ 'amount_currency': 370.0,
+ 'reconciled': False,
+ },
+ ])
+ self.assertRecordValues(invoice.line_ids.matched_credit_ids, [
+ {
+ 'amount': 370.0,
+ 'debit_amount_currency': 1150.0,
+ 'credit_amount_currency': 370.0,
+ },
+ {
+ 'amount': 205.00,
+ 'debit_amount_currency': 0.0,
+ 'credit_amount_currency': 0.0,
+ },
+ ])
+
+ # Cash basis.
+ caba_move = self.env['account.move'].search([('tax_cash_basis_origin_move_id', '=', invoice.id)])
+ self.assertRecordValues(caba_move.line_ids.sorted('balance'), [
+ {
+ 'balance': -321.74,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1000.0,
+ },
+ {
+ 'balance': -48.26,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -150.0,
+ },
+ {
+ 'balance': 48.26,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 150.0,
+ },
+ {
+ 'balance': 321.74,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1000.0,
+ },
+ ])
+
def test_suggested_default_partner_bank_inbound_payment(self):
""" Test the suggested bank account on the wizard for inbound payment. """
self.out_invoice_1.partner_bank_id = False
diff --git a/addons/account/wizard/account_payment_register.py b/addons/account/wizard/account_payment_register.py
index 785b4fce216..d3b8a4212d3 100644
--- a/addons/account/wizard/account_payment_register.py
+++ b/addons/account/wizard/account_payment_register.py
@@ -121,6 +121,9 @@ class AccountPaymentRegister(models.TransientModel):
)
writeoff_label = fields.Char(string='Journal Item Label', default='Write-Off',
help='Change label of the counterpart that will hold the payment difference')
+ writeoff_is_exchange_account = fields.Boolean(
+ compute='_compute_writeoff_is_exchange_account',
+ )
# == Display purpose fields ==
show_partner_bank_account = fields.Boolean(
@@ -360,7 +363,6 @@ class AccountPaymentRegister(models.TransientModel):
'untrusted_bank_ids': untrusted_accounts or False,
})
-
@api.depends('line_ids')
def _compute_from_lines(self):
''' Load initial values from the account.moves passed through the context. '''
@@ -514,6 +516,7 @@ class AccountPaymentRegister(models.TransientModel):
:return: An amount in the currency of the wizard.
"""
self.ensure_one()
+
comp_curr = self.company_id.currency_id
if self.source_currency_id == self.currency_id:
# Same currency (manage the early payment discount).
@@ -587,6 +590,19 @@ class AccountPaymentRegister(models.TransientModel):
else:
wizard.payment_difference = 0.0
+ @api.depends('can_edit_wizard', 'writeoff_account_id')
+ def _compute_writeoff_is_exchange_account(self):
+ for wizard in self:
+ wizard.writeoff_is_exchange_account = all((
+ wizard.can_edit_wizard,
+ wizard.currency_id != wizard.source_currency_id,
+ wizard.writeoff_account_id,
+ wizard.writeoff_account_id in (
+ wizard.company_id.expense_currency_exchange_account_id,
+ wizard.company_id.income_currency_exchange_account_id,
+ ),
+ ))
+
@api.depends('early_payment_discount_mode')
def _compute_payment_difference_handling(self):
for wizard in self:
@@ -697,21 +713,30 @@ class AccountPaymentRegister(models.TransientModel):
payment_vals['write_off_line_vals'] += aml_values_list
elif not self.currency_id.is_zero(self.payment_difference):
- if self.payment_type == 'inbound':
- # Receive money.
- write_off_amount_currency = self.payment_difference
- else: # if self.payment_type == 'outbound':
- # Send money.
- write_off_amount_currency = -self.payment_difference
- payment_vals['write_off_line_vals'].append({
- 'name': self.writeoff_label,
- 'account_id': self.writeoff_account_id.id,
- 'partner_id': self.partner_id.id,
- 'currency_id': self.currency_id.id,
- 'amount_currency': write_off_amount_currency,
- 'balance': self.currency_id._convert(write_off_amount_currency, self.company_id.currency_id, self.company_id, self.payment_date),
- })
+ if self.writeoff_is_exchange_account:
+ # Force the rate when computing the 'balance' only when the payment has a foreign currency.
+ # If not, the rate is forced during the reconciliation to put the difference directly on the
+ # exchange difference.
+ if self.currency_id != self.company_currency_id:
+ payment_vals['force_balance'] = sum(batch_result['lines'].mapped('amount_residual'))
+ else:
+ if self.payment_type == 'inbound':
+ # Receive money.
+ write_off_amount_currency = self.payment_difference
+ else: # if self.payment_type == 'outbound':
+ # Send money.
+ write_off_amount_currency = -self.payment_difference
+
+ payment_vals['write_off_line_vals'].append({
+ 'name': self.writeoff_label,
+ 'account_id': self.writeoff_account_id.id,
+ 'partner_id': self.partner_id.id,
+ 'currency_id': self.currency_id.id,
+ 'amount_currency': write_off_amount_currency,
+ 'balance': self.currency_id._convert(write_off_amount_currency, self.company_id.currency_id, self.company_id, self.payment_date),
+ })
+
return payment_vals
def _create_payment_vals_from_batch(self, batch_result):
@@ -860,9 +885,11 @@ class AccountPaymentRegister(models.TransientModel):
for vals in to_process:
payment_lines = vals['payment'].line_ids.filtered_domain(domain)
lines = vals['to_reconcile']
+ extra_context = {'forced_rate_from_register_payment': vals['rate']} if 'rate' in vals else {}
for account in payment_lines.account_id:
(payment_lines + lines)\
+ .with_context(**extra_context)\
.filtered_domain([('account_id', '=', account.id), ('reconciled', '=', False)])\
.reconcile()
@@ -886,11 +913,19 @@ class AccountPaymentRegister(models.TransientModel):
if edit_mode:
payment_vals = self._create_payment_vals_from_wizard(first_batch_result)
- to_process.append({
+ to_process_values = {
'create_vals': payment_vals,
'to_reconcile': first_batch_result['lines'],
'batch': first_batch_result,
- })
+ }
+
+ # Force the rate during the reconciliation to put the difference directly on the
+ # exchange difference.
+ if self.writeoff_is_exchange_account and self.currency_id == self.company_currency_id:
+ total_batch_residual = sum(first_batch_result['lines'].mapped('amount_residual_currency'))
+ to_process_values['rate'] = abs(total_batch_residual / self.amount) if self.amount else 0.0
+
+ to_process.append(to_process_values)
else:
# Don't group payments: Create one batch per move.
if not self.group_payment:
diff --git a/addons/account/wizard/account_payment_register_views.xml b/addons/account/wizard/account_payment_register_views.xml
index c2d09bb8e7b..5e2a6cd5a1e 100644
--- a/addons/account/wizard/account_payment_register_views.xml
+++ b/addons/account/wizard/account_payment_register_views.xml
@@ -29,6 +29,7 @@