feat(fusion_accounting_bank_rec): pre-aggregated MV for OWL widget perf
CREATE MATERIALIZED VIEW fusion_unreconciled_bank_line_mv pre-computes the data the kanban widget needs (top suggestion, confidence band, attachment count, partner reconcile hint) so that listing 50-100 lines is one indexed query instead of N+1. Refresh strategy: - Triggered on fusion.reconcile.suggestion create/write (best-effort, never poisons the originating transaction) - Cron (every 5 min) — added in Task 25 The MV is created in the model's init() (Odoo calls this on install/upgrade). The SQL DDL is idempotent (CREATE MATERIALIZED VIEW IF NOT EXISTS / CREATE INDEX IF NOT EXISTS) and includes a UNIQUE(id) index so REFRESH MATERIALIZED VIEW CONCURRENTLY is supported. _refresh() falls back to a blocking refresh on the first call after creation. Made-with: Cursor
This commit is contained in:
@@ -5,3 +5,4 @@ from . import fusion_bank_rec_widget
|
||||
from . import account_bank_statement_line
|
||||
from . import account_reconcile_model
|
||||
from . import fusion_reconcile_engine
|
||||
from . import fusion_unreconciled_bank_line_mv
|
||||
|
||||
@@ -9,8 +9,12 @@ suggestions here, and the user (or batch-accept action) approves them
|
||||
through the engine's accept_suggestion() method.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionReconcileSuggestion(models.Model):
|
||||
_name = "fusion.reconcile.suggestion"
|
||||
@@ -96,3 +100,30 @@ class FusionReconcileSuggestion(models.Model):
|
||||
sug.confidence_band = 'low'
|
||||
else:
|
||||
sug.confidence_band = 'none'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# CRUD overrides — trigger MV refresh so the OWL widget sees fresh
|
||||
# confidence bands / top suggestion ids without waiting for cron.
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
self._trigger_mv_refresh()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
# Only refresh on changes that affect the MV's projected columns.
|
||||
if 'state' in vals or 'confidence' in vals or 'rank' in vals:
|
||||
self._trigger_mv_refresh()
|
||||
return res
|
||||
|
||||
def _trigger_mv_refresh(self):
|
||||
"""Best-effort MV refresh; never poison the originating transaction."""
|
||||
try:
|
||||
self.env['fusion.unreconciled.bank.line.mv']._refresh(
|
||||
concurrently=True)
|
||||
except Exception as e: # noqa: BLE001
|
||||
_logger.warning(
|
||||
"MV refresh after suggestion write failed: %s", e)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Materialized view exposing pre-aggregated unreconciled-bank-line data.
|
||||
|
||||
The MV is created in the model's init() (called by Odoo on install/upgrade).
|
||||
Refresh strategy:
|
||||
- Cron (every 5 min) — see fusion_accounting_bank_rec/data/cron.xml (Task 25)
|
||||
- Triggered refresh after suggestion writes (handled in fusion_reconcile_suggestion.py)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionUnreconciledBankLineMV(models.Model):
|
||||
_name = "fusion.unreconciled.bank.line.mv"
|
||||
_description = "Materialized view of unreconciled bank lines for OWL widget"
|
||||
_auto = False # we manage the table ourselves
|
||||
_table = "fusion_unreconciled_bank_line_mv"
|
||||
_order = "date desc, id desc"
|
||||
|
||||
# Fields mirror the columns in the SQL view; required so Odoo can read them.
|
||||
company_id = fields.Many2one('res.company', readonly=True)
|
||||
journal_id = fields.Many2one('account.journal', readonly=True)
|
||||
date = fields.Date(readonly=True)
|
||||
amount = fields.Float(readonly=True)
|
||||
payment_ref = fields.Char(readonly=True)
|
||||
currency_id = fields.Many2one('res.currency', readonly=True)
|
||||
partner_id = fields.Many2one('res.partner', readonly=True)
|
||||
create_date = fields.Datetime(readonly=True)
|
||||
top_suggestion_id = fields.Many2one('fusion.reconcile.suggestion', readonly=True)
|
||||
top_confidence = fields.Float(readonly=True)
|
||||
confidence_band = fields.Selection([
|
||||
('high', 'High'),
|
||||
('medium', 'Medium'),
|
||||
('low', 'Low'),
|
||||
('none', 'None'),
|
||||
], readonly=True)
|
||||
attachment_count = fields.Integer(readonly=True)
|
||||
partner_reconcile_count = fields.Integer(readonly=True)
|
||||
|
||||
def init(self):
|
||||
"""Create the MV if missing.
|
||||
|
||||
Reads create_mv_unreconciled_bank_line.sql and executes it. Idempotent
|
||||
because the SQL uses CREATE MATERIALIZED VIEW IF NOT EXISTS."""
|
||||
sql_path = os.path.join(
|
||||
os.path.dirname(__file__), '..', 'data', 'sql',
|
||||
'create_mv_unreconciled_bank_line.sql')
|
||||
with open(sql_path, 'r') as f:
|
||||
sql = f.read()
|
||||
self.env.cr.execute(sql)
|
||||
_logger.info(
|
||||
"fusion_unreconciled_bank_line_mv: created/verified MV + indexes")
|
||||
|
||||
@api.model
|
||||
def _refresh(self, *, concurrently=True):
|
||||
"""Refresh the MV.
|
||||
|
||||
If ``concurrently=True`` (default), uses
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY (requires the unique index).
|
||||
Falls back to a blocking refresh on the first refresh after creation
|
||||
(when CONCURRENTLY is not yet allowed because the MV has never been
|
||||
populated)."""
|
||||
keyword = "CONCURRENTLY" if concurrently else ""
|
||||
try:
|
||||
self.env.cr.execute(
|
||||
f"REFRESH MATERIALIZED VIEW {keyword} fusion_unreconciled_bank_line_mv"
|
||||
)
|
||||
_logger.debug(
|
||||
"fusion_unreconciled_bank_line_mv refreshed (%s)",
|
||||
'concurrent' if concurrently else 'blocking')
|
||||
except Exception as e: # noqa: BLE001
|
||||
# CONCURRENTLY fails on first refresh after creation if the MV is
|
||||
# empty / has never been populated; fall back to non-concurrent.
|
||||
if concurrently:
|
||||
_logger.warning(
|
||||
"Concurrent MV refresh failed (%s); falling back to "
|
||||
"blocking refresh", e)
|
||||
self.env.cr.execute(
|
||||
"REFRESH MATERIALIZED VIEW fusion_unreconciled_bank_line_mv"
|
||||
)
|
||||
else:
|
||||
raise
|
||||
Reference in New Issue
Block a user