[FIX] account: performance of reconciliation

Instead of trying to find the reconciled lines recursively, we can use
the `matching_number`, since it now has a distinct value for each
subgraph of reconciliation even for partials graphs.

The situation in 16 was computing the graph by doing one query per level
of depth. In 17, before this commit, only one query was done but
computing the graph from each node, leading in a worse time complexity.
The result was that for small batches, it was slightly faster in 17 than
16 but considerably slower for bigger batches.
This commit just makes things go zoom in all cases.

closes odoo/odoo#149336

Signed-off-by: Laurent Smet (las) <las@odoo.com>
This commit is contained in:
william-andre
2024-01-22 19:01:02 +00:00
parent 119fa9f974
commit b4ccdb34b6
2 changed files with 45 additions and 53 deletions
+6 -17
View File
@@ -261,6 +261,7 @@ class AccountMoveLine(models.Model):
matching_number = fields.Char(
string="Matching #",
copy=False,
index='btree',
help="Matching number for this line, 'P' if it is only partially reconcile, or the name of "
"the full reconcile if it exists.",
) # can also start with `I` for imports: see `_reconcile_marked`
@@ -3045,23 +3046,11 @@ class AccountMoveLine(models.Model):
return ids
def _all_reconciled_lines(self):
reconciliation_lines = self.filtered(lambda x: x.account_id.reconcile or x.account_id.account_type in ('asset_cash', 'liability_credit_card'))
self.env['account.partial.reconcile'].flush_model()
self.env.cr.execute("""
WITH RECURSIVE partials (current_id) AS (
SELECT line.id
FROM account_move_line line
WHERE id = ANY(%s)
UNION
SELECT CASE WHEN partial.debit_move_id = p.current_id THEN partial.credit_move_id ELSE partial.debit_move_id END
FROM partials p
JOIN account_partial_reconcile partial ON partial.debit_move_id = p.current_id OR partial.credit_move_id = p.current_id
)
SELECT current_id FROM partials;
""", [reconciliation_lines.ids])
return self.browse(r[0] for r in self.env.cr.fetchall())
reconciled = self
matching_numbers = [n for n in set(self.mapped('matching_number')) if n]
if matching_numbers:
reconciled |= self.search([('matching_number', 'in', matching_numbers)])
return reconciled
def _get_attachment_domains(self):
self.ensure_one()
@@ -138,44 +138,47 @@ class AccountPartialReconcile(models.Model):
@api.model
def _update_matching_number(self, amls):
amls = amls._all_reconciled_lines()
self.env['account.partial.reconcile'].flush_model()
amls.flush_recordset(['full_reconcile_id'])
self.env.cr.execute("""
WITH RECURSIVE partials (line_id, current_id) AS (
SELECT id, id
FROM account_move_line
WHERE id = ANY(%s)
AND full_reconcile_id IS NULL
all_partials = amls.matched_debit_ids | amls.matched_credit_ids
UNION
SELECT p.line_id,
CASE WHEN partial.debit_move_id = p.current_id THEN partial.credit_move_id
ELSE partial.debit_move_id
END
FROM partials p
JOIN account_partial_reconcile partial ON p.current_id = partial.debit_move_id
OR p.current_id = partial.credit_move_id
)
SELECT line_id, 'P' || MIN(partial.id) AS matching_number
FROM partials
JOIN account_partial_reconcile partial ON current_id = partial.debit_move_id
OR current_id = partial.credit_move_id
GROUP BY line_id
""", [
amls.ids,
])
line_matching_number = dict(self.env.cr.fetchall())
for line in amls.with_context(skip_invoice_sync=True):
if line.full_reconcile_id:
line.matching_number = str(line.full_reconcile_id.id)
elif line.matched_debit_ids or line.matched_credit_ids:
line.matching_number = line_matching_number[line.id]
else:
line.matching_number = False
# The matchings form a set of graphs, which can be numbered: this is the matching number.
# We iterate on each edge of the graphs, giving it a number (min of its edge ids).
# By iterating, we either simply add a node (move line) to the graph and asign the number to
# it or we merge the two graphs.
# At the end, we have an index for the number to assign of all lines.
number2lines = {}
line2number = {}
for partial in all_partials.sorted('id'):
debit_min_id = line2number.get(partial.debit_move_id.id)
credit_min_id = line2number.get(partial.credit_move_id.id)
if debit_min_id and credit_min_id: # merging the 2 graph into the one with smalles number
if debit_min_id != credit_min_id:
min_min_id = min(debit_min_id, credit_min_id)
max_min_id = max(debit_min_id, credit_min_id)
for line_id in number2lines[max_min_id]:
line2number[line_id] = min_min_id
number2lines[min_min_id].extend(number2lines.pop(max_min_id))
elif debit_min_id: # adding a new node to a graph
number2lines[debit_min_id].append(partial.credit_move_id.id)
line2number[partial.credit_move_id.id] = debit_min_id
elif credit_min_id: # adding a new node to a graph
number2lines[credit_min_id].append(partial.debit_move_id.id)
line2number[partial.debit_move_id.id] = credit_min_id
else: # creating a new graph
number2lines[partial.id] = [partial.debit_move_id.id, partial.credit_move_id.id]
line2number[partial.debit_move_id.id] = partial.id
line2number[partial.credit_move_id.id] = partial.id
processed_aml_ids = []
with amls.move_id._check_balanced({'records': amls.move_id}): # avoid checking the consistency for each individual write
for min_partial_id, line_ids in number2lines.items():
min_partial = self.browse(min_partial_id)
self.env['account.move.line'].browse(line_ids).matching_number = (
str(min_partial.full_reconcile_id.id)
if min_partial.full_reconcile_id else
f"P{min_partial.id}"
)
processed_aml_ids.extend(line_ids)
self.env['account.move.line'].browse(set(amls.ids) - set(processed_aml_ids)).matching_number = False
# -------------------------------------------------------------------------
# RECONCILIATION METHODS