"""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}