"""Cron handler model for fusion_accounting_bank_rec. Three scheduled jobs: - _cron_suggest_pending: warm AI suggestions for unreconciled lines (30 min) - _cron_refresh_patterns: recompute fusion.reconcile.pattern aggregates (daily 02:00) - _cron_refresh_mv: REFRESH MATERIALIZED VIEW CONCURRENTLY (5 min) """ import logging from datetime import timedelta import odoo from odoo import api, fields, models from ..services.pattern_extractor import extract_pattern_for_partner _logger = logging.getLogger(__name__) class FusionBankRecCron(models.AbstractModel): _name = "fusion.bank.rec.cron" _description = "Fusion Bank Reconciliation Cron Handlers" @api.model def _cron_suggest_pending(self, batch_size=50): """For each unreconciled bank line that doesn't have a recent pending suggestion, run engine.suggest_matches. Recent = a pending suggestion created within the last 24 hours.""" cutoff = fields.Datetime.now() - timedelta(hours=24) Line = self.env['account.bank.statement.line'] lines_to_consider = Line.search([ ('is_reconciled', '=', False), ('partner_id', '!=', False), ], limit=batch_size * 5) Suggestion = self.env['fusion.reconcile.suggestion'] lines_needing_suggestions = self.env['account.bank.statement.line'] for line in lines_to_consider: recent = Suggestion.search_count([ ('statement_line_id', '=', line.id), ('state', '=', 'pending'), ('create_date', '>=', cutoff), ]) if recent == 0: lines_needing_suggestions |= line if len(lines_needing_suggestions) >= batch_size: break if not lines_needing_suggestions: _logger.debug("Cron: no bank lines need suggestion warming") return _logger.info( "Cron: warming suggestions for %d bank lines", len(lines_needing_suggestions)) try: self.env['fusion.reconcile.engine'].suggest_matches( lines_needing_suggestions, limit_per_line=3) except Exception as e: _logger.exception("Cron suggest_pending failed: %s", e) @api.model def _cron_refresh_patterns(self): """For each (company, partner) pair with precedents, recompute and upsert the fusion.reconcile.pattern row.""" Pattern = self.env['fusion.reconcile.pattern'] self.env.cr.execute(""" SELECT DISTINCT company_id, partner_id FROM fusion_reconcile_precedent WHERE partner_id IS NOT NULL """) pairs = self.env.cr.fetchall() _logger.info( "Cron: refreshing patterns for %d (company, partner) pairs", len(pairs)) for company_id, partner_id in pairs: try: vals = extract_pattern_for_partner( self.env, company_id=company_id, partner_id=partner_id) existing = Pattern.search([ ('company_id', '=', company_id), ('partner_id', '=', partner_id), ], limit=1) if existing: existing.write(vals) else: Pattern.create(vals) except Exception as e: _logger.warning( "Pattern refresh failed for company=%s partner=%s: %s", company_id, partner_id, e) @api.model def _cron_refresh_mv(self): """Refresh the materialized view CONCURRENTLY using an autocommit cursor. REFRESH CONCURRENTLY can't run inside a transaction, so we open a fresh connection in autocommit mode (per Task 24's note). On any failure, we fall back to the model's blocking refresh.""" try: db_name = self.env.cr.dbname db = odoo.sql_db.db_connect(db_name) with db.cursor() as cron_cr: cron_cr._cnx.set_session(autocommit=True) cron_cr.execute( "REFRESH MATERIALIZED VIEW CONCURRENTLY " "fusion_unreconciled_bank_line_mv") _logger.debug("Cron: MV refresh CONCURRENTLY succeeded") except Exception as e: _logger.warning( "Cron MV refresh CONCURRENTLY failed (%s); falling back to " "blocking refresh", e) try: self.env['fusion.unreconciled.bank.line.mv']._refresh( concurrently=False) except Exception as e2: _logger.exception( "Cron MV refresh fallback also failed: %s", e2)