account_reconcile_oca

This commit is contained in:
Ernad Husremovic 2025-10-25 10:34:41 +02:00
parent 64fdc5b0df
commit a8804cdf59
95 changed files with 17541 additions and 0 deletions

View file

@ -0,0 +1,8 @@
from . import account_reconcile_abstract
from . import account_journal
from . import account_bank_statement_line
from . import account_bank_statement
from . import account_account_reconcile
from . import account_move_line
from . import res_company
from . import res_config_settings

View file

@ -0,0 +1,215 @@
# Copyright 2023 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class CharId(fields.Id):
type = "string"
column_type = ("varchar", fields.pg_varchar())
class AccountAccountReconcile(models.Model):
_name = "account.account.reconcile"
_description = "Account Account Reconcile"
_inherit = "account.reconcile.abstract"
_auto = False
reconcile_data_info = fields.Serialized(inverse="_inverse_reconcile_data_info")
partner_id = fields.Many2one("res.partner", readonly=True)
account_id = fields.Many2one("account.account", readonly=True)
name = fields.Char(readonly=True)
is_reconciled = fields.Boolean(readonly=True)
active = fields.Boolean(default=True)
@property
def _table_query(self):
return "%s %s %s %s %s" % (
self._select(),
self._from(),
self._where(),
self._groupby(),
self._having(),
)
def _select(self):
account_account_name_field = (
self.env["ir.model.fields"]
.sudo()
.search([("model", "=", "account.account"), ("name", "=", "name")])
)
account_name = (
f"a.name ->> '{self.env.user.lang}'"
if account_account_name_field.translate
else "a.name"
)
return f"""
SELECT
min(aml.id) as id,
MAX({account_name}) as name,
CASE
WHEN a.account_type in ('asset_receivable', 'liability_payable')
THEN aml.partner_id
ELSE NULL
END as partner_id,
a.id as account_id,
FALSE as is_reconciled,
aml.currency_id as currency_id,
a.company_id,
false as foreign_currency_id,
(
SUM(
CASE WHEN aml.amount_residual > 0
THEN aml.amount_residual
ELSE 0 END
) > 0
AND SUM(
CASE WHEN aml.amount_residual < 0
THEN -aml.amount_residual
ELSE 0 END
) > 0
) as active
"""
def _from(self):
return """
FROM
account_account a
INNER JOIN account_move_line aml ON aml.account_id = a.id
INNER JOIN account_move am ON am.id = aml.move_id
"""
def _where(self):
return """
WHERE a.reconcile
AND am.state = 'posted'
"""
def _groupby(self):
return """
GROUP BY
a.id,
CASE
WHEN a.account_type in ('asset_receivable', 'liability_payable')
THEN aml.partner_id
ELSE NULL
END,
aml.currency_id,
a.company_id
"""
def _having(self):
return """
"""
def _compute_reconcile_data_info(self):
data_obj = self.env["account.account.reconcile.data"]
for record in self:
if self.env.context.get("default_account_move_lines"):
data = {
"data": [],
"counterparts": self.env.context.get("default_account_move_lines"),
}
record.reconcile_data_info = self._recompute_data(data)
continue
data_record = data_obj.search(
[("user_id", "=", self.env.user.id), ("reconcile_id", "=", record.id)]
)
if data_record:
record.reconcile_data_info = data_record.data
else:
record.reconcile_data_info = {"data": [], "counterparts": []}
def _inverse_reconcile_data_info(self):
data_obj = self.env["account.account.reconcile.data"]
for record in self:
data_record = data_obj.search(
[("user_id", "=", self.env.user.id), ("reconcile_id", "=", record.id)]
)
if data_record:
data_record.data = record.reconcile_data_info
else:
data_obj.create(
{
"reconcile_id": record.id,
"user_id": self.env.user.id,
"data": record.reconcile_data_info,
}
)
@api.onchange("add_account_move_line_id")
def _onchange_add_account_move_line(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
if move_line.id not in data["counterparts"]:
data["counterparts"].append(move_line.id)
elif not keep_current:
del data["counterparts"][data["counterparts"].index(move_line.id)]
self.reconcile_data_info = self._recompute_data(data)
@api.onchange("manual_reference", "manual_delete")
def _onchange_manual_reconcile_reference(self):
self.ensure_one()
data = self.reconcile_data_info
counterparts = []
for line in data["data"]:
if line["reference"] == self.manual_reference:
if self.manual_delete:
continue
counterparts.append(line["id"])
data["counterparts"] = counterparts
self.reconcile_data_info = self._recompute_data(data)
self.manual_delete = False
self.manual_reference = False
def _recompute_data(self, data):
new_data = {"data": [], "counterparts": data["counterparts"]}
counterparts = data["counterparts"]
amount = 0.0
for line_id in counterparts:
max_amount = amount if line_id == counterparts[-1] else 0
lines = self._get_reconcile_line(
self.env["account.move.line"].browse(line_id),
"other",
is_counterpart=True,
max_amount=max_amount,
move=True,
)
new_data["data"] += lines
amount += sum(line["amount"] for line in lines)
return new_data
def clean_reconcile(self):
self.ensure_one()
self.reconcile_data_info = {"data": [], "counterparts": []}
def reconcile(self):
lines = self.env["account.move.line"].browse(
self.reconcile_data_info["counterparts"]
)
lines.reconcile()
data_record = self.env["account.account.reconcile.data"].search(
[("user_id", "=", self.env.user.id), ("reconcile_id", "=", self.id)]
)
data_record.unlink()
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
class AccountAccountReconcileData(models.TransientModel):
_name = "account.account.reconcile.data"
_description = "Reconcile data model to store user info"
user_id = fields.Many2one("res.users", required=True)
reconcile_id = fields.Integer(required=True)
data = fields.Serialized()

View file

@ -0,0 +1,30 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models
from odoo.tools.safe_eval import safe_eval
class AccountBankStatement(models.Model):
_inherit = "account.bank.statement"
def action_open_statement(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id(
"account_reconcile_oca.account_bank_statement_action_edit"
)
action["res_id"] = self.id
return action
def action_open_statement_lines(self):
"""Open in reconciling view directly"""
self.ensure_one()
if not self:
return {}
action = self.env["ir.actions.act_window"]._for_xml_id(
"account_reconcile_oca.action_bank_statement_line_reconcile"
)
action["domain"] = [("statement_id", "=", self.id)]
action["context"] = safe_eval(
action["context"], locals_dict={"active_id": self._context.get("active_id")}
)
return action

View file

@ -0,0 +1,45 @@
# Copyright 2023 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, fields, models
class AccountJournal(models.Model):
_inherit = "account.journal"
reconcile_mode = fields.Selection(
[("edit", "Edit Move"), ("keep", "Keep Suspense Accounts")],
default="edit",
required=True,
)
company_currency_id = fields.Many2one(
related="company_id.currency_id", string="Company Currency"
)
reconcile_aggregate = fields.Selection(
[
("statement", "Statement"),
("day", "Day"),
("week", "Week"),
("month", "Month"),
],
string="Reconcile aggregation",
help="Aggregation to use on reconcile view",
)
def get_rainbowman_message(self):
self.ensure_one()
if self.get_journal_dashboard_datas()["number_to_reconcile"] > 0:
return False
return _("Well done! Everything has been reconciled")
def open_action(self):
"""
Return OCA *Reconcile All* when core *Bank Statements* tree is requested;
leave other actions unchanged.
"""
action = super().open_action()
if action.get("xml_id") == "account.action_bank_statement_tree":
action = self.env["ir.actions.actions"]._for_xml_id(
"account_reconcile_oca.action_bank_statement_line_reconcile_all"
)
return action

View file

@ -0,0 +1,34 @@
# Copyright 2023 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, models
from odoo.exceptions import ValidationError
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
def action_reconcile_manually(self):
if not self:
return {}
accounts = self.mapped("account_id")
if len(accounts) > 1:
raise ValidationError(
_("You can only reconcile journal items belonging to the same account.")
)
partner = self.mapped("partner_id")
action = self.env["ir.actions.act_window"]._for_xml_id(
"account_reconcile_oca.account_account_reconcile_act_window"
)
action["domain"] = [("account_id", "=", self.mapped("account_id").id)]
if len(partner) == 1 and self.account_id.account_type in [
"asset_receivable",
"liability_payable",
]:
action["domain"] += [("partner_id", "=", partner.id)]
action["context"] = self.env.context.copy()
action["context"]["default_account_move_lines"] = self.filtered(
lambda r: not r.reconciled
).ids
return action

View file

@ -0,0 +1,130 @@
# Copyright 2023 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
from odoo.tools import float_is_zero
class AccountReconcileAbstract(models.AbstractModel):
_name = "account.reconcile.abstract"
_description = "Account Reconcile Abstract"
reconcile_data_info = fields.Serialized(
compute="_compute_reconcile_data_info",
prefetch=False,
)
company_id = fields.Many2one("res.company")
add_account_move_line_id = fields.Many2one(
"account.move.line",
check_company=True,
store=False,
default=False,
prefetch=False,
)
manual_reference = fields.Char(store=False, default=False, prefetch=False)
manual_delete = fields.Boolean(
store=False,
default=False,
prefetch=False,
)
currency_id = fields.Many2one("res.currency", readonly=True)
foreign_currency_id = fields.Many2one("res.currency")
company_currency_id = fields.Many2one(
related="company_id.currency_id", string="Company Currency"
)
def _get_reconcile_currency(self):
return self.currency_id or self.company_id._currency_id
def _get_reconcile_line(
self,
line,
kind,
is_counterpart=False,
max_amount=False,
from_unreconcile=False,
move=False,
is_reconciled=False,
):
date = self.date if "date" in self._fields else line.date
original_amount = amount = net_amount = line.debit - line.credit
line_currency = line.currency_id
if is_counterpart:
currency_amount = -line.amount_residual_currency or line.amount_residual
amount = -line.amount_residual
currency = line.currency_id or line.company_id.currency_id
original_amount = net_amount = -line.amount_residual
if max_amount:
dest_currency = self._get_reconcile_currency()
if currency == dest_currency:
real_currency_amount = currency_amount
elif self.company_id.currency_id == dest_currency:
real_currency_amount = amount
else:
real_currency_amount = self.company_id.currency_id._convert(
amount,
dest_currency,
self.company_id,
date,
)
if (
-real_currency_amount > max_amount > 0
or -real_currency_amount < max_amount < 0
):
currency_max_amount = self._get_reconcile_currency()._convert(
max_amount, currency, self.company_id, date
)
amount = currency_max_amount
net_amount = -max_amount
currency_amount = -amount
amount = currency._convert(
currency_amount,
self.company_id.currency_id,
self.company_id,
date,
)
elif is_reconciled:
currency_amount = line.amount_currency
else:
currency_amount = self.amount_currency or self.amount
line_currency = self._get_reconcile_currency()
vals = {
"move_id": move and line.move_id.id,
"move": move and line.move_id.name,
"reference": "account.move.line;%s" % line.id,
"id": line.id,
"account_id": line.account_id.name_get()[0],
"partner_id": line.partner_id and line.partner_id.name_get()[0] or False,
"date": fields.Date.to_string(line.date),
"name": line.name or line.move_id.name,
"debit": amount if amount > 0 else 0.0,
"credit": -amount if amount < 0 else 0.0,
"amount": amount,
"net_amount": amount - net_amount,
"currency_id": self.company_id.currency_id.id,
"line_currency_id": line_currency.id,
"currency_amount": currency_amount,
"analytic_distribution": line.analytic_distribution,
"kind": kind,
}
if from_unreconcile:
vals.update(
{
"credit": vals["debit"] and from_unreconcile["debit"],
"debit": vals["credit"] and from_unreconcile["credit"],
"amount": from_unreconcile["amount"],
"net_amount": from_unreconcile["amount"],
"currency_amount": from_unreconcile["currency_amount"],
}
)
if not float_is_zero(
amount - original_amount, precision_digits=line.currency_id.decimal_places
):
vals["original_amount"] = abs(original_amount)
vals["original_amount_unsigned"] = original_amount
if is_counterpart:
vals["counterpart_line_ids"] = line.ids
return [vals]
def add_multiple_lines(self, domain):
self.ensure_one()

View file

@ -0,0 +1,39 @@
# Copyright 2024 Dixmit
# Copyright 2025 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResCompany(models.Model):
_inherit = "res.company"
reconcile_aggregate = fields.Selection(
selection=lambda self: self.env["account.journal"]
._fields["reconcile_aggregate"]
.selection
)
def _get_fiscalyear_lock_statement_lines_redirect_action(
self, unreconciled_statement_lines
):
"""Define the appropriate views that this method will have, by default the
account module does not add any.
"""
action = super()._get_fiscalyear_lock_statement_lines_redirect_action(
unreconciled_statement_lines
)
if len(unreconciled_statement_lines) == 1:
custom_action = self.env["ir.actions.actions"]._for_xml_id(
"account_reconcile_oca.action_bank_statement_line_create"
)
action.update(views=custom_action["views"])
else:
custom_action = self.env["ir.actions.actions"]._for_xml_id(
"account_reconcile_oca.action_bank_statement_line_reconcile_all"
)
action.update(
view_mode=custom_action["view_mode"],
views=custom_action["views"],
)
return action

View file

@ -0,0 +1,12 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
reconcile_aggregate = fields.Selection(
related="company_id.reconcile_aggregate", readonly=False
)