From 3fce9ac66150044fdaff55dc86e476cefb35b951 Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Tue, 17 Sep 2024 11:35:58 +0200 Subject: [PATCH] [FIX] account_reconcile_oca : Fix multi currency management Fix the case of payment with a foreign currency set on bank statement line improve tests around multi-currency --- .../models/account_bank_statement_line.py | 96 ++++++++------ .../models/account_reconcile_abstract.py | 24 ++-- .../tests/test_bank_account_reconcile.py | 122 +++++++++++------- 3 files changed, 151 insertions(+), 91 deletions(-) diff --git a/account_reconcile_oca/models/account_bank_statement_line.py b/account_reconcile_oca/models/account_bank_statement_line.py index 434f18e2..ad1d298c 100644 --- a/account_reconcile_oca/models/account_bank_statement_line.py +++ b/account_reconcile_oca/models/account_bank_statement_line.py @@ -194,6 +194,18 @@ class AccountBankStatementLine(models.Model): )._default_reconcile_data() self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) + def _get_amount_currency(self, line, dest_curr): + if line["line_currency_id"] == dest_curr.id: + amount = line["currency_amount"] + else: + amount = self.company_id.currency_id._convert( + line["amount"], + dest_curr, + self.company_id, + self.date, + ) + return amount + @api.onchange("add_account_move_line_id") def _onchange_add_account_move_line_id(self): if self.add_account_move_line_id: @@ -201,16 +213,10 @@ class AccountBankStatementLine(models.Model): new_data = [] is_new_line = True pending_amount = 0.0 - currency = self._get_reconcile_currency() for line in data: if line["kind"] != "suspense": - pending_amount += currency._convert( - line["currency_amount"], - self.env["res.currency"].browse( - line.get("line_currency_id", currency.id) - ), - self.company_id, - self.date, + pending_amount += self._get_amount_currency( + line, self._get_reconcile_currency() ) if self.add_account_move_line_id.id in line.get( "counterpart_line_ids", [] @@ -242,6 +248,7 @@ class AccountBankStatementLine(models.Model): new_data = [] suspense_line = False counterparts = [] + suspense_currency = self.foreign_currency_id or self.currency_id for line in data: if line.get("counterpart_line_ids"): counterparts += line["counterpart_line_ids"] @@ -253,24 +260,25 @@ class AccountBankStatementLine(models.Model): if line["kind"] != "suspense": new_data.append(line) total_amount += line["amount"] - if line.get("currency_amount"): - currency_amount += ( - self.env["res.currency"] - .browse(line["line_currency_id"]) - ._convert( - line["currency_amount"], - self._get_reconcile_currency(), + if not line.get("is_exchange_counterpart"): + # case of statement line with foreign_currency + if ( + line["kind"] == "liquidity" + and line["line_currency_id"] != suspense_currency.id + ): + currency_amount += self.amount_currency + elif ( + line.get("currency_amount") + and line.get("line_currency_id") == suspense_currency.id + ): + currency_amount += line.get("currency_amount") + else: + currency_amount += self.company_id.currency_id._convert( + line["amount"], + suspense_currency, self.company_id, self.date, ) - ) - else: - currency_amount += self.company_id.currency_id._convert( - line["amount"], - self._get_reconcile_currency(), - self.company_id, - self.date, - ) else: suspense_line = line if not float_is_zero( @@ -283,6 +291,7 @@ class AccountBankStatementLine(models.Model): "amount": -total_amount, "credit": total_amount if total_amount > 0 else 0.0, "debit": -total_amount if total_amount < 0 else 0.0, + "currency_amount": -currency_amount, } ) else: @@ -301,7 +310,7 @@ class AccountBankStatementLine(models.Model): "debit": -total_amount if total_amount < 0 else 0.0, "kind": "suspense", "currency_id": self.company_id.currency_id.id, - "line_currency_id": self.currency_id.id, + "line_currency_id": suspense_currency.id, "currency_amount": -currency_amount, } reconcile_auxiliary_id += 1 @@ -323,7 +332,9 @@ class AccountBankStatementLine(models.Model): or self.manual_account_id.id != line["account_id"][0] or self.manual_name != line["name"] or ( - self.manual_partner_id and self.manual_partner_id.name_get()[0] or False + self.manual_partner_id + and self.manual_partner_id.name_get()[0] + or [False, False] ) != line.get("partner_id") or self.analytic_distribution != line.get("analytic_distribution", False) @@ -407,7 +418,7 @@ class AccountBankStatementLine(models.Model): if self.manual_line_id.exists() and self.manual_line_id: self.manual_amount = self.manual_in_currency_id._convert( self.manual_amount_in_currency, - self._get_reconcile_currency(), + self.company_id.currency_id, self.company_id, self.manual_line_id.date, ) @@ -590,6 +601,7 @@ class AccountBankStatementLine(models.Model): self.manual_reference, ) elif res and res.get("amls"): + # TODO should be signed in currency get_reconcile_currency amount = self.amount_total_signed for line in res.get("amls", []): reconcile_auxiliary_id, line_data = self._get_reconcile_line( @@ -896,7 +908,7 @@ class AccountBankStatementLine(models.Model): self.manual_reference, ) elif res.get("amls"): - amount = self.amount + amount = self.amount_currency or self.amount for line in res.get("amls", []): reconcile_auxiliary_id, line_datas = record._get_reconcile_line( line, "other", is_counterpart=True, max_amount=amount, move=True @@ -1021,26 +1033,38 @@ class AccountBankStatementLine(models.Model): ) rates = [] for vals in new_vals: + rate = False if vals["partner_id"] is False: vals["partner_id"] = (False, self.partner_name) - reconcile_auxiliary_id, rate = self._compute_exchange_rate( - vals, line, reconcile_auxiliary_id - ) + if vals.get("kind") not in ("suspense", "liquidity"): + reconcile_auxiliary_id, rate = self._compute_exchange_rate( + vals, line, reconcile_auxiliary_id + ) if rate: rates.append(rate) new_vals += rates return reconcile_auxiliary_id, new_vals def _get_exchange_rate_amount(self, amount, currency_amount, currency, line): - return ( - currency._convert( + if self.foreign_currency_id: + # take real rate of statement line to compute the exchange rate gain/loss + real_rate = self.amount / self.amount_currency + to_amount_journal_currency = currency_amount * real_rate + to_amount_company_currency = self.currency_id._convert( + to_amount_journal_currency, + self.company_id.currency_id, + self.company_id, + self.date, + ) + to_amount = self.company_id.currency_id.round(to_amount_company_currency) + else: + to_amount = currency._convert( currency_amount, self.company_id.currency_id, self.company_id, self.date, ) - - amount - ) + return self.company_id.currency_id.round(to_amount - amount) def _compute_exchange_rate( self, @@ -1109,7 +1133,7 @@ class AccountBankStatementLine(models.Model): def _get_reconcile_currency(self): return ( - self.currency_id + self.foreign_currency_id or self.journal_id.currency_id - or self.company_id._currency_id + or self.company_id.currency_id ) diff --git a/account_reconcile_oca/models/account_reconcile_abstract.py b/account_reconcile_oca/models/account_reconcile_abstract.py index bd8c5025..3a77ff28 100644 --- a/account_reconcile_oca/models/account_reconcile_abstract.py +++ b/account_reconcile_oca/models/account_reconcile_abstract.py @@ -47,18 +47,25 @@ class AccountReconcileAbstract(models.AbstractModel): ): date = self.date if "date" in self._fields else line.date original_amount = amount = net_amount = line.debit - line.credit + line_currency = line.currency_id if is_counterpart: currency_amount = -line.amount_residual_currency or line.amount_residual amount = -line.amount_residual currency = line.currency_id or line.company_id.currency_id original_amount = net_amount = -line.amount_residual if max_amount: - real_currency_amount = currency._convert( - currency_amount, - self._get_reconcile_currency(), - self.company_id, - date, - ) + dest_currency = self._get_reconcile_currency() + if currency == dest_currency: + real_currency_amount = currency_amount + elif self.company_id.currency_id == dest_currency: + real_currency_amount = amount + else: + real_currency_amount = self.company_id.currency_id._convert( + amount, + dest_currency, + self.company_id, + date, + ) if ( -real_currency_amount > max_amount > 0 or -real_currency_amount < max_amount < 0 @@ -76,7 +83,8 @@ class AccountReconcileAbstract(models.AbstractModel): date, ) else: - currency_amount = line.amount_currency + currency_amount = self.amount_currency or self.amount + line_currency = self._get_reconcile_currency() vals = { "move_id": move and line.move_id.id, "move": move and line.move_id.name, @@ -91,7 +99,7 @@ class AccountReconcileAbstract(models.AbstractModel): "amount": amount, "net_amount": amount - net_amount, "currency_id": self.company_id.currency_id.id, - "line_currency_id": line.currency_id.id, + "line_currency_id": line_currency.id, "currency_amount": currency_amount, "analytic_distribution": line.analytic_distribution, "kind": kind, diff --git a/account_reconcile_oca/tests/test_bank_account_reconcile.py b/account_reconcile_oca/tests/test_bank_account_reconcile.py index 716b8f13..72649a15 100644 --- a/account_reconcile_oca/tests/test_bank_account_reconcile.py +++ b/account_reconcile_oca/tests/test_bank_account_reconcile.py @@ -84,7 +84,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): inv1 = self.create_invoice(currency_id=self.currency_usd_id, invoice_amount=100) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -112,6 +111,42 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): self.assertFalse(f.add_account_move_line_id) self.assertTrue(f.can_reconcile) + def test_manual_line_with_currency(self): + bank_stmt = self.acc_bank_stmt_model.create( + { + "journal_id": self.bank_journal_euro.id, + "date": time.strftime("%Y-07-15"), + "name": "test", + } + ) + bank_stmt_line = self.acc_bank_stmt_line_model.create( + { + "name": "testLine", + "journal_id": self.bank_journal_euro.id, + "statement_id": bank_stmt.id, + "amount": 50, + "amount_currency": 100, + "foreign_currency_id": self.currency_usd_id, + "date": time.strftime("%Y-07-15"), + } + ) + receivable_acc = self.company_data["default_account_receivable"] + with Form( + bank_stmt_line, + view="account_reconcile_oca.bank_statement_line_form_reconcile_view", + ) as f: + self.assertFalse(f.can_reconcile) + f.manual_reference = "reconcile_auxiliary;1" + f.manual_account_id = receivable_acc + self.assertTrue(f.can_reconcile) + bank_stmt_line.reconcile_bank_line() + receivable_line = bank_stmt_line.line_ids.filtered( + lambda line: line.account_id == receivable_acc + ) + self.assertEqual(receivable_line.currency_id.id, self.currency_usd_id) + self.assertEqual(receivable_line.amount_currency, -100) + self.assertEqual(receivable_line.balance, -50) + def test_reconcile_invoice_reconcile_full(self): """ We want to test the reconcile widget for bank statements on invoices. @@ -123,7 +158,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -172,7 +206,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -235,7 +268,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -299,7 +331,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -353,7 +384,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): """ bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -397,7 +427,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -452,7 +481,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -512,7 +540,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -542,7 +569,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -600,7 +626,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -642,7 +667,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): """ bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -677,7 +701,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -715,7 +738,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -756,7 +778,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -797,7 +818,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -833,7 +853,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): """ bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -884,7 +903,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): """ bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -927,7 +945,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -991,7 +1008,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_euro.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -1025,7 +1041,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): inv1 = self.create_invoice(currency_id=self.currency_usd_id, invoice_amount=100) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_usd.id, "date": time.strftime("%Y-07-15"), "name": "test", @@ -1064,30 +1079,51 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) def test_journal_foreign_currency_change(self): + cny = self.env.ref("base.CNY") + cny.write({"active": True}) + cny_journal = self.env["account.journal"].create( + { + "name": "Bank CNY", + "type": "bank", + "currency_id": cny.id, + } + ) self.env["res.currency.rate"].create( { - "currency_id": self.env.ref("base.EUR").id, - "name": time.strftime("%Y-07-14"), - "rate": 1.15, + "name": time.strftime("%Y-09-10"), + "currency_id": cny.id, + "inverse_company_rate": 0.125989013758, + } + ) + self.env["res.currency.rate"].create( + { + "name": time.strftime("%Y-09-09"), + "currency_id": cny.id, + "inverse_company_rate": 0.126225969731, } ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, - "journal_id": self.bank_journal_usd.id, - "date": time.strftime("%Y-07-15"), + "journal_id": cny_journal.id, + "date": time.strftime("%Y-09-10"), "name": "test", } ) bank_stmt_line = self.acc_bank_stmt_line_model.create( { "name": "testLine", - "journal_id": self.bank_journal_usd.id, + "journal_id": cny_journal.id, "statement_id": bank_stmt.id, - "amount": 100, - "date": time.strftime("%Y-07-15"), + "amount": 259200, + "date": time.strftime("%Y-09-10"), } ) + inv1 = self._create_invoice( + currency_id=cny.id, + invoice_amount=259200, + date_invoice=time.strftime("%Y-09-09"), + auto_validate=True, + ) with Form( bank_stmt_line, view="account_reconcile_oca.bank_statement_line_form_reconcile_view", @@ -1095,24 +1131,17 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): line = f.reconcile_data_info["data"][0] self.assertEqual( line["currency_amount"], - 100, + 259200, ) - self.env["res.currency.rate"].create( - { - "currency_id": self.env.ref("base.EUR").id, - "name": time.strftime("%Y-07-15"), - "rate": 1.2, - } - ) - with Form( - bank_stmt_line, - view="account_reconcile_oca.bank_statement_line_form_reconcile_view", - ) as f: - line = f.reconcile_data_info["data"][0] - self.assertEqual( - line["currency_amount"], - 100, + f.add_account_move_line_id = inv1.line_ids.filtered( + lambda l: l.account_id.account_type == "asset_receivable" ) + self.assertTrue(f.can_reconcile) + self.assertEqual(len(bank_stmt_line.reconcile_data_info["data"]), 3) + exchange_line = bank_stmt_line.reconcile_data_info["data"][-1] + self.assertEqual(exchange_line["amount"], 61.42) + bank_stmt_line.reconcile_bank_line() + self.assertEqual(inv1.payment_state, "paid") def test_invoice_foreign_currency_change(self): self.env["res.currency.rate"].create( @@ -1137,7 +1166,6 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): ) bank_stmt = self.acc_bank_stmt_model.create( { - "company_id": self.env.ref("base.main_company").id, "journal_id": self.bank_journal_usd.id, "date": time.strftime("%Y-07-15"), "name": "test",