from datetime import date from odoo.tests.common import TransactionCase from odoo.tests import tagged from odoo.exceptions import ValidationError @tagged('post_install', '-at_install') class TestFusionAssetEngine(TransactionCase): def setUp(self): super().setUp() self.engine = self.env['fusion.asset.engine'] self.asset = self.env['fusion.asset'].create({ 'name': 'Test Engine Asset', 'cost': 10000, 'salvage_value': 1000, 'acquisition_date': date(2026, 1, 1), 'in_service_date': date(2026, 1, 1), 'method': 'straight_line', 'useful_life_years': 5, }) def test_engine_model_exists(self): self.assertIn('fusion.asset.engine', self.env.registry) def test_compute_schedule_straight_line(self): result = self.engine.compute_depreciation_schedule(self.asset) self.assertEqual(result['lines_created'], 5) lines = self.asset.depreciation_line_ids self.assertEqual(len(lines), 5) # Total depreciation should equal cost - salvage = 9000 total = sum(lines.mapped('amount')) self.assertAlmostEqual(total, 9000, places=2) def test_compute_schedule_declining_balance(self): self.asset.write({'method': 'declining_balance', 'declining_rate_pct': 30.0}) self.engine.compute_depreciation_schedule(self.asset) lines = self.asset.depreciation_line_ids self.assertGreater(len(lines), 0) # First-period amount should be cost * rate = 10000 * 0.3 = 3000 first = lines.sorted('period_index')[0] self.assertAlmostEqual(first.amount, 3000, places=2) def test_compute_schedule_recompute_wipes_unposted(self): self.engine.compute_depreciation_schedule(self.asset) self.asset.write({'useful_life_years': 8}) self.engine.compute_depreciation_schedule(self.asset, recompute=True) self.assertEqual(len(self.asset.depreciation_line_ids), 8) def test_compute_schedule_validates_zero_cost(self): # Bypass DB constraint with sudo + the constraint allows cost >= 0, # but engine validation requires cost > 0. bad = self.env['fusion.asset'].create({ 'name': 'Zero', 'cost': 0, 'acquisition_date': date(2026, 1, 1), 'method': 'straight_line', 'useful_life_years': 5, }) with self.assertRaises(ValidationError): self.engine.compute_depreciation_schedule(bad) def test_post_depreciation_entry_marks_line_posted(self): self.engine.compute_depreciation_schedule(self.asset) self.asset.action_set_running() result = self.engine.post_depreciation_entry(self.asset) self.assertEqual(result['posted_count'], 1) first_line = self.asset.depreciation_line_ids.sorted('period_index')[0] self.assertTrue(first_line.is_posted) def test_post_depreciation_only_after_running(self): self.engine.compute_depreciation_schedule(self.asset) # asset is still in 'draft' state with self.assertRaises(ValidationError): self.engine.post_depreciation_entry(self.asset) def test_dispose_asset_creates_disposal_record(self): self.engine.compute_depreciation_schedule(self.asset) self.asset.action_set_running() result = self.engine.dispose_asset( self.asset, sale_amount=5000, sale_date=date(2027, 6, 1), ) self.assertEqual(self.asset.state, 'disposed') self.assertIn('disposal_id', result) self.assertEqual(result['book_value_at_disposal'], self.asset.book_value) def test_partial_sale_creates_child_and_disposes(self): self.engine.compute_depreciation_schedule(self.asset) self.asset.action_set_running() original_cost = self.asset.cost result = self.engine.partial_sale( self.asset, sold_amount=3000, sold_qty=0.3, sale_date=date(2027, 6, 1), ) self.assertIn('parent_asset_id', result) self.assertIn('child_asset_id', result) self.asset.invalidate_recordset(['cost']) expected_remaining = round(original_cost * 0.7, 2) self.assertAlmostEqual(self.asset.cost, expected_remaining, places=2) def test_pause_resume_round_trip(self): self.asset.action_set_running() self.engine.pause_asset(self.asset) self.assertEqual(self.asset.state, 'paused') self.engine.resume_asset(self.asset) self.assertEqual(self.asset.state, 'running') def test_reverse_disposal_restores_running_state(self): self.engine.compute_depreciation_schedule(self.asset) self.asset.action_set_running() self.engine.dispose_asset(self.asset, sale_amount=5000) self.assertEqual(self.asset.state, 'disposed') self.engine.reverse_disposal(self.asset) self.assertEqual(self.asset.state, 'running')