Coverage for backend/django/Economics/settings_profiles/services/depreciation.py: 86%

62 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-06-23 21:51 +0000

1from __future__ import annotations 

2 

3from dataclasses import dataclass 

4from decimal import Decimal 

5 

6from Economics.shared.choices import CapitalLineDepreciationMode 

7 

8from Economics.settings_profiles.models import EconomicsSettingsProfile 

9from Economics.costing.capital.capital_line_sources import GENERATED_CAPITAL_LINE_SOURCE 

10from Economics.formulas.builders.capital import ( 

11 build_custom_capital_line_formula, 

12 build_generated_unit_capex_subtotal_formula, 

13) 

14from Economics.formulas.engine.core import FormulaError 

15from Economics.settings_profiles.services.settings_profiles import get_settings_profile 

16 

17 

18@dataclass(frozen=True) 

19class DepreciationLine: 

20 capital_line_id: int 

21 label: str 

22 depreciation_mode: str 

23 depreciable_basis: Decimal 

24 life_years: int 

25 salvage_percent: Decimal 

26 annual_depreciation: Decimal 

27 

28 

29@dataclass(frozen=True) 

30class DepreciationSchedule: 

31 lines: tuple[DepreciationLine, ...] 

32 

33 @property 

34 def annual_depreciation(self) -> Decimal: 

35 return sum((line.annual_depreciation for line in self.lines), Decimal("0")) 

36 

37 

38def build_straight_line_depreciation_schedule(study) -> DepreciationSchedule: 

39 """Return straight-line depreciation rows for included depreciable capital lines.""" 

40 assumptions = get_settings_profile(study) 

41 if assumptions is None: 

42 return DepreciationSchedule(lines=()) 

43 if not assumptions.depreciation_enabled: 

44 return DepreciationSchedule(lines=()) 

45 

46 generated_subtotal = build_generated_unit_capex_subtotal_formula(study).evaluate() or Decimal("0") 

47 rows: list[DepreciationLine] = [] 

48 for line in study.capital_lines.filter(included=True).order_by("pk"): 

49 policy = _depreciation_policy(line, assumptions) 

50 if policy is None: 

51 continue 

52 life_years, salvage_percent = policy 

53 amount = _capital_line_amount(line, generated_subtotal=generated_subtotal) 

54 if amount is None: 54 ↛ 55line 54 didn't jump to line 55 because the condition on line 54 was never true

55 continue 

56 depreciable_basis = amount * (Decimal("1") - (salvage_percent / Decimal("100"))) 

57 if depreciable_basis < 0: 57 ↛ 58line 57 didn't jump to line 58 because the condition on line 57 was never true

58 continue 

59 rows.append( 

60 DepreciationLine( 

61 capital_line_id=line.pk, 

62 label=line.label, 

63 depreciation_mode=line.depreciation_mode, 

64 depreciable_basis=depreciable_basis, 

65 life_years=life_years, 

66 salvage_percent=salvage_percent, 

67 annual_depreciation=depreciable_basis / Decimal(life_years), 

68 ) 

69 ) 

70 return DepreciationSchedule(lines=tuple(rows)) 

71 

72 

73def _depreciation_policy(line, assumptions: EconomicsSettingsProfile) -> tuple[int, Decimal] | None: 

74 if line.depreciation_mode == CapitalLineDepreciationMode.EXCLUDED: 

75 return None 

76 if line.depreciation_mode == CapitalLineDepreciationMode.CUSTOM: 

77 if line.depreciation_life_years in (None, 0): 77 ↛ 78line 77 didn't jump to line 78 because the condition on line 77 was never true

78 return None 

79 return ( 

80 line.depreciation_life_years, 

81 line.depreciation_salvage_percent or Decimal("0"), 

82 ) 

83 if assumptions.default_depreciation_life_years in (None, 0): 83 ↛ 84line 83 didn't jump to line 84 because the condition on line 83 was never true

84 return None 

85 return ( 

86 assumptions.default_depreciation_life_years, 

87 assumptions.default_depreciation_salvage_percent or Decimal("0"), 

88 ) 

89 

90 

91def _capital_line_amount(line, *, generated_subtotal: Decimal) -> Decimal | None: 

92 if line.source == GENERATED_CAPITAL_LINE_SOURCE: 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true

93 return line.amount 

94 try: 

95 return build_custom_capital_line_formula(line, base_capex=generated_subtotal).evaluate() 

96 except FormulaError: 

97 return None