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 @@ + diff --git a/addons/l10n_latam_check/models/account_payment.py b/addons/l10n_latam_check/models/account_payment.py index 156ceb9219a..dc32cbce7c5 100644 --- a/addons/l10n_latam_check/models/account_payment.py +++ b/addons/l10n_latam_check/models/account_payment.py @@ -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: