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 0ee42da6e6..c03ff2eef0 100644 --- a/account_reconcile_oca/models/account_account_reconcile.py +++ b/account_reconcile_oca/models/account_account_reconcile.py @@ -164,7 +164,11 @@ def _recompute_data(self, data): 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 fded5c8313..434f18e223 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 @@ def _onchange_add_account_move_line_id(self): 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 @@ def _onchange_add_account_move_line_id(self): 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 @@ def _onchange_add_account_move_line_id(self): 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 @@ def _recompute_suspense_line(self, data, reconcile_auxiliary_id, manual_referenc 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 @@ def _recompute_suspense_line(self, data, reconcile_auxiliary_id, manual_referenc } ) else: + suspense_line = { "reference": "reconcile_auxiliary;%s" % reconcile_auxiliary_id, "id": False, @@ -269,8 +301,8 @@ def _recompute_suspense_line(self, data, reconcile_auxiliary_id, manual_referenc "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 @@ def _onchange_manual_amount_in_currency(self): 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 @@ def _default_reconcile_data(self, from_unreconcile=False): 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 @@ def _default_reconcile_data(self, from_unreconcile=False): 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 @@ def _unreconcile_bank_line_keep(self): 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 @@ def _reconcile_move_line_vals(self, line, move_id=False): "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 @@ def create(self, mvals): 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 @@ def create(self, mvals): 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 @@ def button_manual_reference_full_paid(self): 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 @@ def _get_reconcile_line( max_amount=False, from_unreconcile=False, reconcile_auxiliary_id=False, + move=False, ): new_vals = super()._get_reconcile_line( line, @@ -901,6 +1017,7 @@ def _get_reconcile_line( is_counterpart=is_counterpart, max_amount=max_amount, from_unreconcile=from_unreconcile, + move=move, ) rates = [] for vals in new_vals: @@ -989,3 +1106,10 @@ def add_statement(self): "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 fff2772f3e..bd8c5025c7 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 @@ def _get_reconcile_line( 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 @@ def _get_reconcile_line( 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 37f524e8ca..cd34592e90 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 ac26c7927f..f1f77d719e 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 434f18e223..ad1d298c7e 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 @@ def _onchange_manual_model_id(self): )._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 @@ def _onchange_add_account_move_line_id(self): 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 @@ def _recompute_suspense_line(self, data, reconcile_auxiliary_id, manual_referenc 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 @@ def _recompute_suspense_line(self, data, reconcile_auxiliary_id, manual_referenc 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 @@ def _recompute_suspense_line(self, data, reconcile_auxiliary_id, manual_referenc "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 @@ def _recompute_suspense_line(self, data, reconcile_auxiliary_id, manual_referenc "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 @@ def _check_line_changed(self, line): 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 @@ def _onchange_manual_amount_in_currency(self): 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 @@ def _default_reconcile_data(self, from_unreconcile=False): 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 @@ def create(self, mvals): 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 @@ def _get_reconcile_line( ) 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 @@ def add_statement(self): 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 bd8c5025c7..3a77ff284b 100644 --- a/account_reconcile_oca/models/account_reconcile_abstract.py +++ b/account_reconcile_oca/models/account_reconcile_abstract.py @@ -47,18 +47,25 @@ def _get_reconcile_line( ): 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 @@ def _get_reconcile_line( 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 @@ def _get_reconcile_line( "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 716b8f1328..72649a15f0 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 @@ def test_reconcile_invoice_currency(self): 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 @@ def test_reconcile_invoice_currency(self): 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 @@ def test_reconcile_invoice_reconcile_full(self): ) 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 @@ def test_reconcile_invoice_unreconcile(self): ) 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 @@ def test_reconcile_invoice_partial(self): ) 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 @@ def test_reconcile_invoice_partial_supplier(self): ) 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 @@ def test_reconcile_model(self): """ 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 @@ def test_reconcile_model_tax_included(self): ) 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 @@ def test_reconcile_invoice_model(self): ) 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 @@ def test_reconcile_rule_on_create(self): 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 @@ def test_reconcile_invoice_keep(self): ) 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 @@ def test_reconcile_invoice_to_check_reconciled(self): ) 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 @@ def test_reconcile_invoice_to_check_not_reconciled(self): """ 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 @@ def test_widget_invoice_clean(self): ) 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 @@ def test_widget_invoice_delete(self): ) 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 @@ def test_widget_invoice_unselect(self): ) 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 @@ def test_widget_invoice_change_partner(self): ) 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 @@ def test_widget_model_clean(self): """ 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 @@ def test_bank_statement_line_actions(self): """ 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 @@ def test_filter_partner(self): 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 @@ def test_partner_name_with_parent(self): 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 @@ def test_journal_foreign_currency(self): 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,45 +1079,50 @@ def test_journal_foreign_currency(self): ) 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"), } ) - 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.env["res.currency.rate"].create( - { - "currency_id": self.env.ref("base.EUR").id, - "name": time.strftime("%Y-07-15"), - "rate": 1.2, - } + 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, @@ -1111,8 +1131,17 @@ def test_journal_foreign_currency_change(self): line = f.reconcile_data_info["data"][0] self.assertEqual( line["currency_amount"], - 100, + 259200, + ) + 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 @@ def test_invoice_foreign_currency_change(self): ) 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 ad1d298c7e..a80eec1905 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 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 @@ def _onchange_manual_reconcile_reference(self): @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 @@ def _get_reconcile_line( 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 @@ def _get_exchange_rate_amount(self, amount, currency_amount, currency, line): 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 72649a15f0..f9533ea310 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 @@ def test_invoice_foreign_currency_change(self): 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 a80eec1905..b93ec6bfae 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 @@ def _onchange_add_account_move_line_id(self): 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 @@ def _onchange_add_account_move_line_id(self): 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 b93ec6bfae..9a68a1ea22 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 @@ def _compute_exchange_rate( "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 9161878fe4..48f225c6f6 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 @@ 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 c03ff2eef0..590fe0bf98 100644 --- a/account_reconcile_oca/models/account_account_reconcile.py +++ b/account_reconcile_oca/models/account_account_reconcile.py @@ -166,8 +166,8 @@ def _recompute_data(self, data): 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 9a68a1ea22..a7294bcb8a 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 @@ def _onchange_add_account_move_line_id(self): 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 @@ def _onchange_manual_amount_in_currency(self): 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 @@ def _get_manual_reconcile_vals(self): "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",