feat(fusion_accounting_reports): fusion.report.anomaly persisted model

Made-with: Cursor
This commit is contained in:
gsinghpal
2026-04-19 15:32:09 -04:00
parent 22b277c6b8
commit c20e0888e1
6 changed files with 112 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting Reports',
'version': '19.0.1.0.12',
'version': '19.0.1.0.13',
'category': 'Accounting/Accounting',
'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).',
'description': """

View File

@@ -1,3 +1,4 @@
from . import fusion_report
from . import fusion_report_engine
from . import fusion_report_commentary
from . import fusion_report_anomaly

View File

@@ -0,0 +1,56 @@
"""Persisted anomaly flags from the engine's variance detection.
Each row captures one flagged report row variance. Used by the OWL
anomaly_strip + the audit trail."""
from odoo import _, api, fields, models
SEVERITY = [('low', 'Low'), ('medium', 'Medium'), ('high', 'High')]
DIRECTION = [('increase', 'Increase'), ('decrease', 'Decrease')]
class FusionReportAnomaly(models.Model):
_name = "fusion.report.anomaly"
_description = "Flagged Report Variance"
_order = "detected_at desc, severity desc"
report_id = fields.Many2one('fusion.report', required=True, ondelete='cascade')
company_id = fields.Many2one('res.company', required=True,
default=lambda self: self.env.company)
period_from = fields.Date(required=True)
period_to = fields.Date(required=True)
row_id = fields.Char(required=True, help="Engine-generated row id (e.g. 'line_3').")
label = fields.Char(required=True)
current_amount = fields.Float()
comparison_amount = fields.Float()
variance_amount = fields.Float()
variance_pct = fields.Float()
severity = fields.Selection(SEVERITY, required=True)
direction = fields.Selection(DIRECTION, required=True)
detected_at = fields.Datetime(default=fields.Datetime.now, required=True)
state = fields.Selection([
('new', 'New'),
('acknowledged', 'Acknowledged'),
('investigating', 'Investigating'),
('resolved', 'Resolved'),
('dismissed', 'Dismissed'),
], default='new', required=True)
notes = fields.Text()
acknowledged_by = fields.Many2one('res.users')
acknowledged_at = fields.Datetime()
def action_acknowledge(self):
self.write({
'state': 'acknowledged',
'acknowledged_by': self.env.uid,
'acknowledged_at': fields.Datetime.now(),
})
def action_dismiss(self):
self.write({'state': 'dismissed'})
def action_resolve(self):
self.write({'state': 'resolved'})

View File

@@ -2,3 +2,4 @@ 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
access_fusion_report_commentary,fusion.report.commentary,model_fusion_report_commentary,base.group_user,1,1,1,0
access_fusion_report_anomaly,fusion.report.anomaly,model_fusion_report_anomaly,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_fusion_report_user fusion.report.user model_fusion_report base.group_user 1 0 0 0
3 access_fusion_report_admin fusion.report.admin model_fusion_report fusion_accounting_core.group_fusion_accounting_admin 1 1 1 1
4 access_fusion_report_commentary fusion.report.commentary model_fusion_report_commentary base.group_user 1 1 1 0
5 access_fusion_report_anomaly fusion.report.anomaly model_fusion_report_anomaly base.group_user 1 1 1 0

View File

@@ -9,3 +9,4 @@ from . import test_anomaly_detection
from . import test_commentary_prompt
from . import test_commentary_generator
from . import test_fusion_report_commentary
from . import test_fusion_report_anomaly

View File

@@ -0,0 +1,52 @@
"""Tests for fusion.report.anomaly model."""
from datetime import date
from odoo.tests.common import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestFusionReportAnomaly(TransactionCase):
def setUp(self):
super().setUp()
self.report = self.env.ref('fusion_accounting_reports.report_pnl')
def _make(self, **vals):
defaults = {
'report_id': self.report.id,
'period_from': date(2026, 4, 1),
'period_to': date(2026, 4, 30),
'row_id': 'line_0',
'label': 'Revenue',
'current_amount': 12000,
'comparison_amount': 10000,
'variance_amount': 2000,
'variance_pct': 20.0,
'severity': 'medium',
'direction': 'increase',
}
defaults.update(vals)
return self.env['fusion.report.anomaly'].create(defaults)
def test_create_basic(self):
a = self._make()
self.assertEqual(a.severity, 'medium')
self.assertEqual(a.state, 'new')
self.assertTrue(a.detected_at)
def test_acknowledge_action(self):
a = self._make()
a.action_acknowledge()
self.assertEqual(a.state, 'acknowledged')
self.assertEqual(a.acknowledged_by, self.env.user)
self.assertTrue(a.acknowledged_at)
def test_dismiss_action(self):
a = self._make()
a.action_dismiss()
self.assertEqual(a.state, 'dismissed')
def test_resolve_action(self):
a = self._make()
a.action_resolve()
self.assertEqual(a.state, 'resolved')