feat(fusion_accounting_ai): 5 new financial reports AI tools
Adds financial_reports.py tools module with 5 fusion-engine-routed tools registered in TOOL_DISPATCH: - fusion_run_report - fusion_get_anomalies - fusion_generate_commentary - fusion_drill_down_report_line - fusion_compare_periods Each tool guards on 'fusion.report.engine' being in the registry and otherwise returns a structured error so the chat agent can surface a clear "module not installed" message. 6 new TransactionCase tests (including a TOOL_DISPATCH registration sanity check). Made-with: Cursor
This commit is contained in:
@@ -9,11 +9,12 @@ from .inventory import TOOLS as INVENTORY_TOOLS
|
||||
from .adp import TOOLS as ADP_TOOLS
|
||||
from .reporting import TOOLS as REPORTING_TOOLS
|
||||
from .audit import TOOLS as AUDIT_TOOLS
|
||||
from .financial_reports import TOOLS as FINANCIAL_REPORTS_TOOLS
|
||||
|
||||
TOOL_DISPATCH = {}
|
||||
for tools_dict in [
|
||||
BANK_RECON_TOOLS, HST_TOOLS, AR_TOOLS, AP_TOOLS, JOURNAL_TOOLS,
|
||||
MONTH_END_TOOLS, PAYROLL_TOOLS, INVENTORY_TOOLS, ADP_TOOLS,
|
||||
REPORTING_TOOLS, AUDIT_TOOLS,
|
||||
REPORTING_TOOLS, AUDIT_TOOLS, FINANCIAL_REPORTS_TOOLS,
|
||||
]:
|
||||
TOOL_DISPATCH.update(tools_dict)
|
||||
|
||||
127
fusion_accounting_ai/services/tools/financial_reports.py
Normal file
127
fusion_accounting_ai/services/tools/financial_reports.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""Fusion-engine-routed AI tools for financial reports.
|
||||
|
||||
These 5 tools route through ReportsAdapter's Phase-2 methods
|
||||
(run_fusion_report / get_anomalies / get_commentary), which in turn
|
||||
call fusion.report.engine when fusion_accounting_reports is installed.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _company_id(env, params):
|
||||
raw = params.get('company_id')
|
||||
return int(raw) if raw else env.company.id
|
||||
|
||||
|
||||
def fusion_run_report(env, params):
|
||||
"""Run a fusion financial report.
|
||||
|
||||
Params: report_type (pnl|balance_sheet|trial_balance|general_ledger),
|
||||
date_from, date_to, comparison (none|previous_period|previous_year),
|
||||
optional company_id.
|
||||
"""
|
||||
if 'fusion.report.engine' not in env.registry:
|
||||
return {'error': 'fusion_accounting_reports not installed'}
|
||||
from ..data_adapters import get_adapter
|
||||
adapter = get_adapter(env, 'reports')
|
||||
result = adapter.run_fusion_report(
|
||||
report_type=params.get('report_type'),
|
||||
date_from=params.get('date_from'),
|
||||
date_to=params.get('date_to'),
|
||||
comparison=params.get('comparison', 'none'),
|
||||
company_id=_company_id(env, params),
|
||||
)
|
||||
rows = result.get('rows', [])
|
||||
return {
|
||||
'report_type': params.get('report_type'),
|
||||
'period': result.get('period'),
|
||||
'comparison_period': result.get('comparison_period'),
|
||||
'row_count': len(rows),
|
||||
'rows': rows,
|
||||
}
|
||||
|
||||
|
||||
def fusion_get_anomalies(env, params):
|
||||
"""Detect variance anomalies in a report."""
|
||||
if 'fusion.report.engine' not in env.registry:
|
||||
return {'error': 'fusion_accounting_reports not installed'}
|
||||
from ..data_adapters import get_adapter
|
||||
adapter = get_adapter(env, 'reports')
|
||||
result = adapter.get_anomalies(
|
||||
report_type=params.get('report_type'),
|
||||
date_from=params.get('date_from'),
|
||||
date_to=params.get('date_to'),
|
||||
comparison=params.get('comparison', 'previous_year'),
|
||||
company_id=_company_id(env, params),
|
||||
)
|
||||
anomalies = result.get('anomalies', [])
|
||||
return {'count': len(anomalies), 'anomalies': anomalies}
|
||||
|
||||
|
||||
def fusion_generate_commentary(env, params):
|
||||
"""Generate AI commentary for a report."""
|
||||
if 'fusion.report.engine' not in env.registry:
|
||||
return {'error': 'fusion_accounting_reports not installed'}
|
||||
from ..data_adapters import get_adapter
|
||||
adapter = get_adapter(env, 'reports')
|
||||
result = adapter.get_commentary(
|
||||
report_type=params.get('report_type'),
|
||||
date_from=params.get('date_from'),
|
||||
date_to=params.get('date_to'),
|
||||
comparison=params.get('comparison', 'none'),
|
||||
company_id=_company_id(env, params),
|
||||
)
|
||||
return {
|
||||
'summary': result.get('summary', ''),
|
||||
'highlights': result.get('highlights', []),
|
||||
'concerns': result.get('concerns', []),
|
||||
'next_actions': result.get('next_actions', []),
|
||||
}
|
||||
|
||||
|
||||
def fusion_drill_down_report_line(env, params):
|
||||
"""Drill from a report line into the underlying journal items."""
|
||||
if 'fusion.report.engine' not in env.registry:
|
||||
return {'error': 'fusion_accounting_reports not installed'}
|
||||
from datetime import datetime
|
||||
|
||||
from odoo.addons.fusion_accounting_reports.services.date_periods import (
|
||||
Period,
|
||||
)
|
||||
date_from = params['date_from']
|
||||
date_to = params['date_to']
|
||||
if isinstance(date_from, str):
|
||||
date_from = datetime.strptime(date_from, '%Y-%m-%d').date()
|
||||
if isinstance(date_to, str):
|
||||
date_to = datetime.strptime(date_to, '%Y-%m-%d').date()
|
||||
period = Period(date_from=date_from, date_to=date_to, label='drill')
|
||||
engine = env['fusion.report.engine']
|
||||
rows = engine.drill_down(
|
||||
account_id=int(params['account_id']),
|
||||
period=period,
|
||||
company_id=_company_id(env, params),
|
||||
)
|
||||
return {'count': len(rows), 'rows': rows}
|
||||
|
||||
|
||||
def fusion_compare_periods(env, params):
|
||||
"""Run a report with period comparison side-by-side.
|
||||
|
||||
Defaults comparison to 'previous_year' so callers get a comparison
|
||||
column without specifying it explicitly.
|
||||
"""
|
||||
return fusion_run_report(env, {
|
||||
**params,
|
||||
'comparison': params.get('comparison', 'previous_year'),
|
||||
})
|
||||
|
||||
|
||||
TOOLS = {
|
||||
'fusion_run_report': fusion_run_report,
|
||||
'fusion_get_anomalies': fusion_get_anomalies,
|
||||
'fusion_generate_commentary': fusion_generate_commentary,
|
||||
'fusion_drill_down_report_line': fusion_drill_down_report_line,
|
||||
'fusion_compare_periods': fusion_compare_periods,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
'name': 'Fusion Accounting Reports',
|
||||
'version': '19.0.1.0.15',
|
||||
'version': '19.0.1.0.16',
|
||||
'category': 'Accounting/Accounting',
|
||||
'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).',
|
||||
'description': """
|
||||
|
||||
@@ -12,3 +12,4 @@ from . import test_fusion_report_commentary
|
||||
from . import test_fusion_report_anomaly
|
||||
from . import test_reports_controller
|
||||
from . import test_reports_adapter
|
||||
from . import test_fusion_report_tools
|
||||
|
||||
81
fusion_accounting_reports/tests/test_fusion_report_tools.py
Normal file
81
fusion_accounting_reports/tests/test_fusion_report_tools.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""Tests for the 5 fusion AI tools registered in TOOL_DISPATCH."""
|
||||
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
||||
from odoo.addons.fusion_accounting_ai.services.tools import financial_reports as tools
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestFusionReportTools(TransactionCase):
|
||||
|
||||
def test_fusion_run_report_pnl(self):
|
||||
result = tools.fusion_run_report(self.env, {
|
||||
'report_type': 'pnl',
|
||||
'date_from': '2026-01-01',
|
||||
'date_to': '2026-12-31',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.assertEqual(result['report_type'], 'pnl')
|
||||
self.assertIn('rows', result)
|
||||
self.assertIn('row_count', result)
|
||||
|
||||
def test_fusion_get_anomalies(self):
|
||||
result = tools.fusion_get_anomalies(self.env, {
|
||||
'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.assertIn('count', result)
|
||||
|
||||
def test_fusion_generate_commentary(self):
|
||||
result = tools.fusion_generate_commentary(self.env, {
|
||||
'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)
|
||||
|
||||
def test_fusion_drill_down(self):
|
||||
line = self.env['account.move.line'].search(
|
||||
[('parent_state', '=', 'posted')], limit=1,
|
||||
)
|
||||
if not line:
|
||||
self.skipTest("No posted move lines")
|
||||
result = tools.fusion_drill_down_report_line(self.env, {
|
||||
'account_id': line.account_id.id,
|
||||
'date_from': str(line.date),
|
||||
'date_to': str(line.date),
|
||||
'company_id': line.company_id.id,
|
||||
})
|
||||
self.assertIn('rows', result)
|
||||
self.assertIn('count', result)
|
||||
|
||||
def test_fusion_compare_periods(self):
|
||||
result = tools.fusion_compare_periods(self.env, {
|
||||
'report_type': 'pnl',
|
||||
'date_from': '2026-01-01',
|
||||
'date_to': '2026-12-31',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.assertEqual(result['report_type'], 'pnl')
|
||||
|
||||
def test_tools_registered_in_dispatch(self):
|
||||
from odoo.addons.fusion_accounting_ai.services.tools import TOOL_DISPATCH
|
||||
for tool_name in [
|
||||
'fusion_run_report',
|
||||
'fusion_get_anomalies',
|
||||
'fusion_generate_commentary',
|
||||
'fusion_drill_down_report_line',
|
||||
'fusion_compare_periods',
|
||||
]:
|
||||
self.assertIn(
|
||||
tool_name, TOOL_DISPATCH,
|
||||
f"{tool_name} not registered in TOOL_DISPATCH",
|
||||
)
|
||||
Reference in New Issue
Block a user