diff --git a/fusion_accounting_ai/services/data_adapters/reports.py b/fusion_accounting_ai/services/data_adapters/reports.py index f73730a2..98c89afe 100644 --- a/fusion_accounting_ai/services/data_adapters/reports.py +++ b/fusion_accounting_ai/services/data_adapters/reports.py @@ -16,7 +16,12 @@ _logger = logging.getLogger(__name__) 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' # ------------------------------------------------------------------ @@ -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) diff --git a/fusion_accounting_reports/__manifest__.py b/fusion_accounting_reports/__manifest__.py index deec9036..af850876 100644 --- a/fusion_accounting_reports/__manifest__.py +++ b/fusion_accounting_reports/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Fusion Accounting Reports', - 'version': '19.0.1.0.14', + 'version': '19.0.1.0.15', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).', 'description': """ diff --git a/fusion_accounting_reports/tests/__init__.py b/fusion_accounting_reports/tests/__init__.py index d3f3f770..5e75a417 100644 --- a/fusion_accounting_reports/tests/__init__.py +++ b/fusion_accounting_reports/tests/__init__.py @@ -11,3 +11,4 @@ from . import test_commentary_generator from . import test_fusion_report_commentary from . import test_fusion_report_anomaly from . import test_reports_controller +from . import test_reports_adapter diff --git a/fusion_accounting_reports/tests/test_reports_adapter.py b/fusion_accounting_reports/tests/test_reports_adapter.py new file mode 100644 index 00000000..8f68feb8 --- /dev/null +++ b/fusion_accounting_reports/tests/test_reports_adapter.py @@ -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)