mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-26 07:22:02 +02:00
account_reconcile_oca
This commit is contained in:
parent
64fdc5b0df
commit
a8804cdf59
95 changed files with 17541 additions and 0 deletions
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue