Verifies the bank_rec_bootstrap migration step (a) creates precedents from existing partial.reconcile rows, (b) is idempotent on re-run, and (c) refreshes the MV without erroring. Three TransactionCase tests: - test_bootstrap_creates_precedents_from_existing_reconciles seeds two reconciles via the engine, wipes the auto-recorded precedents, then asserts the bootstrap produces source='backfill' precedents. - test_bootstrap_step_idempotent runs the bootstrap twice and asserts the second pass creates zero new precedents. - test_bootstrap_refreshes_mv_without_error runs the bootstrap on a clean partner and asserts no exception is raised and the result dict reports MV + pattern refresh outcomes. Implementation fixes uncovered by these tests: - precedent_backfill.backfill_precedents now pre-filters account.partial.reconcile to rows that touch a bank statement line on either side. Previously it walked every partial in the DB; on the westin-v19 dev DB that's 16k rows and the default limit=10000 missed the newest test fixtures (highest IDs). - backfill skips the periodic env.cr.commit() when running under a TestCursor, since committing inside a test breaks the rollback. Test count: 139 -> 142. Made-with: Cursor
117 lines
4.5 KiB
Python
117 lines
4.5 KiB
Python
"""Pure-Python helpers for backfilling fusion.reconcile.precedent
|
|
from existing account.partial.reconcile rows during migration.
|
|
|
|
Strategy:
|
|
- Each account.partial.reconcile that involves at least one
|
|
account.bank.statement.line's reconcile-account line is a candidate.
|
|
- One precedent per qualifying partial. The (statement_line.id, account_id,
|
|
amount) triple is encoded into matched_account_ids so a second run can
|
|
detect and skip already-backfilled rows (idempotency).
|
|
"""
|
|
|
|
import logging
|
|
|
|
from .memo_tokenizer import tokenize_memo
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _identify_bank_side(partial):
|
|
"""Return (bank_move_line, counterpart_move_line, statement_line_id)
|
|
or (None, None, None) if neither side is a bank statement line."""
|
|
debit_line = partial.debit_move_id
|
|
credit_line = partial.credit_move_id
|
|
|
|
if debit_line.move_id.statement_line_id:
|
|
return debit_line, credit_line, debit_line.move_id.statement_line_id.id
|
|
if credit_line.move_id.statement_line_id:
|
|
return credit_line, debit_line, credit_line.move_id.statement_line_id.id
|
|
return None, None, None
|
|
|
|
|
|
def backfill_precedents(env, *, company_id=None, batch_size=500, limit=10000):
|
|
"""Walk account.partial.reconcile and create fusion.reconcile.precedent
|
|
rows for any reconcile that involves a bank statement line.
|
|
|
|
Idempotent: skips partials whose (statement_line, account, amount)
|
|
signature is already present in fusion.reconcile.precedent (encoded
|
|
via matched_account_ids).
|
|
|
|
Returns dict with `created` and `skipped` counts.
|
|
"""
|
|
Precedent = env['fusion.reconcile.precedent'].sudo()
|
|
Partial = env['account.partial.reconcile'].sudo()
|
|
Line = env['account.bank.statement.line'].sudo()
|
|
|
|
in_test_mode = env.cr.__class__.__name__ == 'TestCursor'
|
|
|
|
# Pre-filter to partials that touch a bank statement line on either side.
|
|
# In a real DB we typically have 10x more invoice<->payment partials than
|
|
# bank-rec partials; filtering here keeps the loop bounded and makes the
|
|
# default limit reflect "real" candidates rather than every partial ever.
|
|
domain = [
|
|
'|',
|
|
('debit_move_id.move_id.statement_line_id', '!=', False),
|
|
('credit_move_id.move_id.statement_line_id', '!=', False),
|
|
]
|
|
if company_id:
|
|
domain.append(('company_id', '=', company_id))
|
|
partials = Partial.search(domain, limit=limit, order='id asc')
|
|
|
|
created = 0
|
|
skipped = 0
|
|
for partial in partials:
|
|
bank_line, counterpart, bsl_id = _identify_bank_side(partial)
|
|
if not bsl_id:
|
|
skipped += 1
|
|
continue
|
|
|
|
signature_account = str(counterpart.account_id.id)
|
|
|
|
existing = Precedent.search([
|
|
('partner_id', '=',
|
|
counterpart.partner_id.id if counterpart.partner_id else False),
|
|
('amount', '=', abs(partial.amount)),
|
|
('matched_account_ids', '=ilike', f'%{signature_account}%'),
|
|
('source', '=', 'backfill'),
|
|
], limit=1)
|
|
if existing:
|
|
skipped += 1
|
|
continue
|
|
|
|
statement_line = Line.browse(bsl_id)
|
|
try:
|
|
currency = (partial.debit_currency_id
|
|
or partial.company_id.currency_id)
|
|
Precedent.create({
|
|
'company_id': partial.company_id.id,
|
|
'partner_id': (counterpart.partner_id.id
|
|
if counterpart.partner_id else False),
|
|
'amount': abs(partial.amount),
|
|
'currency_id': currency.id,
|
|
'date': statement_line.date or partial.create_date.date(),
|
|
'memo_tokens': ','.join(
|
|
tokenize_memo(statement_line.payment_ref or '')),
|
|
'journal_id': statement_line.journal_id.id,
|
|
'matched_move_line_count': 1,
|
|
'matched_account_ids': signature_account,
|
|
'reconciler_user_id': partial.create_uid.id,
|
|
'reconciled_at': partial.create_date,
|
|
'source': 'backfill',
|
|
})
|
|
created += 1
|
|
if created % batch_size == 0:
|
|
if not in_test_mode:
|
|
env.cr.commit()
|
|
_logger.info(
|
|
"Backfill progress: %d created, %d skipped",
|
|
created, skipped)
|
|
except Exception as e: # noqa: BLE001
|
|
_logger.warning("Backfill skip partial %s: %s", partial.id, e)
|
|
skipped += 1
|
|
|
|
_logger.info(
|
|
"precedent_backfill complete: %d created, %d skipped",
|
|
created, skipped)
|
|
return {'created': created, 'skipped': skipped}
|