feat(fusion_accounting_ai): wire ReportsAdapter fusion paths to engine
Adds three new method families on ReportsAdapter that route through fusion.report.engine when fusion_accounting_reports is installed: - run_fusion_report (pnl/balance_sheet/trial_balance/general_ledger) - get_anomalies (variance detection on engine output) - get_commentary (LLM narrative; falls back to templated) These coexist with the legacy ref_id-shaped run_report / export_report API so existing reporting tools (profit_loss, balance_sheet, etc.) keep working unchanged. FUSION_MODEL is updated to fusion.report.engine so mode detection picks FUSION when the new engine is installed. 4 new TransactionCase tests cover the fusion + community paths. Made-with: Cursor
This commit is contained in:
@@ -16,7 +16,12 @@ _logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class ReportsAdapter(DataAdapter):
|
class ReportsAdapter(DataAdapter):
|
||||||
FUSION_MODEL = 'fusion.account.report'
|
# Phase 2 wires fusion.report.engine as the FUSION-mode backend for
|
||||||
|
# the new report_type-shaped methods (run_fusion_report, get_anomalies,
|
||||||
|
# get_commentary). The legacy ref_id-shaped run_report / export_report
|
||||||
|
# methods continue to defer to community when in FUSION mode (their
|
||||||
|
# original behavior), so this rename does not change their results.
|
||||||
|
FUSION_MODEL = 'fusion.report.engine'
|
||||||
ENTERPRISE_MODULE = 'account_reports'
|
ENTERPRISE_MODULE = 'account_reports'
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -167,4 +172,159 @@ class ReportsAdapter(DataAdapter):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ==================================================================
|
||||||
|
# Phase 2 (Task 19): fusion.report.engine-routed report methods
|
||||||
|
#
|
||||||
|
# These coexist with the legacy ref_id-shaped run_report/export_report
|
||||||
|
# API. New callers (financial_reports AI tools, OWL widget) use the
|
||||||
|
# *_fusion_report methods below; those route through the engine when
|
||||||
|
# fusion_accounting_reports is installed.
|
||||||
|
# ==================================================================
|
||||||
|
|
||||||
|
# ------------------ run_fusion_report --------------------------
|
||||||
|
|
||||||
|
def run_fusion_report(self, report_type, date_from, date_to,
|
||||||
|
comparison='none', company_id=None):
|
||||||
|
return self._dispatch(
|
||||||
|
'run_fusion_report',
|
||||||
|
report_type=report_type,
|
||||||
|
date_from=date_from, date_to=date_to,
|
||||||
|
comparison=comparison, company_id=company_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_fusion_report_via_fusion(self, report_type, date_from, date_to,
|
||||||
|
comparison='none', company_id=None):
|
||||||
|
if 'fusion.report.engine' not in self.env.registry:
|
||||||
|
return {'rows': [], 'error': 'fusion.report.engine not installed'}
|
||||||
|
from datetime import datetime
|
||||||
|
from odoo.addons.fusion_accounting_reports.services.date_periods import (
|
||||||
|
Period,
|
||||||
|
)
|
||||||
|
df = (datetime.strptime(date_from, '%Y-%m-%d').date()
|
||||||
|
if isinstance(date_from, str) else date_from)
|
||||||
|
dt = (datetime.strptime(date_to, '%Y-%m-%d').date()
|
||||||
|
if isinstance(date_to, str) else date_to)
|
||||||
|
period = Period(date_from=df, date_to=dt, label=f"{df} - {dt}")
|
||||||
|
engine = self.env['fusion.report.engine']
|
||||||
|
company_id = company_id or self.env.company.id
|
||||||
|
if report_type == 'pnl':
|
||||||
|
return engine.compute_pnl(
|
||||||
|
period, comparison=comparison, company_id=company_id,
|
||||||
|
)
|
||||||
|
if report_type == 'balance_sheet':
|
||||||
|
return engine.compute_balance_sheet(
|
||||||
|
dt, comparison=comparison, company_id=company_id,
|
||||||
|
)
|
||||||
|
if report_type == 'trial_balance':
|
||||||
|
return engine.compute_trial_balance(
|
||||||
|
period, company_id=company_id,
|
||||||
|
)
|
||||||
|
if report_type == 'general_ledger':
|
||||||
|
return engine.compute_gl(period, company_id=company_id)
|
||||||
|
return {'rows': [], 'error': f'unknown report_type {report_type}'}
|
||||||
|
|
||||||
|
def run_fusion_report_via_enterprise(self, report_type, date_from, date_to,
|
||||||
|
comparison='none', company_id=None):
|
||||||
|
# Enterprise's account_reports has its own UI; we don't proxy from
|
||||||
|
# Python. Callers should use the Enterprise menus or the legacy
|
||||||
|
# run_report(ref_id=...) method instead.
|
||||||
|
return {
|
||||||
|
'rows': [],
|
||||||
|
'error': 'Enterprise reports must be run from the Enterprise UI',
|
||||||
|
}
|
||||||
|
|
||||||
|
def run_fusion_report_via_community(self, report_type, date_from, date_to,
|
||||||
|
comparison='none', company_id=None):
|
||||||
|
return {
|
||||||
|
'rows': [],
|
||||||
|
'error': 'No fusion reports engine available in pure Community',
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------ get_anomalies ------------------------------
|
||||||
|
|
||||||
|
def get_anomalies(self, report_type, date_from, date_to,
|
||||||
|
comparison='previous_year', company_id=None):
|
||||||
|
return self._dispatch(
|
||||||
|
'get_anomalies',
|
||||||
|
report_type=report_type,
|
||||||
|
date_from=date_from, date_to=date_to,
|
||||||
|
comparison=comparison, company_id=company_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_anomalies_via_fusion(self, report_type, date_from, date_to,
|
||||||
|
comparison='previous_year', company_id=None):
|
||||||
|
if 'fusion.report.engine' not in self.env.registry:
|
||||||
|
return {'anomalies': []}
|
||||||
|
from odoo.addons.fusion_accounting_reports.services.anomaly_detection import (
|
||||||
|
detect,
|
||||||
|
)
|
||||||
|
report = self.run_fusion_report_via_fusion(
|
||||||
|
report_type=report_type,
|
||||||
|
date_from=date_from, date_to=date_to,
|
||||||
|
comparison=comparison, company_id=company_id,
|
||||||
|
)
|
||||||
|
if 'error' in report:
|
||||||
|
return {'anomalies': []}
|
||||||
|
return {'anomalies': detect(report)}
|
||||||
|
|
||||||
|
def get_anomalies_via_enterprise(self, report_type, date_from, date_to,
|
||||||
|
comparison='previous_year', company_id=None):
|
||||||
|
return {'anomalies': []}
|
||||||
|
|
||||||
|
def get_anomalies_via_community(self, report_type, date_from, date_to,
|
||||||
|
comparison='previous_year', company_id=None):
|
||||||
|
return {'anomalies': []}
|
||||||
|
|
||||||
|
# ------------------ get_commentary -----------------------------
|
||||||
|
|
||||||
|
def get_commentary(self, report_type, date_from, date_to,
|
||||||
|
comparison='none', company_id=None):
|
||||||
|
return self._dispatch(
|
||||||
|
'get_commentary',
|
||||||
|
report_type=report_type,
|
||||||
|
date_from=date_from, date_to=date_to,
|
||||||
|
comparison=comparison, company_id=company_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_commentary_via_fusion(self, report_type, date_from, date_to,
|
||||||
|
comparison='none', company_id=None):
|
||||||
|
empty = {
|
||||||
|
'summary': '', 'highlights': [],
|
||||||
|
'concerns': [], 'next_actions': [],
|
||||||
|
}
|
||||||
|
if 'fusion.report.engine' not in self.env.registry:
|
||||||
|
return empty
|
||||||
|
from odoo.addons.fusion_accounting_reports.services.anomaly_detection import (
|
||||||
|
detect,
|
||||||
|
)
|
||||||
|
from odoo.addons.fusion_accounting_reports.services.commentary_generator import (
|
||||||
|
generate_commentary,
|
||||||
|
)
|
||||||
|
report = self.run_fusion_report_via_fusion(
|
||||||
|
report_type=report_type,
|
||||||
|
date_from=date_from, date_to=date_to,
|
||||||
|
comparison=comparison, company_id=company_id,
|
||||||
|
)
|
||||||
|
if 'error' in report:
|
||||||
|
return empty
|
||||||
|
anomalies = detect(report)
|
||||||
|
return generate_commentary(
|
||||||
|
self.env, report_result=report, anomalies=anomalies,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_commentary_via_enterprise(self, report_type, date_from, date_to,
|
||||||
|
comparison='none', company_id=None):
|
||||||
|
return {
|
||||||
|
'summary': '', 'highlights': [],
|
||||||
|
'concerns': [], 'next_actions': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_commentary_via_community(self, report_type, date_from, date_to,
|
||||||
|
comparison='none', company_id=None):
|
||||||
|
return {
|
||||||
|
'summary': '', 'highlights': [],
|
||||||
|
'concerns': [], 'next_actions': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
register_adapter('reports', ReportsAdapter)
|
register_adapter('reports', ReportsAdapter)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Fusion Accounting Reports',
|
'name': 'Fusion Accounting Reports',
|
||||||
'version': '19.0.1.0.14',
|
'version': '19.0.1.0.15',
|
||||||
'category': 'Accounting/Accounting',
|
'category': 'Accounting/Accounting',
|
||||||
'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).',
|
'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ from . import test_commentary_generator
|
|||||||
from . import test_fusion_report_commentary
|
from . import test_fusion_report_commentary
|
||||||
from . import test_fusion_report_anomaly
|
from . import test_fusion_report_anomaly
|
||||||
from . import test_reports_controller
|
from . import test_reports_controller
|
||||||
|
from . import test_reports_adapter
|
||||||
|
|||||||
56
fusion_accounting_reports/tests/test_reports_adapter.py
Normal file
56
fusion_accounting_reports/tests/test_reports_adapter.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
"""Tests for ReportsAdapter Phase-2 (engine-routed) methods."""
|
||||||
|
|
||||||
|
from odoo.tests.common import TransactionCase, tagged
|
||||||
|
|
||||||
|
from odoo.addons.fusion_accounting_ai.services.data_adapters.reports import (
|
||||||
|
ReportsAdapter,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestReportsAdapter(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.adapter = ReportsAdapter(self.env)
|
||||||
|
|
||||||
|
def test_run_fusion_report_via_fusion_pnl(self):
|
||||||
|
result = self.adapter.run_fusion_report_via_fusion(
|
||||||
|
report_type='pnl',
|
||||||
|
date_from='2026-01-01',
|
||||||
|
date_to='2026-12-31',
|
||||||
|
company_id=self.env.company.id,
|
||||||
|
)
|
||||||
|
self.assertEqual(result.get('report_type'), 'pnl')
|
||||||
|
self.assertIn('rows', result)
|
||||||
|
|
||||||
|
def test_run_fusion_report_via_community_returns_error(self):
|
||||||
|
result = self.adapter.run_fusion_report_via_community(
|
||||||
|
report_type='pnl',
|
||||||
|
date_from='2026-01-01',
|
||||||
|
date_to='2026-12-31',
|
||||||
|
)
|
||||||
|
self.assertIn('error', result)
|
||||||
|
|
||||||
|
def test_get_anomalies_via_fusion(self):
|
||||||
|
result = self.adapter.get_anomalies_via_fusion(
|
||||||
|
report_type='pnl',
|
||||||
|
date_from='2026-01-01',
|
||||||
|
date_to='2026-12-31',
|
||||||
|
comparison='previous_year',
|
||||||
|
company_id=self.env.company.id,
|
||||||
|
)
|
||||||
|
self.assertIn('anomalies', result)
|
||||||
|
self.assertIsInstance(result['anomalies'], list)
|
||||||
|
|
||||||
|
def test_get_commentary_via_fusion(self):
|
||||||
|
result = self.adapter.get_commentary_via_fusion(
|
||||||
|
report_type='pnl',
|
||||||
|
date_from='2026-01-01',
|
||||||
|
date_to='2026-12-31',
|
||||||
|
company_id=self.env.company.id,
|
||||||
|
)
|
||||||
|
self.assertIn('summary', result)
|
||||||
|
self.assertIn('highlights', result)
|
||||||
|
self.assertIn('concerns', result)
|
||||||
|
self.assertIn('next_actions', result)
|
||||||
Reference in New Issue
Block a user