- cron_suggest (every 30min): warm AI suggestions for unreconciled lines that don't have a recent pending one - cron_pattern_refresh (daily 02:00): recompute fusion.reconcile.pattern for each (company, partner) pair with precedents - cron_mv_refresh (every 5min): REFRESH MATERIALIZED VIEW CONCURRENTLY using a dedicated autocommit cursor (REFRESH CONCURRENTLY can't run inside a regular Odoo transaction) V19 note: ir.cron dropped the numbercall field, so the data XML omits it (cron now repeats indefinitely as long as active=True). Tests: 5 new TestFusionBankRecCron tests pass; full module suite is 0 failed / 0 errors of 123 logical tests on westin-v19. Made-with: Cursor
120 lines
4.6 KiB
Python
120 lines
4.6 KiB
Python
"""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)
|