Files
account-reconcile/account_reconciliation_widget/models/reconciliation_widget.py
Pedro M. Baeza 963fa23483 [IMP] account_reconciliation_widget: Performance opening reconciliation widget
There was an unneeded mapped of the initial statements that fetches a lot of
data from statement lines that are not going to be used later, so let's
remove it and optimize a bit the initial opening time.

In a customer database, we have improved the opening time from 120 seconds
to 15.

TT48753
2024-08-19 08:36:42 +02:00

1183 lines
47 KiB
Python

import copy
from psycopg2 import sql
from odoo import _, api, models
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.tools.misc import format_date, formatLang, parse_date
class AccountReconciliation(models.AbstractModel):
_name = "account.reconciliation.widget"
_description = "Account Reconciliation widget"
####################################################
# Public
####################################################
@api.model
def process_bank_statement_line(self, st_line_ids, data):
"""Handles data sent from the bank statement reconciliation widget
(and can otherwise serve as an old-API bridge)
:param st_line_ids
:param list of dicts data: must contains the keys
'counterpart_aml_dicts', 'payment_aml_ids' and 'new_aml_dicts',
whose value is the same as described in process_reconciliation
except that ids are used instead of recordsets.
:returns dict: used as a hook to add additional keys.
"""
st_lines = self.env["account.bank.statement.line"].browse(st_line_ids)
AccountMoveLine = self.env["account.move.line"]
ctx = dict(self._context, force_price_include=False)
processed_moves = self.env["account.move"]
for st_line, datum in zip(st_lines, copy.deepcopy(data)):
payment_aml_rec = AccountMoveLine.browse(datum.get("payment_aml_ids", []))
for aml_dict in datum.get("counterpart_aml_dicts", []):
aml_dict["move_line"] = AccountMoveLine.browse(
aml_dict["counterpart_aml_id"]
)
del aml_dict["counterpart_aml_id"]
vals = {}
if datum.get("partner_id") is not None:
vals["partner_id"] = datum["partner_id"]
if datum.get("ref") is not None:
vals["ref"] = datum["ref"]
if vals:
st_line.write(vals)
ctx["default_to_check"] = datum.get("to_check")
moves = st_line.with_context(ctx).process_reconciliation(
datum.get("counterpart_aml_dicts", []),
payment_aml_rec,
datum.get("new_aml_dicts", []),
)
processed_moves = processed_moves | moves
return {
"moves": processed_moves.ids,
"statement_line_ids": processed_moves.mapped(
"line_ids.statement_line_id"
).ids,
}
@api.model
def get_move_lines_for_bank_statement_line(
self,
st_line_id,
partner_id=None,
excluded_ids=None,
search_str=False,
offset=0,
limit=None,
mode=None,
):
"""Returns move lines for the bank statement reconciliation widget,
formatted as a list of dicts
:param st_line_id: ids of the statement lines
:param partner_id: optional partner id to select only the moves
line corresponding to the partner
:param excluded_ids: optional move lines ids excluded from the
result
:param search_str: optional search (can be the amout, display_name,
partner name, move line name)
:param offset: useless but kept in stable to preserve api
:param limit: number of the result to search
:param mode: 'rp' for receivable/payable or 'other'
"""
st_line = self.env["account.bank.statement.line"].browse(st_line_id)
# Blue lines = payment on bank account not assigned to a statement yet
aml_accounts = [
st_line.journal_id.default_account_id.id,
]
if partner_id is None:
partner_id = st_line.partner_id.id
domain = self._domain_move_lines_for_reconciliation(
st_line,
aml_accounts,
partner_id,
excluded_ids=excluded_ids,
search_str=search_str,
mode=mode,
)
from_clause, where_clause, where_clause_params = (
self.env["account.move.line"]._where_calc(domain).get_sql()
)
query_str = sql.SQL(
"""
SELECT "account_move_line".id, COUNT(*) OVER() FROM {from_clause}
{where_str}
ORDER BY ("account_move_line".debit -
"account_move_line".credit) = {amount} DESC,
"account_move_line".date_maturity ASC,
"account_move_line".id ASC
{limit_str}
""".format(
from_clause=from_clause,
where_str=where_clause and (" WHERE %s" % where_clause) or "",
amount=st_line.amount,
limit_str=limit and " LIMIT %s" or "",
)
)
params = where_clause_params + (limit and [limit] or [])
self.env["account.move"].flush()
self.env["account.move.line"].flush()
self.env["account.bank.statement"].flush()
self._cr.execute(query_str, params)
res = self._cr.fetchall()
try:
# All records will have the same count value, just get the 1st one
recs_count = res[0][1]
except IndexError:
recs_count = 0
aml_recs = self.env["account.move.line"].browse([i[0] for i in res])
target_currency = (
st_line.foreign_currency_id
or st_line.journal_id.currency_id
or st_line.journal_id.company_id.currency_id
)
return self._prepare_move_lines(
aml_recs,
target_currency=target_currency,
target_date=st_line.date,
recs_count=recs_count,
)
@api.model
def _get_bank_statement_line_partners(self, st_lines):
params = []
# Add the res.partner.ban's IR rules. In case partners are not shared
# between companies, identical bank accounts may exist in a company we
# don't have access to.
ir_rules_query = self.env["res.partner.bank"]._where_calc([])
self.env["res.partner.bank"]._apply_ir_rules(ir_rules_query, "read")
from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
if where_clause:
where_bank = ("AND %s" % where_clause).replace(
'"res_partner_bank"', '"bank"'
)
params += where_clause_params
else:
where_bank = ""
# Add the res.partner's IR rules. In case partners are not shared
# between companies, identical partners may exist in a company we don't
# have access to.
ir_rules_query = self.env["res.partner"]._where_calc([])
self.env["res.partner"]._apply_ir_rules(ir_rules_query, "read")
from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
if where_clause:
where_partner = ("AND %s" % where_clause).replace('"res_partner"', '"p3"')
params += where_clause_params
else:
where_partner = ""
query = """
SELECT
st_line.id AS id,
COALESCE(p1.id,p2.id,p3.id) AS partner_id
FROM account_bank_statement_line st_line
"""
query += "INNER JOIN account_move m ON m.id = st_line.move_id \n"
query += (
"LEFT JOIN res_partner_bank bank ON bank.id = m.partner_bank_id OR "
"bank.sanitized_acc_number "
"ILIKE regexp_replace(st_line.account_number, '\\W+', '', 'g') %s\n"
% (where_bank)
)
query += "LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id \n"
query += "LEFT JOIN res_partner p2 ON bank.partner_id=p2.id \n"
# By definition the commercial partner_id doesn't have a parent_id set
query += (
"LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name %s "
"AND p3.parent_id is NULL \n" % (where_partner)
)
query += "WHERE st_line.id IN %s"
params += [tuple(st_lines.ids)]
self._cr.execute(query, params)
result = {}
for res in self._cr.dictfetchall():
result[res["id"]] = res["partner_id"]
return result
@api.model
def get_bank_statement_line_data(self, st_line_ids, excluded_ids=None):
"""Returns the data required to display a reconciliation widget, for
each statement line in self
:param st_line_id: ids of the statement lines
:param excluded_ids: optional move lines ids excluded from the
result
"""
results = {
"lines": [],
"value_min": 0,
"value_max": 0,
"reconciled_aml_ids": [],
}
if not st_line_ids:
return results
excluded_ids = excluded_ids or []
# Make a search to preserve the table's order.
bank_statement_lines = self.env["account.bank.statement.line"].search(
[("id", "in", st_line_ids)]
)
results["value_max"] = len(bank_statement_lines)
reconcile_model = self.env["account.reconcile.model"].search(
[("rule_type", "!=", "writeoff_button")]
)
# Search for missing partners when opening the reconciliation widget.
if bank_statement_lines:
partner_map = self._get_bank_statement_line_partners(bank_statement_lines)
matching_amls = reconcile_model._apply_rules(
bank_statement_lines, excluded_ids=excluded_ids, partner_map=partner_map
)
# Iterate on st_lines to keep the same order in the results list.
bank_statements_left = self.env["account.bank.statement"]
for line in bank_statement_lines:
if matching_amls[line.id].get("status") == "reconciled":
reconciled_move_lines = matching_amls[line.id].get("reconciled_lines")
results["value_min"] += 1
results["reconciled_aml_ids"] += (
reconciled_move_lines and reconciled_move_lines.ids or []
)
else:
aml_ids = matching_amls[line.id]["aml_ids"]
bank_statements_left += line.statement_id
target_currency = (
line.foreign_currency_id
or line.journal_id.currency_id
or line.journal_id.company_id.currency_id
)
amls = aml_ids and self.env["account.move.line"].browse(aml_ids)
line_vals = {
"st_line": self._get_statement_line(line),
"reconciliation_proposition": aml_ids
and self._prepare_move_lines(
amls, target_currency=target_currency, target_date=line.date
)
or [],
"model_id": matching_amls[line.id].get("model")
and matching_amls[line.id]["model"].id,
"write_off": matching_amls[line.id].get("status") == "write_off",
}
if not line.partner_id:
partner = False
if matching_amls[line.id].get("partner"):
partner = matching_amls[line.id]["partner"]
elif partner_map.get(line.id):
partner = self.env["res.partner"].browse(partner_map[line.id])
if partner:
line_vals.update(
{
"partner_id": partner.id,
"partner_name": partner.name,
}
)
results["lines"].append(line_vals)
return results
@api.model
def get_bank_statement_data(self, bank_statement_line_ids, srch_domain=None):
"""Get statement lines of the specified statements or all unreconciled
statement lines and try to automatically reconcile them / find them
a partner.
Return ids of statement lines left to reconcile and other data for
the reconciliation widget.
:param bank_statement_line_ids: ids of the bank statement lines
"""
if not bank_statement_line_ids:
return {}
query = """
SELECT line.id
FROM account_bank_statement_line line
LEFT JOIN res_partner p on p.id = line.partner_id
INNER JOIN account_bank_statement st ON line.statement_id = st.id
AND st.state = 'posted'
WHERE line.is_reconciled = FALSE
AND line.amount != 0.0
AND line.id IN %(ids)s
GROUP BY line.id
"""
self.env.cr.execute(query, {"ids": tuple(bank_statement_line_ids)})
domain = [["id", "in", [line.get("id") for line in self.env.cr.dictfetchall()]]]
if srch_domain is not None:
domain += srch_domain
bank_statement_lines = self.env["account.bank.statement.line"].search(domain)
results = self.get_bank_statement_line_data(
bank_statement_lines.ids,
excluded_ids=bank_statement_lines.move_id.line_ids.ids,
)
bank_statement_lines_left = self.env["account.bank.statement.line"].browse(
[line["st_line"]["id"] for line in results["lines"]]
)
bank_statements_left = bank_statement_lines_left.mapped("statement_id")
data = bank_statements_left.read(["name", "journal_id"])
data = data and data[0] or {}
results.update(
{
"statement_id": len(bank_statements_left) == 1
and bank_statements_left[0].id
or False,
"statement_name": len(bank_statements_left) == 1
and data.get("name")
or False,
"journal_id": data and data.get("journal_id", [False])[0] or False,
"notifications": [],
}
)
if len(results["lines"]) < len(bank_statement_lines):
results["notifications"].append(
{
"type": "info",
"template": "reconciliation.notification.reconciled",
"reconciled_aml_ids": results["reconciled_aml_ids"],
"nb_reconciled_lines": results["value_min"],
"details": {
"name": _("Journal Items"),
"model": "account.move.line",
"ids": results["reconciled_aml_ids"],
},
}
)
return results
@api.model
def get_move_lines_for_manual_reconciliation(
self,
account_id,
partner_id=False,
excluded_ids=None,
search_str=False,
offset=0,
limit=None,
target_currency_id=False,
):
"""Returns unreconciled move lines for an account or a partner+account,
formatted for the manual reconciliation widget"""
Account_move_line = self.env["account.move.line"]
Account = self.env["account.account"]
Currency = self.env["res.currency"]
domain = self._domain_move_lines_for_manual_reconciliation(
account_id, partner_id, excluded_ids, search_str
)
recs_count = Account_move_line.search_count(domain)
lines = Account_move_line.search(
domain, limit=limit, order="date_maturity desc, id desc"
)
if target_currency_id:
target_currency = Currency.browse(target_currency_id)
else:
account = Account.browse(account_id)
target_currency = account.currency_id or account.company_id.currency_id
return self._prepare_move_lines(
lines, target_currency=target_currency, recs_count=recs_count
)
@api.model
def get_all_data_for_manual_reconciliation(self, partner_ids, account_ids):
"""Returns the data required for the invoices & payments matching of
partners/accounts.
If an argument is None, fetch all related reconciliations. Use [] to
fetch nothing.
"""
MoveLine = self.env["account.move.line"]
aml_ids = (
self._context.get("active_ids")
and self._context.get("active_model") == "account.move.line"
and tuple(self._context.get("active_ids"))
)
if aml_ids:
aml = MoveLine.browse(aml_ids)
account = aml[0].account_id
currency = account.currency_id or account.company_id.currency_id
return {
"accounts": [
{
"reconciliation_proposition": self._prepare_move_lines(
aml, target_currency=currency
),
"company_id": account.company_id.id,
"currency_id": currency.id,
"mode": "accounts",
"account_id": account.id,
"account_name": account.name,
"account_code": account.code,
}
],
"customers": [],
"suppliers": [],
}
# If we have specified partner_ids, don't return the list of
# reconciliation for specific accounts as it will show entries that are
# not reconciled with other partner. Asking for a specific partner on a
# specific account is never done.
accounts_data = []
if not partner_ids or not any(partner_ids):
accounts_data = self.get_data_for_manual_reconciliation(
"account", account_ids
)
return {
"customers": self.get_data_for_manual_reconciliation(
"partner", partner_ids, "receivable"
),
"suppliers": self.get_data_for_manual_reconciliation(
"partner", partner_ids, "payable"
),
"accounts": accounts_data,
}
@api.model
def get_data_for_manual_reconciliation(
self, res_type, res_ids=None, account_type=None
):
"""Returns the data required for the invoices & payments matching of
partners/accounts (list of dicts).
If no res_ids is passed, returns data for all partners/accounts that can
be reconciled.
:param res_type: either 'partner' or 'account'
:param res_ids: ids of the partners/accounts to reconcile, use None to
fetch data indiscriminately of the id, use [] to prevent from
fetching any data at all.
:param account_type: if a partner is both customer and vendor, you can
use 'payable' to reconcile the vendor-related journal entries and
'receivable' for the customer-related entries.
"""
Account = self.env["account.account"]
Partner = self.env["res.partner"]
if res_ids is not None and len(res_ids) == 0:
# Note : this short-circuiting is better for performances, but also
# required since postgresql doesn't implement empty list (so 'AND id
# in ()' is useless)
return []
res_ids = res_ids and tuple(res_ids)
assert res_type in ("partner", "account")
assert account_type in ("payable", "receivable", None)
is_partner = res_type == "partner"
res_alias = is_partner and "p" or "a"
aml_ids = (
self._context.get("active_ids")
and self._context.get("active_model") == "account.move.line"
and tuple(self._context.get("active_ids"))
)
all_entries = self._context.get("all_entries", False)
all_entries_query = """
AND EXISTS (
SELECT NULL
FROM account_move_line l
JOIN account_move move ON l.move_id = move.id
JOIN account_journal journal ON l.journal_id = journal.id
WHERE l.account_id = a.id
{inner_where}
AND l.amount_residual != 0
AND move.state = 'posted'
)
""".format(
inner_where=is_partner and "AND l.partner_id = p.id" or " "
)
only_dual_entries_query = """
AND EXISTS (
SELECT NULL
FROM account_move_line l
JOIN account_move move ON l.move_id = move.id
JOIN account_journal journal ON l.journal_id = journal.id
WHERE l.account_id = a.id
{inner_where}
AND l.amount_residual > 0
AND move.state = 'posted'
)
AND EXISTS (
SELECT NULL
FROM account_move_line l
JOIN account_move move ON l.move_id = move.id
JOIN account_journal journal ON l.journal_id = journal.id
WHERE l.account_id = a.id
{inner_where}
AND l.amount_residual < 0
AND move.state = 'posted'
)
""".format(
inner_where=is_partner and "AND l.partner_id = p.id" or " "
)
query = sql.SQL(
"""
SELECT {select} account_id, account_name, account_code, max_date
FROM (
SELECT {inner_select}
a.id AS account_id,
a.name AS account_name,
a.code AS account_code,
MAX(l.write_date) AS max_date
FROM
account_move_line l
RIGHT JOIN account_account a ON (a.id = l.account_id)
RIGHT JOIN account_account_type at
ON (at.id = a.user_type_id)
{inner_from}
WHERE
a.reconcile IS TRUE
AND l.full_reconcile_id is NULL
{where1}
{where2}
{where3}
AND l.company_id = {company_id}
{where4}
{where5}
GROUP BY {group_by1} a.id, a.name, a.code {group_by2}
{order_by}
) as s
{outer_where}
""".format(
select=is_partner
and "partner_id, partner_name, to_char(last_time_entries_checked, "
"'YYYY-MM-DD') AS last_time_entries_checked,"
or " ",
inner_select=is_partner
and "p.id AS partner_id, p.name AS partner_name, "
"p.last_time_entries_checked AS last_time_entries_checked,"
or " ",
inner_from=is_partner
and "RIGHT JOIN res_partner p ON (l.partner_id = p.id)"
or " ",
where1=is_partner
and " "
or "AND ((at.type <> 'payable' AND at.type <> 'receivable') "
"OR l.partner_id IS NULL)",
where2=account_type and "AND at.type = %(account_type)s" or "",
where3=res_ids and "AND " + res_alias + ".id in %(res_ids)s" or "",
company_id=self.env.company.id,
where4=aml_ids and "AND l.id IN %(aml_ids)s" or " ",
where5=all_entries and all_entries_query or only_dual_entries_query,
group_by1=is_partner and "l.partner_id, p.id," or " ",
group_by2=is_partner and ", p.last_time_entries_checked" or " ",
order_by=is_partner
and "ORDER BY p.last_time_entries_checked"
or "ORDER BY a.code",
outer_where=is_partner
and "WHERE (last_time_entries_checked IS NULL "
"OR max_date > last_time_entries_checked)"
or " ",
)
)
self.env["account.move.line"].flush()
self.env["account.account"].flush()
self.env.cr.execute(query, locals())
# Apply ir_rules by filtering out
rows = self.env.cr.dictfetchall()
ids = [x["account_id"] for x in rows]
allowed_ids = set(Account.browse(ids).ids)
rows = [row for row in rows if row["account_id"] in allowed_ids]
if is_partner:
ids = [x["partner_id"] for x in rows]
allowed_ids = set(Partner.browse(ids).ids)
rows = [row for row in rows if row["partner_id"] in allowed_ids]
# Keep mode for future use in JS
if res_type == "account":
mode = "accounts"
else:
mode = "customers" if account_type == "receivable" else "suppliers"
# Fetch other data
for row in rows:
account = Account.browse(row["account_id"])
currency = account.currency_id or account.company_id.currency_id
row["currency_id"] = currency.id
partner_id = is_partner and row["partner_id"] or None
rec_prop = (
aml_ids
and self.env["account.move.line"].browse(aml_ids)
or self._get_move_line_reconciliation_proposition(
account.id, partner_id
)
)
row["reconciliation_proposition"] = self._prepare_move_lines(
rec_prop, target_currency=currency
)
row["mode"] = mode
row["company_id"] = account.company_id.id
# Return the partners with a reconciliation proposition first, since
# they are most likely to be reconciled.
return [r for r in rows if r["reconciliation_proposition"]] + [
r for r in rows if not r["reconciliation_proposition"]
]
@api.model
def process_move_lines(self, data):
"""Used to validate a batch of reconciliations in a single call
:param data: list of dicts containing:
- 'type': either 'partner' or 'account'
- 'id': id of the affected res.partner or account.account
- 'mv_line_ids': ids of existing account.move.line to reconcile
- 'new_mv_line_dicts': list of dicts containing values suitable for
account_move_line.create()
"""
Partner = self.env["res.partner"]
for datum in data:
if (
len(datum["mv_line_ids"]) >= 1
or len(datum["mv_line_ids"]) + len(datum["new_mv_line_dicts"]) >= 2
):
self._process_move_lines(
datum["mv_line_ids"], datum["new_mv_line_dicts"]
)
if datum["type"] == "partner":
partners = Partner.browse(datum["id"])
partners.mark_as_reconciled()
####################################################
# Private
####################################################
def _str_domain_for_mv_line(self, search_str):
return [
"|",
("account_id.code", "ilike", search_str),
"|",
("move_id.name", "ilike", search_str),
"|",
("move_id.ref", "ilike", search_str),
"|",
("date_maturity", "like", parse_date(self.env, search_str)),
"&",
("name", "!=", "/"),
("name", "ilike", search_str),
]
@api.model
def _domain_move_lines(self, search_str):
"""Returns the domain from the search_str search
:param search_str: search string
"""
if not search_str:
return []
str_domain = self._str_domain_for_mv_line(search_str)
if search_str[0] in ["-", "+"]:
try:
amounts_str = search_str.split("|")
for amount_str in amounts_str:
amount = (
amount_str[0] == "-"
and float(amount_str)
or float(amount_str[1:])
)
amount_domain = [
"|",
("amount_residual", "=", amount),
"|",
("amount_residual_currency", "=", amount),
"|",
(
amount_str[0] == "-" and "credit" or "debit",
"=",
float(amount_str[1:]),
),
("amount_currency", "=", amount),
]
str_domain = expression.OR([str_domain, amount_domain])
except Exception:
pass
else:
try:
amount = float(search_str)
amount_domain = [
"|",
("amount_residual", "=", amount),
"|",
("amount_residual_currency", "=", amount),
"|",
("amount_residual", "=", -amount),
"|",
("amount_residual_currency", "=", -amount),
"&",
("account_id.internal_type", "=", "liquidity"),
"|",
"|",
"|",
("debit", "=", amount),
("credit", "=", amount),
("amount_currency", "=", amount),
("amount_currency", "=", -amount),
]
str_domain = expression.OR([str_domain, amount_domain])
except Exception:
pass
return str_domain
@api.model
def _domain_move_lines_for_reconciliation(
self,
st_line,
aml_accounts,
partner_id,
excluded_ids=None,
search_str=False,
mode="rp",
):
"""Return the domain for account.move.line records which can be used for
bank statement reconciliation.
:param aml_accounts:
:param partner_id:
:param excluded_ids:
:param search_str:
:param mode: 'rp' for receivable/payable or 'other'
"""
AccountMoveLine = self.env["account.move.line"]
# Always exclude the journal items that have been marked as
# 'to be checked' in a former bank statement reconciliation
to_check_excluded = AccountMoveLine.search(
AccountMoveLine._get_suspense_moves_domain()
).ids
if excluded_ids is None:
excluded_ids = []
excluded_ids.extend(to_check_excluded)
domain_reconciliation = [
"&",
"&",
("statement_line_id", "=", False),
("account_id", "in", aml_accounts),
("balance", "!=", 0.0),
]
if st_line.company_id.account_bank_reconciliation_start:
domain_reconciliation = expression.AND(
[
domain_reconciliation,
[
(
"date",
">=",
st_line.company_id.account_bank_reconciliation_start,
)
],
]
)
# default domain matching
domain_matching = [
"&",
"&",
"&",
"&",
("id", "not in", st_line.move_id.line_ids.ids),
("reconciled", "=", False),
("account_id.reconcile", "=", True),
("balance", "!=", 0.0),
("parent_state", "=", "posted"),
]
domain = expression.OR([domain_reconciliation, domain_matching])
if partner_id:
domain = expression.AND([domain, [("partner_id", "=", partner_id)]])
if mode == "rp":
domain = expression.AND(
[
domain,
[
(
"account_id.internal_type",
"in",
["receivable", "payable", "liquidity"],
)
],
]
)
else:
domain = expression.AND(
[
domain,
[
(
"account_id.internal_type",
"not in",
["receivable", "payable", "liquidity"],
)
],
]
)
# Domain factorized for all reconciliation use cases
if search_str:
str_domain = self._domain_move_lines(search_str=search_str)
str_domain = expression.OR(
[str_domain, [("partner_id.name", "ilike", search_str)]]
)
domain = expression.AND([domain, str_domain])
if excluded_ids:
domain = expression.AND([[("id", "not in", excluded_ids)], domain])
# filter on account.move.line having the same company as the statement
# line
domain = expression.AND([domain, [("company_id", "=", st_line.company_id.id)]])
return domain
@api.model
def _domain_move_lines_for_manual_reconciliation(
self, account_id, partner_id=False, excluded_ids=None, search_str=False
):
"""Create domain criteria that are relevant to manual reconciliation."""
domain = [
"&",
"&",
("reconciled", "=", False),
("account_id", "=", account_id),
("move_id.state", "=", "posted"),
]
domain = expression.AND([domain, [("balance", "!=", 0.0)]])
if partner_id:
domain = expression.AND([domain, [("partner_id", "=", partner_id)]])
if excluded_ids:
domain = expression.AND([[("id", "not in", excluded_ids)], domain])
if search_str:
str_domain = self._domain_move_lines(search_str=search_str)
domain = expression.AND([domain, str_domain])
# filter on account.move.line having the same company as the given account
account = self.env["account.account"].browse(account_id)
domain = expression.AND([domain, [("company_id", "=", account.company_id.id)]])
return domain
@api.model
def _prepare_move_lines(
self, move_lines, target_currency=False, target_date=False, recs_count=0
):
"""Returns move lines formatted for the manual/bank reconciliation
widget
:param move_line_ids:
:param target_currency: currency (browse) you want the move line
debit/credit converted into
:param target_date: date to use for the monetary conversion
"""
ret = []
for line in move_lines:
company_currency = line.company_id.currency_id
line_currency = (
(line.currency_id and line.amount_currency)
and line.currency_id
or company_currency
)
ret_line = {
"id": line.id,
"name": line.name
and line.name != "/"
and line.move_id.name != line.name
and line.move_id.name + ": " + line.name
or line.move_id.name,
"ref": line.move_id.ref or "",
# For reconciliation between statement transactions and already
# registered payments (eg. checks)
# NB : we don't use the 'reconciled' field because the line
# we're selecting is not the one that gets reconciled
"account_id": [line.account_id.id, line.account_id.display_name],
"already_paid": line.account_id.internal_type == "liquidity",
"account_code": line.account_id.code,
"account_name": line.account_id.name,
"analytic_account_code": line.analytic_account_id.display_name or "",
"account_type": line.account_id.internal_type,
"date_maturity": format_date(self.env, line.date_maturity),
"date": format_date(self.env, line.date),
"journal_id": [line.journal_id.id, line.journal_id.display_name],
"partner_id": line.partner_id.id,
"partner_name": line.partner_id.name,
"currency_id": line_currency.id,
}
debit = line.debit
credit = line.credit
amount = line.amount_residual
amount_currency = line.amount_residual_currency
# For already reconciled lines, don't use amount_residual(_currency)
if line.account_id.internal_type == "liquidity":
amount = debit - credit
amount_currency = line.amount_currency
target_currency = target_currency or company_currency
# Use case:
# Let's assume that company currency is in USD and that we have the
# 3 following move lines
# Debit Credit Amount currency Currency
# 1) 25 0 0 NULL
# 2) 17 0 25 EUR
# 3) 33 0 25 YEN
#
# If we ask to see the information in the reconciliation widget in
# company currency, we want to see The following information
# 1) 25 USD (no currency information)
# 2) 17 USD [25 EUR] (show 25 euro in currency information,
# in the little bill)
# 3) 33 USD [25 YEN] (show 25 yen in currency information)
#
# If we ask to see the information in another currency than the
# company let's say EUR
# 1) 35 EUR [25 USD]
# 2) 25 EUR (no currency information)
# 3) 50 EUR [25 YEN]
# In that case, we have to convert the debit-credit to the currency
# we want and we show next to it the value of the amount_currency or
# the debit-credit if no amount currency
if target_currency == company_currency:
if line_currency == target_currency:
amount = amount
amount_currency = ""
total_amount = debit - credit
total_amount_currency = ""
else:
amount = amount
amount_currency = amount_currency
total_amount = debit - credit
total_amount_currency = line.amount_currency
if target_currency != company_currency:
if line_currency == target_currency:
amount = amount_currency
amount_currency = ""
total_amount = line.amount_currency
total_amount_currency = ""
else:
amount_currency = line.currency_id and amount_currency or amount
company = line.account_id.company_id
date = target_date or line.date
amount = company_currency._convert(
amount, target_currency, company, date
)
total_amount = company_currency._convert(
(line.debit - line.credit), target_currency, company, date
)
total_amount_currency = (
line.currency_id
and line.amount_currency
or (line.debit - line.credit)
)
ret_line["recs_count"] = recs_count
ret_line["debit"] = amount > 0 and amount or 0
ret_line["credit"] = amount < 0 and -amount or 0
ret_line["amount_currency"] = amount_currency
ret_line["amount_str"] = formatLang(
self.env, abs(amount), currency_obj=target_currency
)
ret_line["total_amount_str"] = formatLang(
self.env, abs(total_amount), currency_obj=target_currency
)
ret_line["amount_currency_str"] = (
amount_currency
and formatLang(
self.env, abs(amount_currency), currency_obj=line_currency
)
or ""
)
ret_line["total_amount_currency_str"] = (
total_amount_currency
and formatLang(
self.env, abs(total_amount_currency), currency_obj=line_currency
)
or ""
)
ret.append(ret_line)
return ret
@api.model
def _get_statement_line(self, st_line):
"""Returns the data required by the bank statement reconciliation
widget to display a statement line"""
group_analytic = self.env.user.has_group("analytic.group_analytic_accounting")
group_analytic_tags = self.env.user.has_group("analytic.group_analytic_tags")
statement_currency = (
st_line.journal_id.currency_id or st_line.journal_id.company_id.currency_id
)
if st_line.amount_currency and st_line.foreign_currency_id:
amount = st_line.amount_currency
amount_currency = st_line.amount
amount_currency_str = formatLang(
self.env, abs(amount_currency), currency_obj=statement_currency
)
else:
amount = st_line.amount
amount_currency = amount
amount_currency_str = ""
amount_str = formatLang(
self.env,
abs(amount),
currency_obj=st_line.foreign_currency_id or statement_currency,
)
data = {
"id": st_line.id,
"ref": st_line.ref,
"narration": st_line.narration or "",
"name": st_line.name,
"payment_ref": st_line.payment_ref,
"date": format_date(self.env, st_line.date),
"amount": amount,
"amount_str": amount_str, # Amount in the statement line currency
"currency_id": st_line.foreign_currency_id.id or statement_currency.id,
"partner_id": st_line.partner_id.id,
"journal_id": st_line.journal_id.id,
"statement_id": st_line.statement_id.id,
"account_id": [
st_line.journal_id.default_account_id.id,
st_line.journal_id.default_account_id.display_name,
],
"account_code": st_line.journal_id.default_account_id.code,
"account_name": st_line.journal_id.default_account_id.name,
"partner_name": st_line.partner_id.name,
"communication_partner_name": st_line.partner_name,
# Amount in the statement currency
"amount_currency_str": amount_currency_str,
# Amount in the statement currency
"amount_currency": amount_currency,
"has_no_partner": not st_line.partner_id.id,
"company_id": st_line.company_id.id,
"group_analytic_accounting": group_analytic,
"group_analytic_tags": group_analytic_tags,
}
if st_line.partner_id:
data["open_balance_account_id"] = (
amount > 0
and st_line.partner_id.property_account_receivable_id.id
or st_line.partner_id.property_account_payable_id.id
)
return data
@api.model
def _get_move_line_reconciliation_proposition(self, account_id, partner_id=None):
"""Returns two lines whose amount are opposite"""
Account_move_line = self.env["account.move.line"]
ir_rules_query = Account_move_line._where_calc([])
Account_move_line._apply_ir_rules(ir_rules_query, "read")
from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
where_str = where_clause and (" WHERE %s" % where_clause) or ""
# Get pairs
query = sql.SQL(
"""
SELECT a.id, b.id
FROM account_move_line a, account_move_line b,
account_move move_a, account_move move_b,
account_journal journal_a, account_journal journal_b
WHERE a.id != b.id
AND move_a.id = a.move_id
AND move_a.state = 'posted'
AND move_a.journal_id = journal_a.id
AND move_b.id = b.move_id
AND move_b.journal_id = journal_b.id
AND move_b.state = 'posted'
AND a.amount_residual = -b.amount_residual
AND a.balance != 0.0
AND b.balance != 0.0
AND NOT a.reconciled
AND a.account_id = %s
AND (%s IS NULL AND b.account_id = %s)
AND (%s IS NULL AND NOT b.reconciled OR b.id = %s)
AND (%s is NULL OR (a.partner_id = %s AND b.partner_id = %s))
AND a.id IN (SELECT "account_move_line".id FROM {0})
AND b.id IN (SELECT "account_move_line".id FROM {0})
ORDER BY a.date desc
LIMIT 1
""".format(
from_clause + where_str
)
)
move_line_id = self.env.context.get("move_line_id") or None
params = (
[
account_id,
move_line_id,
account_id,
move_line_id,
move_line_id,
partner_id,
partner_id,
partner_id,
]
+ where_clause_params
+ where_clause_params
)
self.env.cr.execute(query, params)
pairs = self.env.cr.fetchall()
if pairs:
return Account_move_line.browse(pairs[0])
return Account_move_line
@api.model
def _process_move_lines(self, move_line_ids, new_mv_line_dicts):
"""Create new move lines from new_mv_line_dicts (if not empty) then call
reconcile_partial on self and new move lines
:param new_mv_line_dicts: list of dicts containing values suitable for
account_move_line.create()
"""
if len(move_line_ids) < 1 or len(move_line_ids) + len(new_mv_line_dicts) < 2:
raise UserError(_("A reconciliation must involve at least 2 move lines."))
AccountMoveLine = self.env["account.move.line"].with_context(
skip_account_move_synchronization=True
)
account_move_line = AccountMoveLine.browse(move_line_ids)
writeoff_lines = AccountMoveLine
# Create writeoff move lines
if len(new_mv_line_dicts) > 0:
company_currency = account_move_line[0].account_id.company_id.currency_id
same_currency = False
currencies = list(
{aml.currency_id or company_currency for aml in account_move_line}
)
if len(currencies) == 1 and currencies[0] != company_currency:
same_currency = True
# We don't have to convert debit/credit to currency as all values in
# the reconciliation widget are displayed in company currency
# If all the lines are in the same currency, create writeoff entry
# with same currency also
for mv_line_dict in new_mv_line_dicts:
if not same_currency:
mv_line_dict["amount_currency"] = False
writeoff_lines += account_move_line._create_writeoff([mv_line_dict])
(account_move_line + writeoff_lines).reconcile()
else:
account_move_line.reconcile()