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
« prev ^ index » next coverage.py v7.10.7, created at 2026-06-23 21:51 +0000
1from __future__ import annotations
3from dataclasses import dataclass
4from decimal import Decimal
6from Economics.shared.choices import CapitalLineDepreciationMode
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
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
29@dataclass(frozen=True)
30class DepreciationSchedule:
31 lines: tuple[DepreciationLine, ...]
33 @property
34 def annual_depreciation(self) -> Decimal:
35 return sum((line.annual_depreciation for line in self.lines), Decimal("0"))
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=())
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))
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 )
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