From b7c171f983ae18194f71627f3579ad93780830ff Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 16:48:18 -0400 Subject: [PATCH] feat(fusion_accounting_assets): salvage_value service Made-with: Cursor --- fusion_accounting_assets/__manifest__.py | 2 +- fusion_accounting_assets/services/__init__.py | 1 + .../services/salvage_value.py | 38 ++++++++++++++++ fusion_accounting_assets/tests/__init__.py | 1 + .../tests/test_salvage_value.py | 45 +++++++++++++++++++ 5 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 fusion_accounting_assets/services/salvage_value.py create mode 100644 fusion_accounting_assets/tests/test_salvage_value.py diff --git a/fusion_accounting_assets/__manifest__.py b/fusion_accounting_assets/__manifest__.py index e5652f2d..7df0c866 100644 --- a/fusion_accounting_assets/__manifest__.py +++ b/fusion_accounting_assets/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Fusion Accounting Assets', - 'version': '19.0.1.0.2', + 'version': '19.0.1.0.3', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented asset management with depreciation schedules.', 'description': """ diff --git a/fusion_accounting_assets/services/__init__.py b/fusion_accounting_assets/services/__init__.py index 17d56607..7be9de01 100644 --- a/fusion_accounting_assets/services/__init__.py +++ b/fusion_accounting_assets/services/__init__.py @@ -1,2 +1,3 @@ from . import depreciation_methods from . import prorate +from . import salvage_value diff --git a/fusion_accounting_assets/services/salvage_value.py b/fusion_accounting_assets/services/salvage_value.py new file mode 100644 index 00000000..ff82ea78 --- /dev/null +++ b/fusion_accounting_assets/services/salvage_value.py @@ -0,0 +1,38 @@ +"""Salvage value (scrap value) calculation helpers. + +Most clients use straight % of cost; some use fixed dollar amounts. +""" + +from dataclasses import dataclass +from typing import Literal + + +SalvageMethod = Literal['percentage', 'fixed', 'zero'] + + +@dataclass +class SalvageConfig: + method: SalvageMethod + value: float = 0.0 + + +def compute_salvage_value(*, cost: float, config: SalvageConfig) -> float: + """Compute end-of-life salvage value.""" + if config.method == 'zero': + return 0.0 + if config.method == 'percentage': + return round(cost * config.value / 100, 2) + if config.method == 'fixed': + return round(config.value, 2) + raise ValueError(f"Unknown salvage method: {config.method}") + + +def remaining_useful_life_value(*, current_book: float, salvage: float, + periods_used: int, total_periods: int) -> float: + """Estimate remaining value if asset is sold/scrapped now.""" + if total_periods <= 0: + return current_book + if periods_used >= total_periods: + return salvage + remaining_pct = (total_periods - periods_used) / total_periods + return round(salvage + (current_book - salvage) * remaining_pct, 2) diff --git a/fusion_accounting_assets/tests/__init__.py b/fusion_accounting_assets/tests/__init__.py index d88f02a5..a070ccd1 100644 --- a/fusion_accounting_assets/tests/__init__.py +++ b/fusion_accounting_assets/tests/__init__.py @@ -1,2 +1,3 @@ from . import test_depreciation_methods from . import test_prorate +from . import test_salvage_value diff --git a/fusion_accounting_assets/tests/test_salvage_value.py b/fusion_accounting_assets/tests/test_salvage_value.py new file mode 100644 index 00000000..5e6e6efb --- /dev/null +++ b/fusion_accounting_assets/tests/test_salvage_value.py @@ -0,0 +1,45 @@ +from odoo.tests.common import TransactionCase +from odoo.tests import tagged +from odoo.addons.fusion_accounting_assets.services.salvage_value import ( + SalvageConfig, compute_salvage_value, remaining_useful_life_value, +) + + +@tagged('post_install', '-at_install') +class TestSalvageValue(TransactionCase): + + def test_zero_method_returns_zero(self): + v = compute_salvage_value(cost=10000, config=SalvageConfig(method='zero')) + self.assertEqual(v, 0.0) + + def test_percentage_method(self): + v = compute_salvage_value( + cost=10000, config=SalvageConfig(method='percentage', value=10), + ) + self.assertAlmostEqual(v, 1000.0, places=2) + + def test_fixed_method(self): + v = compute_salvage_value( + cost=10000, config=SalvageConfig(method='fixed', value=750), + ) + self.assertAlmostEqual(v, 750.0, places=2) + + def test_unknown_method_raises(self): + with self.assertRaises(ValueError): + compute_salvage_value( + cost=10000, + config=SalvageConfig(method='bogus', value=0), # type: ignore[arg-type] + ) + + def test_remaining_useful_life_value_midway(self): + # Halfway through life; current book 6000, salvage 1000 -> 1000 + 5000*0.5 = 3500 + v = remaining_useful_life_value( + current_book=6000, salvage=1000, periods_used=5, total_periods=10, + ) + self.assertAlmostEqual(v, 3500.0, places=2) + + def test_remaining_useful_life_value_at_end_returns_salvage(self): + v = remaining_useful_life_value( + current_book=1200, salvage=1000, periods_used=10, total_periods=10, + ) + self.assertEqual(v, 1000.0)