feat(fusion_accounting_assets): asset anomaly persisted model
- 3 anomaly types: behind_schedule, ahead_of_schedule, low_utilization - 3 severity levels: low, medium, high - expected / actual / variance_pct (mirrors anomaly_detection service output) - 4-state lifecycle: new -> acknowledged -> resolved (or dismissed) - action_acknowledge / action_dismiss / action_resolve transitions - ondelete='cascade' on asset_id (anomalies follow the asset) - 4 new tests (63 total) Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Fusion Accounting Assets',
|
'name': 'Fusion Accounting Assets',
|
||||||
'version': '19.0.1.0.9',
|
'version': '19.0.1.0.10',
|
||||||
'category': 'Accounting/Accounting',
|
'category': 'Accounting/Accounting',
|
||||||
'summary': 'AI-augmented asset management with depreciation schedules.',
|
'summary': 'AI-augmented asset management with depreciation schedules.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ from . import fusion_asset_category
|
|||||||
from . import fusion_asset
|
from . import fusion_asset
|
||||||
from . import fusion_asset_depreciation_line
|
from . import fusion_asset_depreciation_line
|
||||||
from . import fusion_asset_disposal
|
from . import fusion_asset_disposal
|
||||||
|
from . import fusion_asset_anomaly
|
||||||
|
|||||||
42
fusion_accounting_assets/models/fusion_asset_anomaly.py
Normal file
42
fusion_accounting_assets/models/fusion_asset_anomaly.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""Persisted asset anomaly flags from the engine's variance detection."""
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
SEVERITY = [('low', 'Low'), ('medium', 'Medium'), ('high', 'High')]
|
||||||
|
ANOMALY_TYPES = [
|
||||||
|
('behind_schedule', 'Behind Schedule'),
|
||||||
|
('ahead_of_schedule', 'Ahead of Schedule'),
|
||||||
|
('low_utilization', 'Low Utilization'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class FusionAssetAnomaly(models.Model):
|
||||||
|
_name = "fusion.asset.anomaly"
|
||||||
|
_description = "Flagged Asset Anomaly"
|
||||||
|
_order = "detected_at desc, severity desc"
|
||||||
|
|
||||||
|
asset_id = fields.Many2one('fusion.asset', required=True, ondelete='cascade')
|
||||||
|
company_id = fields.Many2one(related='asset_id.company_id', store=True)
|
||||||
|
anomaly_type = fields.Selection(ANOMALY_TYPES, required=True)
|
||||||
|
severity = fields.Selection(SEVERITY, required=True)
|
||||||
|
expected = fields.Float()
|
||||||
|
actual = fields.Float()
|
||||||
|
variance_pct = fields.Float()
|
||||||
|
detail = fields.Text()
|
||||||
|
detected_at = fields.Datetime(default=fields.Datetime.now, required=True)
|
||||||
|
state = fields.Selection([
|
||||||
|
('new', 'New'),
|
||||||
|
('acknowledged', 'Acknowledged'),
|
||||||
|
('resolved', 'Resolved'),
|
||||||
|
('dismissed', 'Dismissed'),
|
||||||
|
], default='new', required=True)
|
||||||
|
|
||||||
|
def action_acknowledge(self):
|
||||||
|
self.write({'state': 'acknowledged'})
|
||||||
|
|
||||||
|
def action_dismiss(self):
|
||||||
|
self.write({'state': 'dismissed'})
|
||||||
|
|
||||||
|
def action_resolve(self):
|
||||||
|
self.write({'state': 'resolved'})
|
||||||
@@ -7,3 +7,5 @@ access_fusion_asset_category_user,fusion.asset.category.user,model_fusion_asset_
|
|||||||
access_fusion_asset_category_admin,fusion.asset.category.admin,model_fusion_asset_category,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
access_fusion_asset_category_admin,fusion.asset.category.admin,model_fusion_asset_category,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
||||||
access_fusion_asset_disposal_user,fusion.asset.disposal.user,model_fusion_asset_disposal,base.group_user,1,0,0,0
|
access_fusion_asset_disposal_user,fusion.asset.disposal.user,model_fusion_asset_disposal,base.group_user,1,0,0,0
|
||||||
access_fusion_asset_disposal_admin,fusion.asset.disposal.admin,model_fusion_asset_disposal,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
access_fusion_asset_disposal_admin,fusion.asset.disposal.admin,model_fusion_asset_disposal,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
||||||
|
access_fusion_asset_anomaly_user,fusion.asset.anomaly.user,model_fusion_asset_anomaly,base.group_user,1,0,0,0
|
||||||
|
access_fusion_asset_anomaly_admin,fusion.asset.anomaly.admin,model_fusion_asset_anomaly,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
||||||
|
|||||||
|
@@ -7,3 +7,4 @@ from . import test_fusion_asset
|
|||||||
from . import test_fusion_asset_depreciation_line
|
from . import test_fusion_asset_depreciation_line
|
||||||
from . import test_fusion_asset_category
|
from . import test_fusion_asset_category
|
||||||
from . import test_fusion_asset_disposal
|
from . import test_fusion_asset_disposal
|
||||||
|
from . import test_fusion_asset_anomaly
|
||||||
|
|||||||
49
fusion_accounting_assets/tests/test_fusion_asset_anomaly.py
Normal file
49
fusion_accounting_assets/tests/test_fusion_asset_anomaly.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.tests import tagged
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestFusionAssetAnomaly(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.asset = self.env['fusion.asset'].create({
|
||||||
|
'name': 'Watched Asset',
|
||||||
|
'cost': 5000,
|
||||||
|
'acquisition_date': date(2026, 1, 1),
|
||||||
|
})
|
||||||
|
|
||||||
|
def _make_anomaly(self, **kw):
|
||||||
|
vals = {
|
||||||
|
'asset_id': self.asset.id,
|
||||||
|
'anomaly_type': 'behind_schedule',
|
||||||
|
'severity': 'medium',
|
||||||
|
'expected': 1000.0,
|
||||||
|
'actual': 700.0,
|
||||||
|
'variance_pct': -30.0,
|
||||||
|
}
|
||||||
|
vals.update(kw)
|
||||||
|
return self.env['fusion.asset.anomaly'].create(vals)
|
||||||
|
|
||||||
|
def test_create_defaults_state_new(self):
|
||||||
|
a = self._make_anomaly()
|
||||||
|
self.assertEqual(a.state, 'new')
|
||||||
|
self.assertTrue(a.detected_at)
|
||||||
|
self.assertEqual(a.company_id, self.asset.company_id)
|
||||||
|
|
||||||
|
def test_acknowledge_transitions(self):
|
||||||
|
a = self._make_anomaly()
|
||||||
|
a.action_acknowledge()
|
||||||
|
self.assertEqual(a.state, 'acknowledged')
|
||||||
|
|
||||||
|
def test_dismiss_transitions(self):
|
||||||
|
a = self._make_anomaly()
|
||||||
|
a.action_dismiss()
|
||||||
|
self.assertEqual(a.state, 'dismissed')
|
||||||
|
|
||||||
|
def test_resolve_transitions(self):
|
||||||
|
a = self._make_anomaly(anomaly_type='low_utilization', severity='high')
|
||||||
|
a.action_resolve()
|
||||||
|
self.assertEqual(a.state, 'resolved')
|
||||||
Reference in New Issue
Block a user