# Copyright 2023 Dixmit # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from collections import defaultdict from dateutil import rrule from dateutil.relativedelta import relativedelta from odoo import Command, _, api, fields, models, tools from odoo.exceptions import UserError from odoo.fields import first from odoo.tools import float_compare, float_is_zero class AccountBankStatementLine(models.Model): _name = "account.bank.statement.line" _inherit = ["account.bank.statement.line", "account.reconcile.abstract"] reconcile_data_info = fields.Serialized(inverse="_inverse_reconcile_data_info") reconcile_mode = fields.Selection( selection=lambda self: self.env["account.journal"] ._fields["reconcile_mode"] .selection ) company_id = fields.Many2one(related="journal_id.company_id") reconcile_data = fields.Serialized() manual_line_id = fields.Many2one( "account.move.line", store=False, default=False, prefetch=False, ) manual_kind = fields.Char( store=False, default=False, prefetch=False, ) manual_account_id = fields.Many2one( "account.account", check_company=True, store=False, default=False, prefetch=False, ) manual_partner_id = fields.Many2one( "res.partner", domain=[("parent_id", "=", False)], check_company=True, store=False, default=False, prefetch=False, ) analytic_distribution = fields.Json( store=False, default=False, prefetch=False, ) analytic_precision = fields.Integer( store=False, default=lambda self: self.env["decimal.precision"].precision_get( "Percentage Analytic" ), ) manual_in_currency = fields.Boolean( readonly=True, store=False, prefetch=False, string="Manual In Currency?" ) manual_in_currency_id = fields.Many2one( "res.currency", readonly=True, store=False, prefetch=False, string="Manual In Currency", ) manual_amount_in_currency = fields.Monetary( store=False, default=False, prefetch=False, currency_field="manual_in_currency_id", ) manual_exchange_counterpart = fields.Boolean( store=False, ) manual_model_id = fields.Many2one( "account.reconcile.model", check_company=True, store=False, default=False, prefetch=False, domain=[("rule_type", "=", "writeoff_button")], ) manual_name = fields.Char(store=False, default=False, prefetch=False) manual_amount = fields.Monetary( store=False, default=False, prefetch=False, currency_field="manual_currency_id" ) manual_currency_id = fields.Many2one( "res.currency", readonly=True, store=False, prefetch=False ) manual_original_amount = fields.Monetary( default=False, store=False, prefetch=False, readonly=True ) manual_move_type = fields.Selection( lambda r: r.env["account.move"]._fields["move_type"].selection, default=False, store=False, prefetch=False, readonly=True, ) manual_move_id = fields.Many2one( "account.move", default=False, store=False, prefetch=False, readonly=True ) can_reconcile = fields.Boolean(sparse="reconcile_data_info") reconcile_aggregate = fields.Char(compute="_compute_reconcile_aggregate") aggregate_id = fields.Integer(compute="_compute_reconcile_aggregate") aggregate_name = fields.Char(compute="_compute_reconcile_aggregate") @api.model def _reconcile_aggregate_map(self): lang = self.env["res.lang"]._lang_get(self.env.user.lang) week_start = rrule.weekday(int(lang.week_start) - 1) return { False: lambda s: (False, False), "statement": lambda s: (s.statement_id.id, s.statement_id.name), "day": lambda s: (s.date.toordinal(), s.date.strftime(lang.date_format)), "week": lambda s: ( (s.date + relativedelta(weekday=week_start(-1))).toordinal(), (s.date + relativedelta(weekday=week_start(-1))).strftime( lang.date_format ), ), "month": lambda s: ( s.date.replace(day=1).toordinal(), s.date.replace(day=1).strftime(lang.date_format), ), } @api.depends("company_id", "journal_id") def _compute_reconcile_aggregate(self): reconcile_aggregate_map = self._reconcile_aggregate_map() for record in self: reconcile_aggregate = ( record.journal_id.reconcile_aggregate or record.company_id.reconcile_aggregate ) record.reconcile_aggregate = reconcile_aggregate record.aggregate_id, record.aggregate_name = reconcile_aggregate_map[ reconcile_aggregate ](record) def save(self): return {"type": "ir.actions.act_window_close"} @api.model def action_new_line(self): action = self.env["ir.actions.act_window"]._for_xml_id( "account_reconcile_oca.action_bank_statement_line_create" ) action["context"] = self.env.context return action @api.onchange("manual_model_id") def _onchange_manual_model_id(self): if self.manual_model_id: data = [] for line in self.reconcile_data_info.get("data", []): if line.get("kind") != "suspense": data.append(line) self.reconcile_data_info = self._recompute_suspense_line( *self._reconcile_data_by_model( data, self.manual_model_id, self.reconcile_data_info["reconcile_auxiliary_id"], ), self.manual_reference, ) else: # Refreshing data self.reconcile_data_info = self.browse( self.id.origin )._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: self._add_account_move_line(self.add_account_move_line_id) self.add_account_move_line_id = False def _add_account_move_line(self, move_line, keep_current=False): data = self.reconcile_data_info["data"] 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, currency) if move_line.id in line.get("counterpart_line_ids", []): is_new_line = False if keep_current: new_data.append(line) else: new_data.append(line) if is_new_line: reconcile_auxiliary_id, lines = self._get_reconcile_line( move_line, "other", is_counterpart=True, max_amount=currency.round(pending_amount), move=True, ) new_data += lines self.reconcile_data_info = self._recompute_suspense_line( new_data, self.reconcile_data_info["reconcile_auxiliary_id"], self.manual_reference, ) self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) 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 = [] 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"] if ( line["account_id"][0] == self.journal_id.suspense_account_id.id or not line["account_id"][0] ) and line["kind"] != "suspense": can_reconcile = False if line["kind"] != "suspense": new_data.append(line) total_amount += line["amount"] 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: suspense_line = line if not float_is_zero( total_amount, precision_digits=self.company_id.currency_id.decimal_places ): can_reconcile = False if suspense_line: suspense_line.update( { "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: account = self.journal_id.suspense_account_id if self.partner_id and total_amount > 0: can_reconcile = True account = self.partner_id.property_account_receivable_id elif self.partner_id and total_amount < 0: can_reconcile = True account = self.partner_id.property_account_payable_id suspense_line = { "reference": "reconcile_auxiliary;%s" % reconcile_auxiliary_id, "id": False, "account_id": [account.id, account.display_name], "partner_id": self.partner_id and [self.partner_id.id, self.partner_id.display_name] or (self.partner_name and (False, self.partner_name)) or False, "date": fields.Date.to_string(self.date), "name": self.payment_ref or self.name, "amount": -total_amount, "credit": total_amount if total_amount > 0 else 0.0, "debit": -total_amount if total_amount < 0 else 0.0, "kind": "suspense", "currency_id": self.company_id.currency_id.id, "line_currency_id": suspense_currency.id, "currency_amount": -currency_amount, } reconcile_auxiliary_id += 1 new_data.append(suspense_line) return { "data": new_data, "counterparts": counterparts, "reconcile_auxiliary_id": reconcile_auxiliary_id, "can_reconcile": can_reconcile, "manual_reference": manual_reference, } def _check_line_changed(self, line): return ( not float_is_zero( self.manual_amount - line["amount"], precision_digits=self.company_id.currency_id.decimal_places, ) 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.id, self.manual_partner_id.display_name] or [False, False] ) != line.get("partner_id") or self.analytic_distribution != line.get("analytic_distribution", False) ) def _check_reconcile_data_changed(self): self.ensure_one() data = self.reconcile_data_info.get("data", []) liquidity_lines, _suspense_lines, _other_lines = self._seek_for_lines() move_amount_cur = sum(liquidity_lines.mapped("amount_currency")) move_credit = sum(liquidity_lines.mapped("credit")) move_debit = sum(liquidity_lines.mapped("debit")) stmt_amount_curr = stmt_debit = stmt_credit = 0.0 for line_data in data: if line_data["kind"] != "liquidity": continue stmt_amount_curr += line_data["currency_amount"] stmt_debit += line_data["debit"] stmt_credit += line_data["credit"] prec = self.currency_id.rounding return ( float_compare(move_amount_cur, move_amount_cur, precision_rounding=prec) != 0 or float_compare(move_credit, stmt_credit, precision_rounding=prec) != 0 or float_compare(move_debit, stmt_debit, precision_rounding=prec) != 0 ) def _get_manual_delete_vals(self): return { "manual_reference": False, "manual_account_id": False, "manual_amount": False, "manual_exchange_counterpart": False, "manual_in_currency_id": False, "manual_in_currency": False, "manual_name": False, "manual_partner_id": False, "manual_line_id": False, "manual_move_id": False, "manual_move_type": False, "manual_kind": False, "manual_original_amount": False, "manual_currency_id": False, "analytic_distribution": False, } def _process_manual_reconcile_from_line(self, line): self.manual_account_id = line["account_id"][0] self.manual_amount = line["amount"] self.manual_currency_id = line["currency_id"] self.manual_in_currency_id = line.get("line_currency_id") self.manual_in_currency = line.get("line_currency_id") and line[ "currency_id" ] != line.get("line_currency_id") self.manual_amount_in_currency = line.get("currency_amount") self.manual_name = line["name"] self.manual_exchange_counterpart = line.get("is_exchange_counterpart", False) self.manual_partner_id = line.get("partner_id") and line["partner_id"][0] manual_line = self.env["account.move.line"].browse(line["id"]).exists() self.manual_line_id = manual_line self.analytic_distribution = line.get("analytic_distribution", {}) if self.manual_line_id: self.manual_move_id = self.manual_line_id.move_id self.manual_move_type = self.manual_line_id.move_id.move_type self.manual_kind = line["kind"] self.manual_original_amount = line.get("original_amount", 0.0) @api.onchange("manual_reference", "manual_delete") def _onchange_manual_reconcile_reference(self): self.ensure_one() data = self.reconcile_data_info.get("data", []) new_data = [] related_move_line_id = False for line in data: if line.get("reference") == self.manual_reference: related_move_line_id = line.get("id") break for line in data: if ( self.manual_delete and related_move_line_id and line.get("original_exchange_line_id") == related_move_line_id ): # We should remove the related exchange rate line continue if line["reference"] == self.manual_reference: if self.manual_delete: self.update(self._get_manual_delete_vals()) continue else: self._process_manual_reconcile_from_line(line) new_data.append(line) self.update({"manual_delete": False}) self.reconcile_data_info = self._recompute_suspense_line( new_data, self.reconcile_data_info["reconcile_auxiliary_id"], self.manual_reference, ) self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) @api.onchange("manual_amount_in_currency") def _onchange_manual_amount_in_currency(self): 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, self.company_id, self.manual_line_id.date, ) self._onchange_manual_reconcile_vals() def _get_manual_reconcile_vals(self): vals = { "name": self.manual_name, "partner_id": ( self.manual_partner_id and [self.manual_partner_id.id, self.manual_partner_id.display_name] or (self.partner_name and (False, self.partner_name)) or False ), "account_id": ( [self.manual_account_id.id, self.manual_account_id.display_name] if self.manual_account_id else [False, _("Undefined")] ), "amount": self.manual_amount, "credit": -self.manual_amount if self.manual_amount < 0 else 0.0, "debit": self.manual_amount if self.manual_amount > 0 else 0.0, "analytic_distribution": self.analytic_distribution, } liquidity_lines, _suspense_lines, _other_lines = self._seek_for_lines() if self.manual_line_id and self.manual_line_id.id not in liquidity_lines.ids: vals.update( { "currency_amount": self.manual_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", "manual_partner_id", "manual_name", "manual_amount", "analytic_distribution", ) def _onchange_manual_reconcile_vals(self): self.ensure_one() data = self.reconcile_data_info.get("data", []) new_data = [] for line in data: if line["reference"] == self.manual_reference: if self._check_line_changed(line): line_vals = self._get_manual_reconcile_vals() line_vals["kind"] = ( line["kind"] if line["kind"] != "suspense" else "other" ) line.update(line_vals) if line["kind"] == "liquidity": self._update_move_partner() if self.manual_line_id and self.manual_line_id.id == line.get( "original_exchange_line_id" ): # Now, we should edit the amount of the exchange rate amount = self._get_exchange_rate_amount( self.manual_amount, self.manual_amount_in_currency, self.manual_line_id.currency_id, self.manual_line_id, ) line.update( { "amount": amount, "credit": -amount if amount < 0 else 0.0, "debit": amount if amount > 0 else 0.0, } ) new_data.append(line) self.reconcile_data_info = self._recompute_suspense_line( new_data, self.reconcile_data_info["reconcile_auxiliary_id"], self.manual_reference, ) self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) def _update_move_partner(self): if self.partner_id == self.manual_partner_id: return self.partner_id = self.manual_partner_id @api.depends("reconcile_data", "is_reconciled") def _compute_reconcile_data_info(self): for record in self: if record.reconcile_data: record.reconcile_data_info = record.reconcile_data else: record.reconcile_data_info = record._default_reconcile_data( from_unreconcile=record.is_reconciled ) record.can_reconcile = record.reconcile_data_info.get( "can_reconcile", False ) def action_show_move(self): self.ensure_one() action = self.env["ir.actions.act_window"]._for_xml_id( "account.action_move_journal_line" ) action.update( {"res_id": self.move_id.id, "views": [[False, "form"]], "view_mode": "form"} ) return action def _inverse_reconcile_data_info(self): for record in self: record.reconcile_data = record.reconcile_data_info def _reconcile_data_by_model(self, data, reconcile_model, reconcile_auxiliary_id): new_data = [] liquidity_amount = 0.0 currency = self._get_reconcile_currency() currency_amount = False for line_data in data: if line_data["kind"] == "suspense": continue new_data.append(line_data) liquidity_amount += line_data["amount"] for line in reconcile_model._get_write_off_move_lines_dict( -liquidity_amount, self._retrieve_partner().id ): new_line = line.copy() amount = line.get("balance") if self.foreign_currency_id: amount = self.foreign_currency_id._convert( amount, self.journal_id.currency_id or self.company_currency_id, self.company_id, self.date, ) if currency != self.company_id.currency_id: currency_amount = self.company_id.currency_id._convert( amount, currency, self.company_id, self.date, ) new_line.update( { "reference": "reconcile_auxiliary;%s" % reconcile_auxiliary_id, "id": False, "amount": amount, "debit": amount if amount > 0 else 0, "credit": -amount if amount < 0 else 0, "kind": "other", "account_id": [ line["account_id"], self.env["account.account"] .browse(line["account_id"]) .display_name, ], "date": fields.Date.to_string(self.date), "line_currency_id": currency.id, "currency_id": self.company_id.currency_id.id, "currency_amount": currency_amount or amount, "name": line.get("name") or self.payment_ref, } ) reconcile_auxiliary_id += 1 if line.get("partner_id"): new_line["partner_id"] = ( line["partner_id"], self.env["res.partner"].browse(line["partner_id"]).display_name, ) elif self.partner_id: new_line["partner_id"] = ( self.partner_id.id, self.partner_id.display_name, ) new_data.append(new_line) return new_data, reconcile_auxiliary_id def _default_reconcile_data(self, from_unreconcile=False): liquidity_lines, suspense_lines, other_lines = self._seek_for_lines() data = [] 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, move=True, ) data += lines if not from_unreconcile: res = ( self.env["account.reconcile.model"] .search( [ ( "rule_type", "in", ["invoice_matching", "writeoff_suggestion"], ), ("company_id", "=", self.company_id.id), ] ) ._apply_rules(self, self._retrieve_partner()) ) if res and res.get("status", "") == "write_off": return self._recompute_suspense_line( *self._reconcile_data_by_model( data, res["model"], reconcile_auxiliary_id ), 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( line, "other", is_counterpart=True, max_amount=amount, reconcile_auxiliary_id=reconcile_auxiliary_id, ) amount -= sum(line.get("amount") for line in line_data) data += line_data if res.get("auto_reconcile") and self.reconcile_data_info: self.reconcile_bank_line() return self._recompute_suspense_line( data, reconcile_auxiliary_id, self.manual_reference, ) for line in other_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 ): for rl_item in ( reconciled_line.move_id.line_ids - reconciled_line ): reconcile_auxiliary_id, lines = self._get_reconcile_line( rl_item, "other", from_unreconcile=False, move=True, ) data += lines continue partial = partial_lines.filtered( lambda r, line=reconciled_line: r.debit_move_id == line or r.credit_move_id == line ) partial_amount = sum( partial.filtered( lambda r, line=reconciled_line: r.credit_move_id == line ).mapped("amount") ) - sum( partial.filtered( lambda r, line=reconciled_line: r.debit_move_id == 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, line=reconciled_line: r.credit_move_id == line ).mapped("credit_amount_currency") ) - sum( partial.filtered( lambda r, line=reconciled_line: r.debit_move_id == 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) def reconcile_bank_line(self): self.ensure_one() self.reconcile_mode = self.journal_id.reconcile_mode result = getattr(self, "_reconcile_bank_line_%s" % self.reconcile_mode)( self._prepare_reconcile_line_data(self.reconcile_data_info["data"]) ) self.reconcile_data = False return result def _reconcile_bank_line_edit(self, data): _liquidity_lines, suspense_lines, other_lines = self._seek_for_lines() lines_to_remove = [(2, line.id) for line in suspense_lines + other_lines] # Cleanup previous lines. move = self.move_id container = {"records": move, "self": move} to_reconcile = [] with move._check_balanced(container): move.with_context( skip_account_move_synchronization=True, force_delete=True, skip_invoice_sync=True, ).write( { "line_ids": lines_to_remove, } ) for line_vals in data: if line_vals["kind"] == "liquidity": continue line = ( self.env["account.move.line"] .with_context( check_move_validity=False, skip_sync_invoice=True, skip_invoice_sync=True, validate_analytic=True, ) .create(self._reconcile_move_line_vals(line_vals)) ) if line_vals.get("counterpart_line_ids"): to_reconcile.append( self.env["account.move.line"].browse( line_vals.get("counterpart_line_ids") ) + line ) for reconcile_items in to_reconcile: reconcile_items.reconcile() def _reconcile_bank_line_keep_move_vals(self): return { "journal_id": self.journal_id.id, } def _reconcile_bank_line_keep(self, data): move = ( self.env["account.move"] .with_context(skip_invoice_sync=True) .create(self._reconcile_bank_line_keep_move_vals()) ) _liquidity_lines, suspense_lines, other_lines = self._seek_for_lines() container = {"records": move, "self": move} to_reconcile = defaultdict(lambda: self.env["account.move.line"]) with move._check_balanced(container): for line in suspense_lines | other_lines: to_reconcile[line.account_id.id] |= line line_data = line.with_context( active_test=False, include_business_fields=True, ).copy_data({"move_id": move.id})[0] to_reconcile[line.account_id.id] |= ( self.env["account.move.line"] .with_context( check_move_validity=False, skip_sync_invoice=True, skip_invoice_sync=True, ) .create(line_data) ) move.write( { "line_ids": [ Command.update( line.id, { "balance": -line.balance, "amount_currency": -line.amount_currency, }, ) for line in move.line_ids if line.move_id.move_type == "entry" or line.display_type == "cogs" ] } ) for line_vals in data: if line_vals["kind"] == "liquidity": continue if line_vals["kind"] == "suspense": raise UserError(_("No supense lines are allowed when reconciling")) line = ( self.env["account.move.line"] .with_context(check_move_validity=False, skip_invoice_sync=True) .create(self._reconcile_move_line_vals(line_vals, move.id)) ) if line_vals.get("counterpart_line_ids") and line.account_id.reconcile: to_reconcile[line.account_id.id] |= ( self.env["account.move.line"].browse( line_vals.get("counterpart_line_ids") ) | line ) move.invalidate_recordset() move._post() for _account, lines in to_reconcile.items(): lines.reconcile() def unreconcile_bank_line(self): self.ensure_one() return getattr( self, "_unreconcile_bank_line_%s" % (self.reconcile_mode or "edit") )() def _unreconcile_bank_line_edit(self): self.reconcile_data_info = self._default_reconcile_data(from_unreconcile=True) self.action_undo_reconciliation() def _unreconcile_bank_line_keep(self): self.reconcile_data_info = self._default_reconcile_data(from_unreconcile=True) # Reverse reconciled journal entry to_reverse = ( self.line_ids._all_reconciled_lines() .filtered( lambda line: line.move_id != self.move_id and (line.matched_debit_ids or line.matched_credit_ids) ) .mapped("move_id") ) if to_reverse: default_values_list = [ { "date": move.date, "ref": _("Reversal of: %s", move.name), } for move in to_reverse ] to_reverse._reverse_moves(default_values_list, cancel=True) def _reconcile_move_line_vals(self, line, move_id=False): 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"), "tax_repartition_line_id": line.get("tax_repartition_line_id"), "analytic_distribution": line.get("analytic_distribution"), "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): result = super().create(mvals) if tools.config["test_enable"] and not self.env.context.get( "_test_account_reconcile_oca" ): return result models = self.env["account.reconcile.model"].search( [ ("rule_type", "in", ["invoice_matching", "writeoff_suggestion"]), ("company_id", "in", result.company_id.ids), ("auto_reconcile", "=", True), ] ) for record in result: res = models._apply_rules(record, record._retrieve_partner()) if not res: continue liquidity_lines, suspense_lines, other_lines = record._seek_for_lines() data = [] for line in liquidity_lines: reconcile_auxiliary_id, lines = record._get_reconcile_line( line, "liquidity", move=True, ) data += lines reconcile_auxiliary_id = 1 if res.get("status", "") == "write_off": data = record._recompute_suspense_line( *record._reconcile_data_by_model( data, res["model"], reconcile_auxiliary_id ), self.manual_reference, ) elif res.get("amls"): 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 ) amount -= sum(line_data.get("amount") for line_data in line_datas) data += line_datas data = record._recompute_suspense_line( data, reconcile_auxiliary_id, self.manual_reference, ) if not data.get("can_reconcile"): continue getattr( record, "_reconcile_bank_line_%s" % record.journal_id.reconcile_mode )(self._prepare_reconcile_line_data(data["data"])) return result def _synchronize_to_moves(self, changed_fields): """We want to avoid to change stuff (mainly amounts ) in accounting entries when some changes happen in the reconciliation widget. The only change (among the fields triggering the synchronization) possible from the reconciliation widget is the partner_id field. So, in case of change on partner_id field we do not call super but make only the required change (relative to partner) on accounting entries. And if something else changes, we then re-define reconcile_data_info to make the data consistent (for example, if debit/credit has changed by applying a different rate or even if there was a correction on statement line amount). """ if self._context.get("skip_account_move_synchronization"): return if "partner_id" in changed_fields and not any( field_name in changed_fields for field_name in ( "payment_ref", "amount", "amount_currency", "foreign_currency_id", "currency_id", ) ): for st_line in self.with_context(skip_account_move_synchronization=True): ( liquidity_lines, suspense_lines, _other_lines, ) = st_line._seek_for_lines() line_vals = {"partner_id": st_line.partner_id.id} line_ids_commands = [(1, liquidity_lines.id, line_vals)] if suspense_lines: line_ids_commands.append((1, suspense_lines.id, line_vals)) st_line_vals = {"line_ids": line_ids_commands} if st_line.move_id.partner_id != st_line.partner_id: st_line_vals["partner_id"] = st_line.partner_id.id st_line.move_id.write(st_line_vals) else: super()._synchronize_to_moves(changed_fields=changed_fields) if not any( field_name in changed_fields for field_name in ( "payment_ref", "amount", "amount_currency", "foreign_currency_id", "currency_id", "partner_id", ) ): return # reset reconcile_data_info if amounts are not consistent anymore with the # amounts of the accounting entries for st_line in self: if st_line._check_reconcile_data_changed(): st_line.reconcile_data_info = st_line._default_reconcile_data() def _prepare_reconcile_line_data(self, lines): new_lines = [] reverse_lines = {} for line in lines: if not line.get("id") and not line.get("original_exchange_line_id"): new_lines.append(line) elif not line.get("original_exchange_line_id"): reverse_lines[line["id"]] = line for line in lines: if line.get("original_exchange_line_id"): reverse_lines[line["original_exchange_line_id"]].update( { "amount": reverse_lines[line["original_exchange_line_id"]][ "amount" ] + line["amount"], "credit": reverse_lines[line["original_exchange_line_id"]][ "credit" ] + line["credit"], "debit": reverse_lines[line["original_exchange_line_id"]][ "debit" ] + line["debit"], } ) return new_lines + list(reverse_lines.values()) def button_manual_reference_full_paid(self): self.ensure_one() if not self.reconcile_data_info["manual_reference"]: return manual_reference = self.reconcile_data_info["manual_reference"] data = self.reconcile_data_info.get("data", []) new_data = [] reconcile_auxiliary_id = self.reconcile_data_info["reconcile_auxiliary_id"] for line in data: if line["reference"] == manual_reference and line.get("id"): total_amount = -line["amount"] + line["original_amount_unsigned"] original_amount = line["original_amount_unsigned"] reconcile_auxiliary_id, lines = self._get_reconcile_line( self.env["account.move.line"].browse(line["id"]), "other", is_counterpart=True, reconcile_auxiliary_id=reconcile_auxiliary_id, max_amount=original_amount, move=True, ) new_data += lines new_data.append( { "reference": "reconcile_auxiliary;%s" % reconcile_auxiliary_id, "id": False, "account_id": line["account_id"], "partner_id": line.get("partner_id"), "date": line["date"], "name": line["name"], "amount": -total_amount, "credit": total_amount if total_amount > 0 else 0.0, "debit": -total_amount if total_amount < 0 else 0.0, "kind": "other", "currency_id": line["currency_id"], "line_currency_id": line["currency_id"], "currency_amount": -total_amount, } ) reconcile_auxiliary_id += 1 else: new_data.append(line) self.reconcile_data_info = self._recompute_suspense_line( new_data, reconcile_auxiliary_id, self.manual_reference, ) self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False) def action_to_check(self): self.ensure_one() self.move_id.to_check = True if self.can_reconcile and self.journal_id.reconcile_mode == "edit": self.reconcile_bank_line() def action_checked(self): self.ensure_one() self.move_id.to_check = False def _get_reconcile_line( self, line, kind, is_counterpart=False, max_amount=False, from_unreconcile=False, reconcile_auxiliary_id=False, move=False, ): new_vals = super()._get_reconcile_line( line, kind, is_counterpart=is_counterpart, max_amount=max_amount, from_unreconcile=from_unreconcile, move=move, ) rates = [] for vals in new_vals: rate = False if vals["partner_id"] is False: vals["partner_id"] = (False, self.partner_name) 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): 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 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) 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, self.company_id.currency_id, self.company_id, self.date, ) return self.company_id.currency_id.round(to_amount - amount) def _compute_exchange_rate( self, vals, line, reconcile_auxiliary_id, ): foreign_currency = ( self.currency_id != self.company_id.currency_id or self.foreign_currency_id or vals["currency_id"] != vals["line_currency_id"] ) if not foreign_currency or self.is_reconciled: return reconcile_auxiliary_id, False currency = self.env["res.currency"].browse(vals["line_currency_id"]) amount = self._get_exchange_rate_amount( vals.get("amount", 0), vals.get("currency_amount", 0), currency, line ) if currency.is_zero(amount): return reconcile_auxiliary_id, False account = self.company_id.expense_currency_exchange_account_id if amount < 0: account = self.company_id.income_currency_exchange_account_id data = { "is_exchange_counterpart": True, "original_exchange_line_id": line.id, "reference": "reconcile_auxiliary;%s" % reconcile_auxiliary_id, "id": False, "account_id": (account.id, account.display_name), "partner_id": False, "date": fields.Date.to_string(self.date), "name": self.payment_ref or self.name, "amount": amount, "net_amount": amount, "credit": -amount if amount < 0 else 0.0, "debit": amount if amount > 0 else 0.0, "kind": "other", "currency_id": self.company_id.currency_id.id, "line_currency_id": currency.id, "currency_amount": 0, } reconcile_auxiliary_id += 1 return reconcile_auxiliary_id, data def add_statement(self): self.ensure_one() action = self.env["ir.actions.act_window"]._for_xml_id( "account_reconcile_oca.account_bank_statement_action_edit" ) previous_line_with_statement = self.env["account.bank.statement.line"].search( [ ("internal_index", "<", self.internal_index), ("journal_id", "=", self.journal_id.id), ("state", "=", "posted"), ("statement_id", "!=", self.statement_id.id), ("statement_id", "!=", False), ], limit=1, ) balance = previous_line_with_statement.statement_id.balance_end_real action["context"] = { "default_journal_id": self.journal_id.id, "default_balance_start": balance, "split_line_id": self.id, } return action def _get_reconcile_currency(self): return ( self.foreign_currency_id or self.journal_id.currency_id or self.company_id.currency_id ) def add_multiple_lines(self, domain): res = super().add_multiple_lines(domain) lines = self.env["account.move.line"].search(domain) for line in lines: self._add_account_move_line(line, keep_current=True) return res def _retrieve_partner(self): if self.env.context.get("skip_retrieve_partner"): # This hook can be used, for example, when importing files. # With large databases, we already have the information, moreover, # the data might be preloaded, so it has no sense to import it again return self.partner_id return super()._retrieve_partner()