From 62d508bb2077e22683883713fd1dadd1430f6d73 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Fri, 13 Sep 2024 10:33:50 +0200 Subject: [PATCH 1/7] [IMP] account_reconcile_oca: Improve multicurrency management. We will use currency of the line in order to get the suspense and max line value --- .../models/account_account_reconcile.py | 6 +- .../models/account_bank_statement_line.py | 152 ++++++++++++++++-- .../models/account_reconcile_abstract.py | 39 +++-- .../js/widgets/reconcile_data_widget.esm.js | 12 ++ .../static/src/xml/reconcile.xml | 13 +- 5 files changed, 195 insertions(+), 27 deletions(-) diff --git a/account_reconcile_oca/models/account_account_reconcile.py b/account_reconcile_oca/models/account_account_reconcile.py index 0ee42da6..c03ff2ee 100644 --- a/account_reconcile_oca/models/account_account_reconcile.py +++ b/account_reconcile_oca/models/account_account_reconcile.py @@ -164,7 +164,11 @@ class AccountAccountReconcile(models.Model): for line_id in counterparts: max_amount = amount if line_id == counterparts[-1] else 0 lines = self._get_reconcile_line( - self.env["account.move.line"].browse(line_id), "other", True, max_amount + self.env["account.move.line"].browse(line_id), + "other", + True, + max_amount, + move=True, ) new_data["data"] += lines amount += sum(line["amount"] for line in lines) diff --git a/account_reconcile_oca/models/account_bank_statement_line.py b/account_reconcile_oca/models/account_bank_statement_line.py index fded5c83..434f18e2 100644 --- a/account_reconcile_oca/models/account_bank_statement_line.py +++ b/account_reconcile_oca/models/account_bank_statement_line.py @@ -201,9 +201,17 @@ 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 += line["amount"] + pending_amount += currency._convert( + line["currency_amount"], + self.env["res.currency"].browse( + line.get("line_currency_id", currency.id) + ), + self.company_id, + self.date, + ) if self.add_account_move_line_id.id in line.get( "counterpart_line_ids", [] ): @@ -212,7 +220,11 @@ class AccountBankStatementLine(models.Model): new_data.append(line) if is_new_line: reconcile_auxiliary_id, lines = self._get_reconcile_line( - self.add_account_move_line_id, "other", True, pending_amount + self.add_account_move_line_id, + "other", + True, + max_amount=pending_amount, + move=True, ) new_data += lines self.reconcile_data_info = self._recompute_suspense_line( @@ -226,6 +238,7 @@ class AccountBankStatementLine(models.Model): def _recompute_suspense_line(self, data, reconcile_auxiliary_id, manual_reference): can_reconcile = True total_amount = 0 + currency_amount = 0 new_data = [] suspense_line = False counterparts = [] @@ -240,10 +253,28 @@ 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(), + 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( - total_amount, precision_digits=self.currency_id.decimal_places + total_amount, precision_digits=self.company_id.currency_id.decimal_places ): can_reconcile = False if suspense_line: @@ -255,6 +286,7 @@ class AccountBankStatementLine(models.Model): } ) else: + suspense_line = { "reference": "reconcile_auxiliary;%s" % reconcile_auxiliary_id, "id": False, @@ -269,8 +301,8 @@ 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.company_id.currency_id.id, - "currency_amount": -total_amount, + "line_currency_id": self.currency_id.id, + "currency_amount": -currency_amount, } reconcile_auxiliary_id += 1 new_data.append(suspense_line) @@ -375,7 +407,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.company_id.currency_id, + self._get_reconcile_currency(), self.company_id, self.manual_line_id.date, ) @@ -529,7 +561,10 @@ class AccountBankStatementLine(models.Model): reconcile_auxiliary_id = 1 for line in liquidity_lines: reconcile_auxiliary_id, lines = self._get_reconcile_line( - line, "liquidity", reconcile_auxiliary_id=reconcile_auxiliary_id + line, + "liquidity", + reconcile_auxiliary_id=reconcile_auxiliary_id, + move=True, ) data += lines if not from_unreconcile: @@ -574,16 +609,87 @@ class AccountBankStatementLine(models.Model): self.manual_reference, ) for line in other_lines: - reconcile_auxiliary_id, lines = self._get_reconcile_line( - line, "other", from_unreconcile=from_unreconcile - ) - data += lines + partial_lines = self._all_partials_lines(line) if from_unreconcile else [] + if partial_lines: + for reconciled_line in ( + partial_lines.debit_move_id + partial_lines.credit_move_id - line + ): + if ( + reconciled_line.move_id.journal_id + == self.company_id.currency_exchange_journal_id + ): + reconcile_auxiliary_id, lines = self._get_reconcile_line( + reconciled_line.move_id.line_ids - reconciled_line, + "other", + from_unreconcile=False, + move=True, + ) + data += lines + continue + partial = partial_lines.filtered( + lambda r: r.debit_move_id == reconciled_line + or r.credit_move_id == reconciled_line + ) + partial_amount = sum( + partial.filtered( + lambda r: r.credit_move_id == reconciled_line + ).mapped("amount") + ) - sum( + partial.filtered( + lambda r: r.debit_move_id == reconciled_line + ).mapped("amount") + ) + reconcile_auxiliary_id, lines = self._get_reconcile_line( + reconciled_line, + "other", + from_unreconcile={ + "amount": partial_amount, + "credit": partial_amount > 0 and partial_amount, + "debit": partial_amount < 0 and -partial_amount, + "currency_amount": sum( + partial.filtered( + lambda r: r.credit_move_id == reconciled_line + ).mapped("credit_amount_currency") + ) + - sum( + partial.filtered( + lambda r: r.debit_move_id == reconciled_line + ).mapped("debit_amount_currency") + ), + }, + move=True, + ) + data += lines + else: + reconcile_auxiliary_id, lines = self._get_reconcile_line( + line, "other", from_unreconcile=False + ) + data += lines + return self._recompute_suspense_line( data, reconcile_auxiliary_id, self.manual_reference, ) + def _all_partials_lines(self, lines): + reconciliation_lines = lines.filtered( + lambda x: x.account_id.reconcile + or x.account_id.account_type in ("asset_cash", "liability_credit_card") + ) + current_lines = reconciliation_lines + current_partials = self.env["account.partial.reconcile"] + partials = self.env["account.partial.reconcile"] + while current_lines: + current_partials = ( + current_lines.matched_debit_ids + current_lines.matched_credit_ids + ) - current_partials + current_lines = ( + current_partials.debit_move_id + current_partials.credit_move_id + ) - current_lines + partials += current_partials + return partials + def clean_reconcile(self): self.reconcile_data_info = self._default_reconcile_data() self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) @@ -737,12 +843,13 @@ class AccountBankStatementLine(models.Model): to_reverse._reverse_moves(default_values_list, cancel=True) def _reconcile_move_line_vals(self, line, move_id=False): - return { + vals = { "move_id": move_id or self.move_id.id, "account_id": line["account_id"][0], "partner_id": line.get("partner_id") and line["partner_id"][0], "credit": line["credit"], "debit": line["debit"], + "currency_id": line.get("line_currency_id", self.company_id.currency_id.id), "tax_ids": line.get("tax_ids", []), "tax_tag_ids": line.get("tax_tag_ids", []), "group_tax_id": line.get("group_tax_id"), @@ -751,6 +858,11 @@ class AccountBankStatementLine(models.Model): "name": line.get("name"), "reconcile_model_id": line.get("reconcile_model_id"), } + if line.get("line_currency_id") and line["currency_id"] != line.get( + "line_currency_id" + ): + vals["amount_currency"] = line["currency_amount"] + return vals @api.model_create_multi def create(self, mvals): @@ -770,7 +882,9 @@ class AccountBankStatementLine(models.Model): data = [] for line in liquidity_lines: reconcile_auxiliary_id, lines = record._get_reconcile_line( - line, "liquidity" + line, + "liquidity", + move=True, ) data += lines reconcile_auxiliary_id = 1 @@ -785,7 +899,7 @@ class AccountBankStatementLine(models.Model): amount = 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 + line, "other", is_counterpart=True, max_amount=amount, move=True ) amount -= sum(line_data.get("amount") for line_data in line_datas) data += line_datas @@ -847,6 +961,7 @@ class AccountBankStatementLine(models.Model): is_counterpart=True, reconcile_auxiliary_id=reconcile_auxiliary_id, max_amount=original_amount, + move=True, ) new_data += lines new_data.append( @@ -894,6 +1009,7 @@ class AccountBankStatementLine(models.Model): max_amount=False, from_unreconcile=False, reconcile_auxiliary_id=False, + move=False, ): new_vals = super()._get_reconcile_line( line, @@ -901,6 +1017,7 @@ class AccountBankStatementLine(models.Model): is_counterpart=is_counterpart, max_amount=max_amount, from_unreconcile=from_unreconcile, + move=move, ) rates = [] for vals in new_vals: @@ -989,3 +1106,10 @@ class AccountBankStatementLine(models.Model): "split_line_id": self.id, } return action + + def _get_reconcile_currency(self): + return ( + self.currency_id + or self.journal_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 fff2772f..bd8c5025 100644 --- a/account_reconcile_oca/models/account_reconcile_abstract.py +++ b/account_reconcile_oca/models/account_reconcile_abstract.py @@ -33,24 +33,39 @@ class AccountReconcileAbstract(models.AbstractModel): related="company_id.currency_id", string="Company Currency" ) + def _get_reconcile_currency(self): + return self.currency_id or self.company_id._currency_id + def _get_reconcile_line( - self, line, kind, is_counterpart=False, max_amount=False, from_unreconcile=False + self, + line, + kind, + is_counterpart=False, + max_amount=False, + from_unreconcile=False, + move=False, ): date = self.date if "date" in self._fields else line.date original_amount = amount = net_amount = line.debit - line.credit if is_counterpart: currency_amount = -line.amount_residual_currency or line.amount_residual amount = -line.amount_residual - currency = line.currency_id or self.company_id.currency_id + currency = line.currency_id or line.company_id.currency_id original_amount = net_amount = -line.amount_residual if max_amount: - currency_max_amount = self.company_id.currency_id._convert( - max_amount, currency, self.company_id, date + real_currency_amount = currency._convert( + currency_amount, + self._get_reconcile_currency(), + self.company_id, + date, ) if ( - -currency_amount > currency_max_amount > 0 - or -currency_amount < currency_max_amount < 0 + -real_currency_amount > max_amount > 0 + or -real_currency_amount < max_amount < 0 ): + currency_max_amount = self._get_reconcile_currency()._convert( + max_amount, currency, self.company_id, date + ) amount = currency_max_amount net_amount = -max_amount currency_amount = -amount @@ -63,6 +78,8 @@ class AccountReconcileAbstract(models.AbstractModel): else: currency_amount = line.amount_currency vals = { + "move_id": move and line.move_id.id, + "move": move and line.move_id.name, "reference": "account.move.line;%s" % line.id, "id": line.id, "account_id": line.account_id.name_get()[0], @@ -82,11 +99,11 @@ class AccountReconcileAbstract(models.AbstractModel): if from_unreconcile: vals.update( { - "id": False, - "counterpart_line_ids": ( - line.matched_debit_ids.mapped("debit_move_id") - | line.matched_credit_ids.mapped("credit_move_id") - ).ids, + "credit": vals["debit"] and from_unreconcile["debit"], + "debit": vals["credit"] and from_unreconcile["credit"], + "amount": from_unreconcile["amount"], + "net_amount": from_unreconcile["amount"], + "currency_amount": from_unreconcile["currency_amount"], } ) if not float_is_zero( diff --git a/account_reconcile_oca/static/src/js/widgets/reconcile_data_widget.esm.js b/account_reconcile_oca/static/src/js/widgets/reconcile_data_widget.esm.js index 37f524e8..cd34592e 100644 --- a/account_reconcile_oca/static/src/js/widgets/reconcile_data_widget.esm.js +++ b/account_reconcile_oca/static/src/js/widgets/reconcile_data_widget.esm.js @@ -3,12 +3,15 @@ import fieldUtils from "web.field_utils"; import {registry} from "@web/core/registry"; import session from "web.session"; +import {useService} from "@web/core/utils/hooks"; const {Component} = owl; export class AccountReconcileDataWidget extends Component { setup() { super.setup(...arguments); + this.orm = useService("orm"); + this.action = useService("action"); this.foreignCurrency = this.props && this.props.record && @@ -83,6 +86,15 @@ export class AccountReconcileDataWidget extends Component { }); this.env.bus.trigger("RECONCILE_PAGE_NAVIGATE", triggerEv); } + async openMove(ev, moveId) { + ev.preventDefault(); + ev.stopPropagation(); + console.log(moveId); + const action = await this.orm.call("account.move", "get_formview_action", [ + [moveId], + ]); + this.action.doAction(action); + } } AccountReconcileDataWidget.template = "account_reconcile_oca.ReconcileDataWidget"; diff --git a/account_reconcile_oca/static/src/xml/reconcile.xml b/account_reconcile_oca/static/src/xml/reconcile.xml index ac26c792..f1f77d71 100644 --- a/account_reconcile_oca/static/src/xml/reconcile.xml +++ b/account_reconcile_oca/static/src/xml/reconcile.xml @@ -116,7 +116,18 @@ t-on-click="(ev) => this.selectReconcileLine(ev, reconcile_line)" t-att-class="'o_reconcile_widget_line ' + reconcile_line.kind + (props.record.data.manual_reference == reconcile_line.reference ? ' selected ' : ' ')" > - + +
+
+ + + +
+ Date: Tue, 17 Sep 2024 11:35:58 +0200 Subject: [PATCH 2/7] [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", From cd165e299d3e36b5d231810f194003522dd432fd Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Tue, 8 Oct 2024 12:44:09 +0200 Subject: [PATCH 3/7] [FIX] account_reconcile_oca : foreign currency reconcile with late currency rate It is possible that the statement line in foreign currency is created before the rate of the day is updated in Odoo. In this case we need to take the real rate of the statement line to comput the exchange rate --- .../models/account_bank_statement_line.py | 15 +++- .../tests/test_bank_account_reconcile.py | 72 +++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/account_reconcile_oca/models/account_bank_statement_line.py b/account_reconcile_oca/models/account_bank_statement_line.py index ad1d298c..a80eec19 100644 --- a/account_reconcile_oca/models/account_bank_statement_line.py +++ b/account_reconcile_oca/models/account_bank_statement_line.py @@ -8,6 +8,7 @@ from dateutil.relativedelta import relativedelta from odoo import Command, _, api, fields, models from odoo.exceptions import UserError +from odoo.fields import first from odoo.tools import float_is_zero @@ -415,7 +416,11 @@ class AccountBankStatementLine(models.Model): @api.onchange("manual_amount_in_currency") def _onchange_manual_amount_in_currency(self): - if self.manual_line_id.exists() and self.manual_line_id: + if ( + self.manual_line_id.exists() + and self.manual_line_id + and self.manual_kind != "liquidity" + ): self.manual_amount = self.manual_in_currency_id._convert( self.manual_amount_in_currency, self.company_id.currency_id, @@ -1046,7 +1051,7 @@ class AccountBankStatementLine(models.Model): return reconcile_auxiliary_id, new_vals def _get_exchange_rate_amount(self, amount, currency_amount, currency, line): - if self.foreign_currency_id: + if self.foreign_currency_id == currency: # 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 @@ -1057,6 +1062,12 @@ class AccountBankStatementLine(models.Model): self.date, ) to_amount = self.company_id.currency_id.round(to_amount_company_currency) + elif self.currency_id == currency and not self.foreign_currency_id: + liquidity_lines, _suspense_lines, _other_lines = self._seek_for_lines() + real_rate = ( + first(liquidity_lines).balance / first(liquidity_lines).amount_currency + ) + to_amount = self.company_id.currency_id.round(currency_amount * real_rate) else: to_amount = currency._convert( currency_amount, diff --git a/account_reconcile_oca/tests/test_bank_account_reconcile.py b/account_reconcile_oca/tests/test_bank_account_reconcile.py index 72649a15..f9533ea3 100644 --- a/account_reconcile_oca/tests/test_bank_account_reconcile.py +++ b/account_reconcile_oca/tests/test_bank_account_reconcile.py @@ -1195,3 +1195,75 @@ class TestReconciliationWidget(TestAccountReconciliationCommon): self.assertFalse(f.add_account_move_line_id) self.assertTrue(f.can_reconcile) self.assertEqual(3, len(f.reconcile_data_info["data"])) + + def test_invoice_foreign_currency_late_change_of_rate(self): + # Test we can reconcile lines in foreign currency even if the rate was updated + # late in odoo, meaning the statement line was created and the rate was updated + # in odoo after that. + self.env["res.currency.rate"].create( + { + "currency_id": self.env.ref("base.USD").id, + "name": time.strftime("%Y-07-14"), + "rate": 1.15, + } + ) + self.env["res.currency.rate"].create( + { + "currency_id": self.env.ref("base.USD").id, + "name": time.strftime("%Y-07-15"), + "rate": 1.2, + } + ) + inv1 = self._create_invoice( + currency_id=self.currency_usd_id, + invoice_amount=100, + date_invoice=time.strftime("%Y-07-14"), + auto_validate=True, + ) + bank_stmt = self.acc_bank_stmt_model.create( + { + "journal_id": self.bank_journal_usd.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_usd.id, + "statement_id": bank_stmt.id, + "amount": 100, + "date": time.strftime("%Y-07-16"), + } + ) + # rate of 07-16 is create after the statement line, meaning the rate of the + # statement line is the one of the 07-15 + self.env["res.currency.rate"].create( + { + "currency_id": self.env.ref("base.USD").id, + "name": time.strftime("%Y-07-16"), + "rate": 1.25, + } + ) + 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, + ) + self.assertEqual( + line["amount"], + 83.33, + ) + f.manual_reference = "account.move.line;%s" % line["id"] + # simulate click on statement line, check amount does not recompute + self.assertEqual(f.manual_amount, 83.33) + f.add_account_move_line_id = inv1.line_ids.filtered( + lambda l: l.account_id.account_type == "asset_receivable" + ) + self.assertEqual(3, len(f.reconcile_data_info["data"])) + self.assertTrue(f.can_reconcile) + self.assertEqual(f.reconcile_data_info["data"][-1]["amount"], 3.63) From 66f5f3a8de3d61cc7d27a5e163b9c1bc252735c5 Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Tue, 5 Nov 2024 12:59:25 +0100 Subject: [PATCH 4/7] [FIX] account_reconcile_oca : max_amount rounding error leading to unwanted currency conversion --- .../models/account_bank_statement_line.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/account_reconcile_oca/models/account_bank_statement_line.py b/account_reconcile_oca/models/account_bank_statement_line.py index a80eec19..b93ec6bf 100644 --- a/account_reconcile_oca/models/account_bank_statement_line.py +++ b/account_reconcile_oca/models/account_bank_statement_line.py @@ -214,11 +214,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 += self._get_amount_currency( - line, self._get_reconcile_currency() - ) + pending_amount += self._get_amount_currency(line, currency) if self.add_account_move_line_id.id in line.get( "counterpart_line_ids", [] ): @@ -230,7 +229,7 @@ class AccountBankStatementLine(models.Model): self.add_account_move_line_id, "other", True, - max_amount=pending_amount, + max_amount=currency.round(pending_amount), move=True, ) new_data += lines From c1a7bf72490337791b76294e692df447a9f7cf5e Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Tue, 5 Nov 2024 13:00:48 +0100 Subject: [PATCH 5/7] [FIX] account_reconcile_oca : exchange rate gain/loss currency and amount If currency amount is not 0, the suspense line will have wrong amount --- account_reconcile_oca/models/account_bank_statement_line.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/account_reconcile_oca/models/account_bank_statement_line.py b/account_reconcile_oca/models/account_bank_statement_line.py index b93ec6bf..9a68a1ea 100644 --- a/account_reconcile_oca/models/account_bank_statement_line.py +++ b/account_reconcile_oca/models/account_bank_statement_line.py @@ -1113,8 +1113,8 @@ class AccountBankStatementLine(models.Model): "debit": amount if amount > 0 else 0.0, "kind": "other", "currency_id": self.company_id.currency_id.id, - "line_currency_id": self.company_id.currency_id.id, - "currency_amount": amount, + "line_currency_id": currency.id, + "currency_amount": 0, } reconcile_auxiliary_id += 1 return reconcile_auxiliary_id, data From 9ec53e169dfdf5eb1259deeaa2375e848d599a58 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Fri, 3 Jan 2025 13:05:30 +0100 Subject: [PATCH 6/7] [FIX] account_reconcile_analytic_tag: Add kwargs on _get_reconcile_line --- .../models/account_reconcile_abstract.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/account_reconcile_analytic_tag/models/account_reconcile_abstract.py b/account_reconcile_analytic_tag/models/account_reconcile_abstract.py index 9161878f..48f225c6 100644 --- a/account_reconcile_analytic_tag/models/account_reconcile_abstract.py +++ b/account_reconcile_analytic_tag/models/account_reconcile_abstract.py @@ -6,15 +6,7 @@ from odoo import models class AccountReconcileAbstract(models.AbstractModel): _inherit = "account.reconcile.abstract" - def _get_reconcile_line( - self, line, kind, is_counterpart=False, max_amount=False, from_unreconcile=False - ): - vals = super()._get_reconcile_line( - line=line, - kind=kind, - is_counterpart=is_counterpart, - max_amount=max_amount, - from_unreconcile=from_unreconcile, - ) + def _get_reconcile_line(self, line, kind, **kwargs): + vals = super()._get_reconcile_line(line, kind, **kwargs) vals[0]["manual_analytic_tag_ids"] = [(6, 0, line.analytic_tag_ids.ids)] return vals From f0ae2f00ac2ea5ffbb1d3e19c79d2ee9c37b0635 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Fri, 3 Jan 2025 14:01:50 +0100 Subject: [PATCH 7/7] [FIX] account_reconcile_oca: Update currency_amount --- .../models/account_account_reconcile.py | 4 ++-- .../models/account_bank_statement_line.py | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/account_reconcile_oca/models/account_account_reconcile.py b/account_reconcile_oca/models/account_account_reconcile.py index c03ff2ee..590fe0bf 100644 --- a/account_reconcile_oca/models/account_account_reconcile.py +++ b/account_reconcile_oca/models/account_account_reconcile.py @@ -166,8 +166,8 @@ class AccountAccountReconcile(models.Model): lines = self._get_reconcile_line( self.env["account.move.line"].browse(line_id), "other", - True, - max_amount, + is_counterpart=True, + max_amount=max_amount, move=True, ) new_data["data"] += lines diff --git a/account_reconcile_oca/models/account_bank_statement_line.py b/account_reconcile_oca/models/account_bank_statement_line.py index 9a68a1ea..a7294bcb 100644 --- a/account_reconcile_oca/models/account_bank_statement_line.py +++ b/account_reconcile_oca/models/account_bank_statement_line.py @@ -228,7 +228,7 @@ class AccountBankStatementLine(models.Model): reconcile_auxiliary_id, lines = self._get_reconcile_line( self.add_account_move_line_id, "other", - True, + is_counterpart=True, max_amount=currency.round(pending_amount), move=True, ) @@ -429,7 +429,7 @@ class AccountBankStatementLine(models.Model): self._onchange_manual_reconcile_vals() def _get_manual_reconcile_vals(self): - return { + vals = { "name": self.manual_name, "partner_id": self.manual_partner_id and self.manual_partner_id.name_get()[0] @@ -442,6 +442,18 @@ class AccountBankStatementLine(models.Model): "debit": self.manual_amount if self.manual_amount > 0 else 0.0, "analytic_distribution": self.analytic_distribution, } + if self.manual_line_id: + vals.update( + { + "currency_amount": self.manual_line_id.currency_id._convert( + self.manual_amount, + self.manual_in_currency_id, + self.company_id, + self.manual_line_id.date, + ), + } + ) + return vals @api.onchange( "manual_account_id",