"""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)