The orchestrator AbstractModel for asset depreciation lifecycle. compute_depreciation_schedule, post_depreciation_entry, dispose_asset, partial_sale, pause_asset, resume_asset, reverse_disposal. All controllers, AI tools, wizards, and cron must route through these methods; no direct ORM writes to fusion.asset.depreciation.line or account.move from anywhere else. Made-with: Cursor
116 lines
4.9 KiB
Python
116 lines
4.9 KiB
Python
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')
|