mirror of
https://github.com/OCA/account-reconcile.git
synced 2025-01-20 12:27:39 +02:00
If the journal destination account is reconcilable, as the journal entry is pre-generated before reconciling, the line is appearing in the reconciliation widget, provoking user confusion and error if that one is selected. We explicitly exclude in the matching domain for avoiding the problem. TT42983
1186 lines
47 KiB
Python
1186 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 {}
|
|
bank_statements = (
|
|
self.env["account.bank.statement.line"]
|
|
.browse(bank_statement_line_ids)
|
|
.mapped("statement_id")
|
|
)
|
|
|
|
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)
|
|
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")
|
|
|
|
results.update(
|
|
{
|
|
"statement_id": len(bank_statements_left) == 1
|
|
and bank_statements_left.id
|
|
or False,
|
|
"statement_name": len(bank_statements_left) == 1
|
|
and bank_statements_left.name
|
|
or False,
|
|
"journal_id": bank_statements
|
|
and bank_statements[0].journal_id.id
|
|
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."))
|
|
|
|
account_move_line = self.env["account.move.line"].browse(move_line_ids)
|
|
writeoff_lines = self.env["account.move.line"]
|
|
|
|
# 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()
|