test(fusion_accounting_assets): Hypothesis property-based depreciation invariants
Made-with: Cursor
This commit is contained in:
@@ -15,3 +15,4 @@ from . import test_assets_controller
|
||||
from . import test_assets_adapter
|
||||
from . import test_asset_tools
|
||||
from . import test_assets_cron
|
||||
from . import test_engine_property
|
||||
|
||||
101
fusion_accounting_assets/tests/test_engine_property.py
Normal file
101
fusion_accounting_assets/tests/test_engine_property.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""Property-based invariant tests for the asset engine.
|
||||
|
||||
Hypothesis generates random inputs; we assert mathematical invariants
|
||||
that must hold regardless of input."""
|
||||
|
||||
from hypothesis import given, settings, strategies as st, HealthCheck
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.fusion_accounting_assets.services.depreciation_methods import (
|
||||
straight_line, declining_balance, units_of_production,
|
||||
)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install', 'property_based')
|
||||
class TestDepreciationInvariants(TransactionCase):
|
||||
|
||||
@given(
|
||||
cost=st.floats(min_value=100.0, max_value=1000000.0,
|
||||
allow_nan=False, allow_infinity=False),
|
||||
salvage_pct=st.floats(min_value=0.0, max_value=0.5,
|
||||
allow_nan=False, allow_infinity=False),
|
||||
n_periods=st.integers(min_value=1, max_value=40),
|
||||
)
|
||||
@settings(max_examples=80, deadline=2000,
|
||||
suppress_health_check=[HealthCheck.function_scoped_fixture])
|
||||
def test_straight_line_total_equals_cost_minus_salvage(self, cost, salvage_pct, n_periods):
|
||||
cost = round(cost, 2)
|
||||
salvage = round(cost * salvage_pct, 2)
|
||||
steps = straight_line(cost=cost, salvage_value=salvage, n_periods=n_periods)
|
||||
total = sum(s.period_amount for s in steps)
|
||||
# Within 1c rounding tolerance
|
||||
self.assertAlmostEqual(
|
||||
total, cost - salvage, places=1,
|
||||
msg=f"cost={cost}, salvage={salvage}, n={n_periods}, total={total:.2f}",
|
||||
)
|
||||
|
||||
@given(
|
||||
cost=st.floats(min_value=100.0, max_value=1000000.0,
|
||||
allow_nan=False, allow_infinity=False),
|
||||
salvage_pct=st.floats(min_value=0.0, max_value=0.5,
|
||||
allow_nan=False, allow_infinity=False),
|
||||
n_periods=st.integers(min_value=1, max_value=20),
|
||||
)
|
||||
@settings(max_examples=50, deadline=2000,
|
||||
suppress_health_check=[HealthCheck.function_scoped_fixture])
|
||||
def test_straight_line_book_value_decreasing(self, cost, salvage_pct, n_periods):
|
||||
cost = round(cost, 2)
|
||||
salvage = round(cost * salvage_pct, 2)
|
||||
steps = straight_line(cost=cost, salvage_value=salvage, n_periods=n_periods)
|
||||
for i in range(1, len(steps)):
|
||||
self.assertLessEqual(
|
||||
steps[i].book_value_at_end,
|
||||
steps[i - 1].book_value_at_end + 0.01,
|
||||
)
|
||||
|
||||
@given(
|
||||
cost=st.floats(min_value=1000.0, max_value=100000.0,
|
||||
allow_nan=False, allow_infinity=False),
|
||||
salvage_pct=st.floats(min_value=0.0, max_value=0.3,
|
||||
allow_nan=False, allow_infinity=False),
|
||||
n_periods=st.integers(min_value=2, max_value=20),
|
||||
rate=st.floats(min_value=0.05, max_value=0.5,
|
||||
allow_nan=False, allow_infinity=False),
|
||||
)
|
||||
@settings(max_examples=50, deadline=3000,
|
||||
suppress_health_check=[HealthCheck.function_scoped_fixture])
|
||||
def test_declining_balance_never_below_salvage(self, cost, salvage_pct, n_periods, rate):
|
||||
cost = round(cost, 2)
|
||||
salvage = round(cost * salvage_pct, 2)
|
||||
steps = declining_balance(
|
||||
cost=cost, salvage_value=salvage,
|
||||
n_periods=n_periods, rate=rate,
|
||||
)
|
||||
for s in steps:
|
||||
self.assertGreaterEqual(
|
||||
s.book_value_at_end, salvage - 0.01,
|
||||
msg=f"cost={cost}, salvage={salvage}, rate={rate}, step={s}",
|
||||
)
|
||||
|
||||
@given(
|
||||
cost=st.floats(min_value=1000.0, max_value=100000.0,
|
||||
allow_nan=False, allow_infinity=False),
|
||||
total_units=st.floats(min_value=100.0, max_value=10000.0,
|
||||
allow_nan=False, allow_infinity=False),
|
||||
n_periods=st.integers(min_value=1, max_value=10),
|
||||
)
|
||||
@settings(max_examples=30, deadline=2000,
|
||||
suppress_health_check=[HealthCheck.function_scoped_fixture])
|
||||
def test_units_of_production_total_at_full_use_equals_depreciable(self, cost, total_units, n_periods):
|
||||
cost = round(cost, 2)
|
||||
salvage = 0.0
|
||||
# Distribute total_units evenly across periods
|
||||
per_period = total_units / n_periods
|
||||
steps = units_of_production(
|
||||
cost=cost, salvage_value=salvage,
|
||||
total_units_expected=total_units,
|
||||
units_per_period=[per_period] * n_periods,
|
||||
)
|
||||
total = sum(s.period_amount for s in steps)
|
||||
self.assertAlmostEqual(total, cost - salvage, places=1)
|
||||
Reference in New Issue
Block a user