mirror of
https://github.com/OCA/account-reconcile.git
synced 2025-01-20 12:27:39 +02:00
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
1151 lines
45 KiB
Python
1151 lines
45 KiB
Python
# 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
|
|
from odoo.exceptions import UserError
|
|
from odoo.fields import first
|
|
from odoo.tools import 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(
|
|
string="Is Manual in Currency?",
|
|
readonly=True,
|
|
store=False,
|
|
prefetch=False,
|
|
)
|
|
manual_in_currency_id = fields.Many2one(
|
|
comodel_name="res.currency",
|
|
string="Manual In Currency",
|
|
readonly=True,
|
|
store=False,
|
|
prefetch=False,
|
|
)
|
|
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")
|
|
statement_complete = fields.Boolean(
|
|
related="statement_id.is_complete",
|
|
)
|
|
statement_valid = fields.Boolean(
|
|
related="statement_id.is_valid",
|
|
)
|
|
statement_balance_end_real = fields.Monetary(
|
|
related="statement_id.balance_end_real",
|
|
)
|
|
statement_name = fields.Char(
|
|
string="Statement Name",
|
|
related="statement_id.name",
|
|
)
|
|
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:
|
|
data = self.reconcile_data_info["data"]
|
|
new_data = []
|
|
is_new_line = True
|
|
pending_amount = 0.0
|
|
for line in data:
|
|
if line["kind"] != "suspense":
|
|
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", []
|
|
):
|
|
is_new_line = False
|
|
else:
|
|
new_data.append(line)
|
|
if is_new_line:
|
|
reconcile_auxiliary_id, lines = self._get_reconcile_line(
|
|
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(
|
|
new_data,
|
|
self.reconcile_data_info["reconcile_auxiliary_id"],
|
|
self.manual_reference,
|
|
)
|
|
self.can_reconcile = self.reconcile_data_info.get("can_reconcile", False)
|
|
self.add_account_move_line_id = 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:
|
|
|
|
suspense_line = {
|
|
"reference": "reconcile_auxiliary;%s" % reconcile_auxiliary_id,
|
|
"id": False,
|
|
"account_id": self.journal_id.suspense_account_id.name_get()[0],
|
|
"partner_id": self.partner_id
|
|
and self.partner_id.name_get()[0]
|
|
or (False, self.partner_name),
|
|
"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.name_get()[0]
|
|
or [False, False]
|
|
)
|
|
!= line.get("partner_id")
|
|
or self.analytic_distribution != line.get("analytic_distribution", False)
|
|
)
|
|
|
|
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):
|
|
return {
|
|
"name": self.manual_name,
|
|
"partner_id": self.manual_partner_id
|
|
and self.manual_partner_id.name_get()[0]
|
|
or (False, self.partner_name),
|
|
"account_id": self.manual_account_id.name_get()[0]
|
|
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,
|
|
}
|
|
|
|
@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
|
|
default_name = ""
|
|
for line_data in data:
|
|
if line_data["kind"] == "suspense":
|
|
continue
|
|
new_data.append(line_data)
|
|
liquidity_amount += line_data["amount"]
|
|
if line_data["kind"] == "liquidity":
|
|
default_name = line_data["name"]
|
|
for line in reconcile_model._get_write_off_move_lines_dict(
|
|
-liquidity_amount, self._retrieve_partner().id
|
|
):
|
|
new_line = line.copy()
|
|
new_line["name"] = new_line.get("name") or default_name
|
|
amount = line.get("balance")
|
|
if self.foreign_currency_id:
|
|
amount = self.foreign_currency_id.compute(
|
|
amount, self.journal_id.currency_id or self.company_currency_id
|
|
)
|
|
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": self.env["account.account"]
|
|
.browse(line["account_id"])
|
|
.name_get()[0],
|
|
"date": fields.Date.to_string(self.date),
|
|
"line_currency_id": self.company_id.currency_id.id,
|
|
"currency_id": self.company_id.currency_id.id,
|
|
"currency_amount": amount,
|
|
"name": line.get("name") or self.payment_ref,
|
|
}
|
|
)
|
|
reconcile_auxiliary_id += 1
|
|
if line.get("partner_id"):
|
|
new_line["partner_id"] = (
|
|
self.env["res.partner"].browse(line["partner_id"]).name_get()[0]
|
|
)
|
|
elif self.partner_id:
|
|
new_line["partner_id"] = self.partner_id.name_get()[0]
|
|
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"):
|
|
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
|
|
):
|
|
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)
|
|
|
|
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,
|
|
)
|
|
.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)
|
|
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 _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.name_get()[0],
|
|
"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": self.company_id.currency_id.id,
|
|
"currency_amount": amount,
|
|
}
|
|
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,
|
|
)
|
|
action["context"] = {
|
|
"default_journal_id": self.journal_id.id,
|
|
"default_balance_start": previous_line_with_statement.statement_id.balance_end_real,
|
|
"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
|
|
)
|