From 50f736d8a7acade82d9d46819b4cae1563dca970 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 15:12:38 -0400 Subject: [PATCH] feat(fusion_accounting_reports): fusion.report definition model Persistent definition of a Fusion financial report. Each report (P&L, balance sheet, trial balance, GL) has one row in fusion.report holding its metadata + line specs (stored as JSON for layout flexibility). V19 conventions: models.Constraint inline, no _sql_constraints. Per- company uniqueness on (company_id, code). 3 new tests, 27 total passing. Made-with: Cursor --- fusion_accounting_reports/__init__.py | 1 + fusion_accounting_reports/__manifest__.py | 2 +- fusion_accounting_reports/models/__init__.py | 1 + .../models/fusion_report.py | 63 +++++++++++++++++++ .../security/ir.model.access.csv | 2 + fusion_accounting_reports/tests/__init__.py | 1 + .../tests/test_fusion_report.py | 44 +++++++++++++ 7 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 fusion_accounting_reports/models/fusion_report.py create mode 100644 fusion_accounting_reports/tests/test_fusion_report.py diff --git a/fusion_accounting_reports/__init__.py b/fusion_accounting_reports/__init__.py index 99464a75..5b1c5641 100644 --- a/fusion_accounting_reports/__init__.py +++ b/fusion_accounting_reports/__init__.py @@ -1 +1,2 @@ from . import services +from . import models diff --git a/fusion_accounting_reports/__manifest__.py b/fusion_accounting_reports/__manifest__.py index 64ef0424..2f5f7e14 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.0', + 'version': '19.0.1.0.1', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).', 'description': """ diff --git a/fusion_accounting_reports/models/__init__.py b/fusion_accounting_reports/models/__init__.py index e69de29b..f8cf3dce 100644 --- a/fusion_accounting_reports/models/__init__.py +++ b/fusion_accounting_reports/models/__init__.py @@ -0,0 +1 @@ +from . import fusion_report diff --git a/fusion_accounting_reports/models/fusion_report.py b/fusion_accounting_reports/models/fusion_report.py new file mode 100644 index 00000000..d14c7eb8 --- /dev/null +++ b/fusion_accounting_reports/models/fusion_report.py @@ -0,0 +1,63 @@ +"""Persistent definition of a Fusion financial report. + +Each report (P&L, balance sheet, trial balance, GL) has ONE row in +fusion.report describing its metadata + line specs. The line specs +are stored as a JSON-typed field for flexibility (each line spec +includes account_type filter, sub-totaling rules, sign convention).""" + +from odoo import _, api, fields, models + + +REPORT_TYPES = [ + ('pnl', 'Income Statement (P&L)'), + ('balance_sheet', 'Balance Sheet'), + ('trial_balance', 'Trial Balance'), + ('general_ledger', 'General Ledger'), +] + + +class FusionReport(models.Model): + _name = "fusion.report" + _description = "Fusion Financial Report Definition" + _order = "sequence, id" + + name = fields.Char(required=True, translate=True) + code = fields.Char( + required=True, + help="Unique technical code (e.g. 'pnl', 'balance_sheet').", + ) + report_type = fields.Selection(REPORT_TYPES, required=True) + sequence = fields.Integer(default=10) + description = fields.Text() + active = fields.Boolean(default=True) + + # Layout config - stored as JSON for flexibility per report type. + # Example for P&L: + # [ + # {"label": "Revenue", "account_type_prefix": "income_", "sign": 1}, + # {"label": "Cost of Goods Sold", "account_type_prefix": "expense_direct_", "sign": -1}, + # {"label": "Gross Profit", "compute": "subtotal", "above": 2}, + # ... + # ] + line_specs = fields.Json(string="Line Specs") + + show_zero_balances = fields.Boolean(default=False) + show_unposted = fields.Boolean(default=False) + default_comparison_mode = fields.Selection( + [ + ('none', 'No comparison'), + ('previous_period', 'Previous Period'), + ('previous_year', 'Previous Year'), + ], + default='none', + ) + + company_id = fields.Many2one( + 'res.company', + default=lambda self: self.env.company, + ) + + _unique_company_code = models.Constraint( + 'UNIQUE(company_id, code)', + 'Report code must be unique per company.', + ) diff --git a/fusion_accounting_reports/security/ir.model.access.csv b/fusion_accounting_reports/security/ir.model.access.csv index 97dd8b91..2cdf3a4b 100644 --- a/fusion_accounting_reports/security/ir.model.access.csv +++ b/fusion_accounting_reports/security/ir.model.access.csv @@ -1 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fusion_report_user,fusion.report.user,model_fusion_report,base.group_user,1,0,0,0 +access_fusion_report_admin,fusion.report.admin,model_fusion_report,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1 diff --git a/fusion_accounting_reports/tests/__init__.py b/fusion_accounting_reports/tests/__init__.py index 53f6331b..70e2fc6e 100644 --- a/fusion_accounting_reports/tests/__init__.py +++ b/fusion_accounting_reports/tests/__init__.py @@ -1,2 +1,3 @@ from . import test_services_unit from . import test_currency_conversion +from . import test_fusion_report diff --git a/fusion_accounting_reports/tests/test_fusion_report.py b/fusion_accounting_reports/tests/test_fusion_report.py new file mode 100644 index 00000000..3341119b --- /dev/null +++ b/fusion_accounting_reports/tests/test_fusion_report.py @@ -0,0 +1,44 @@ +"""Tests for fusion.report definition model.""" + +from odoo.tests.common import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class TestFusionReport(TransactionCase): + + def test_create_minimal(self): + report = self.env['fusion.report'].create({ + 'name': 'Test P&L', + 'code': 'test_pnl_minimal', + 'report_type': 'pnl', + }) + self.assertEqual(report.name, 'Test P&L') + self.assertTrue(report.active) + self.assertEqual(report.default_comparison_mode, 'none') + + def test_line_specs_json_roundtrip(self): + specs = [ + {'label': 'Revenue', 'account_type_prefix': 'income_', 'sign': 1}, + {'label': 'COGS', 'account_type_prefix': 'expense_direct_', 'sign': -1}, + ] + report = self.env['fusion.report'].create({ + 'name': 'Test', + 'code': 'test_json_roundtrip', + 'report_type': 'pnl', + 'line_specs': specs, + }) + self.assertEqual(report.line_specs, specs) + self.assertEqual(report.line_specs[0]['label'], 'Revenue') + + def test_company_code_uniqueness(self): + self.env['fusion.report'].create({ + 'name': 'A', + 'code': 'dup_code_test', + 'report_type': 'pnl', + }) + with self.assertRaises(Exception): + self.env['fusion.report'].create({ + 'name': 'B', + 'code': 'dup_code_test', + 'report_type': 'pnl', + })