Files
account-reconcile/account_reconcile_oca/models/account_bank_statement_line.py
Florian da Costa cd165e299d [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
2025-01-03 13:00:21 +01:00

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
)