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