[IMP] account: Payment register wizard - writeoff to exchange diff

When a register payment is made with a writeoff in foreign currency
and when the user set manually an exchange difference account,
don't create a write-off. Instead, 2 cases to consider:
- When the payment is expressed in a foreign currency different than
the invoice's one, play with the rate between amount_currency and
balance and let odoo making an exchange difference accordingly.
That way, the payment has no write-off and the invoice is fully paid.
- When the payment is expressed in company currency but the invoice
has a foreign currency, force the exchange difference to use a rate
that will fully paid the invoice without creating any write-off line
on the payment.

task_id: 3717436

closes odoo/odoo#157398

Related: odoo/enterprise#58511
Signed-off-by: Claire Bretton (clbr) <clbr@odoo.com>
This commit is contained in:
Laurent Smet
2024-04-09 15:06:45 +00:00
parent 4e094189f9
commit bc0e9b3804
7 changed files with 235 additions and 37 deletions
@@ -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):
@@ -271,6 +271,9 @@ 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.
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,
+17 -4
View File
@@ -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,6 +313,10 @@ class AccountPayment(models.Model):
else:
liquidity_amount_currency = 0.0
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,
@@ -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)
@@ -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
@@ -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,6 +713,14 @@ 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.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
@@ -712,6 +736,7 @@ class AccountPaymentRegister(models.TransientModel):
'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:
@@ -29,6 +29,7 @@
<field name="available_partner_bank_ids" invisible="1"/>
<field name="company_currency_id" invisible="1"/>
<field name="hide_writeoff_section" invisible="1"/>
<field name="writeoff_is_exchange_account" invisible="1"/>
<field name="untrusted_bank_ids" invisible="1"/>
<div role="alert" class="alert alert-info" invisible="not hide_writeoff_section">
@@ -77,8 +78,13 @@
string="Post Difference In"
options="{'no_create': True}"
required="payment_difference_handling == 'reconcile' and not early_payment_discount_mode"/>
<label for="writeoff_label" class="oe_edit_only" string="Label"/>
<field name="writeoff_label" required="payment_difference_handling == 'reconcile'"/>
<label for="writeoff_label"
class="oe_edit_only"
string="Label"
invisible="writeoff_is_exchange_account"/>
<field name="writeoff_label"
required="payment_difference_handling == 'reconcile'"
invisible="writeoff_is_exchange_account"/>
</div>
</div>
</group>
@@ -275,9 +275,9 @@ class AccountPayment(models.Model):
res = super()._get_trigger_fields_to_synchronize()
return res + ('l10n_latam_check_number',)
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):
""" Add check name and operation on liquidity line """
res = super()._prepare_move_line_default_vals(write_off_line_vals=write_off_line_vals)
res = super()._prepare_move_line_default_vals(write_off_line_vals=write_off_line_vals, force_balance=force_balance)
check = self if (self.payment_method_line_id.code == 'new_third_party_checks' or (self.payment_method_line_id.code == 'check_printing' and self.l10n_latam_manual_checks)) \
else self.l10n_latam_check_id
if check: