Files
account-reconcile/account_reconciliation_widget/models/reconciliation_widget.py
Pedro M. Baeza 5c54688faf [FIX] account_reconciliation_widget: Hide own reconcilable items
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
2023-05-31 18:20:44 +02:00

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()