feat(fusion_accounting_reports): fusion.report.engine 5-method API

The engine orchestrator. compute_pnl, compute_balance_sheet,
compute_trial_balance, compute_gl, drill_down. All controllers,
wizards, AI tools must route through these methods; no direct
SQL aggregation from anywhere else.

Internal pipeline: validate -> fetch hierarchy -> SQL aggregate
-> resolve line_specs -> optional comparison + anomaly. Uses raw
SQL for the per-account aggregate (the perf-critical step), ORM
for everything else.

Per-company report lookup with global fallback (company_id desc
nulls last). Balance sheet uses 1970 epoch as date_from for
cumulative-since-inception semantics.

7 new tests, 42 total passing.

Made-with: Cursor
This commit is contained in:
gsinghpal
2026-04-19 15:15:54 -04:00
parent 0eee14f69a
commit cabf51add7
5 changed files with 357 additions and 1 deletions

View File

@@ -3,3 +3,4 @@ from . import test_currency_conversion
from . import test_fusion_report
from . import test_line_resolver
from . import test_drill_down_resolver
from . import test_fusion_report_engine

View File

@@ -0,0 +1,109 @@
"""Tests for fusion.report.engine AbstractModel."""
from datetime import date
from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase, tagged
from odoo.addons.fusion_accounting_reports.services.date_periods import Period
@tagged('post_install', '-at_install')
class TestFusionReportEngine(TransactionCase):
def setUp(self):
super().setUp()
self.pnl_report = self.env['fusion.report'].create({
'name': 'Test P&L Engine',
'code': 'test_pnl_engine',
'report_type': 'pnl',
'line_specs': [
{'label': 'Revenue', 'account_type_prefix': 'income_', 'sign': 1},
{'label': 'Expenses', 'account_type_prefix': 'expense_', 'sign': -1},
{'label': 'Net Profit', 'compute': 'subtotal', 'above': 2},
],
'company_id': self.env.company.id,
})
def test_engine_model_exists(self):
self.assertIn('fusion.report.engine', self.env.registry)
def test_compute_pnl_returns_dict_with_rows(self):
period = Period(date(2026, 1, 1), date(2026, 12, 31), 'Test 2026')
result = self.env['fusion.report.engine'].compute_pnl(
period, company_id=self.env.company.id,
)
self.assertIn('rows', result)
self.assertIn('report_type', result)
self.assertEqual(result['report_type'], 'pnl')
def test_compute_balance_sheet(self):
self.env['fusion.report'].create({
'name': 'Test BS',
'code': 'test_bs_engine',
'report_type': 'balance_sheet',
'line_specs': [
{'label': 'Assets', 'account_type_prefix': 'asset_', 'sign': 1},
],
'company_id': self.env.company.id,
})
result = self.env['fusion.report.engine'].compute_balance_sheet(
date(2026, 4, 19), company_id=self.env.company.id,
)
self.assertEqual(result['report_type'], 'balance_sheet')
self.assertEqual(result['period']['date_to'], '2026-04-19')
def test_compute_trial_balance(self):
self.env['fusion.report'].create({
'name': 'Test TB',
'code': 'test_tb_engine',
'report_type': 'trial_balance',
'line_specs': [],
'company_id': self.env.company.id,
})
period = Period(date(2026, 1, 1), date(2026, 12, 31), 'Test 2026')
result = self.env['fusion.report.engine'].compute_trial_balance(
period, company_id=self.env.company.id,
)
self.assertEqual(result['report_type'], 'trial_balance')
def test_compute_pnl_with_comparison(self):
period = Period(date(2026, 1, 1), date(2026, 12, 31), 'Test 2026')
result = self.env['fusion.report.engine'].compute_pnl(
period,
comparison='previous_year',
company_id=self.env.company.id,
)
self.assertIsNotNone(result.get('comparison_period'))
self.assertEqual(result['comparison_period']['date_to'], '2025-12-31')
def test_drill_down_returns_list(self):
line = self.env['account.move.line'].search([
('parent_state', '=', 'posted'),
], limit=1)
if not line:
self.skipTest("No posted lines in DB")
period = Period(line.date, line.date, 'Single day')
rows = self.env['fusion.report.engine'].drill_down(
account_id=line.account_id.id,
period=period,
company_id=line.company_id.id,
)
self.assertIsInstance(rows, list)
def test_no_report_raises_validation_error(self):
period = Period(date(2026, 1, 1), date(2026, 12, 31), 'Test 2026')
# Inactivate any pre-existing GL definitions so the lookup
# fails for this test, then restore them after.
existing = self.env['fusion.report'].search(
[('report_type', '=', 'general_ledger')]
)
prior_active = {r.id: r.active for r in existing}
existing.write({'active': False})
try:
with self.assertRaises(ValidationError):
self.env['fusion.report.engine'].compute_gl(
period, company_id=self.env.company.id,
)
finally:
for r in existing:
r.active = prior_active.get(r.id, True)