117 lines
4.0 KiB
Python
117 lines
4.0 KiB
Python
"""Depreciation method primitives.
|
|
|
|
Three methods supported:
|
|
- straight_line: equal periodic charge over useful_life
|
|
- declining_balance: % per period of remaining book value
|
|
- units_of_production: charge proportional to units used / total units expected
|
|
|
|
All return a list of DepreciationStep dataclasses (period_index, period_amount,
|
|
accumulated_depreciation, book_value_at_end). Total depreciation always
|
|
sums to (cost - salvage_value), within 1-cent rounding tolerance.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Literal
|
|
|
|
|
|
Method = Literal['straight_line', 'declining_balance', 'units_of_production']
|
|
|
|
|
|
@dataclass
|
|
class DepreciationStep:
|
|
period_index: int
|
|
period_amount: float
|
|
accumulated_depreciation: float
|
|
book_value_at_end: float
|
|
|
|
|
|
def straight_line(*, cost: float, salvage_value: float = 0.0,
|
|
n_periods: int) -> list[DepreciationStep]:
|
|
"""Equal charge per period: (cost - salvage) / n_periods.
|
|
|
|
Last period absorbs rounding so total == cost - salvage exactly.
|
|
"""
|
|
if n_periods < 1:
|
|
return []
|
|
depreciable = cost - salvage_value
|
|
per_period = round(depreciable / n_periods, 2)
|
|
steps = []
|
|
accumulated = 0.0
|
|
for i in range(n_periods):
|
|
if i == n_periods - 1:
|
|
amount = round(depreciable - accumulated, 2)
|
|
else:
|
|
amount = per_period
|
|
accumulated = round(accumulated + amount, 2)
|
|
book = round(cost - accumulated, 2)
|
|
steps.append(DepreciationStep(
|
|
period_index=i,
|
|
period_amount=amount,
|
|
accumulated_depreciation=accumulated,
|
|
book_value_at_end=book,
|
|
))
|
|
return steps
|
|
|
|
|
|
def declining_balance(*, cost: float, salvage_value: float = 0.0,
|
|
n_periods: int, rate: float) -> list[DepreciationStep]:
|
|
"""Apply `rate` (e.g. 0.20 = 20%) to remaining book each period.
|
|
|
|
Switches to straight-line when straight-line would deplete remaining book
|
|
faster (typical Odoo behavior). Last step caps at salvage_value.
|
|
"""
|
|
if n_periods < 1 or rate <= 0:
|
|
return []
|
|
if rate >= 1:
|
|
# Pathological: 100%+ rate. Charge full depreciable amount in period 0.
|
|
depreciable = round(cost - salvage_value, 2)
|
|
return [DepreciationStep(0, depreciable, depreciable, round(salvage_value, 2))]
|
|
steps = []
|
|
book = cost
|
|
accumulated = 0.0
|
|
for i in range(n_periods):
|
|
remaining_periods = n_periods - i
|
|
db_amount = round(book * rate, 2)
|
|
sl_amount = round((book - salvage_value) / remaining_periods, 2) if remaining_periods else 0.0
|
|
amount = max(db_amount, sl_amount)
|
|
if book - amount < salvage_value:
|
|
amount = round(book - salvage_value, 2)
|
|
accumulated = round(accumulated + amount, 2)
|
|
book = round(book - amount, 2)
|
|
steps.append(DepreciationStep(
|
|
period_index=i,
|
|
period_amount=amount,
|
|
accumulated_depreciation=accumulated,
|
|
book_value_at_end=book,
|
|
))
|
|
if book <= salvage_value:
|
|
break
|
|
return steps
|
|
|
|
|
|
def units_of_production(*, cost: float, salvage_value: float = 0.0,
|
|
total_units_expected: float,
|
|
units_per_period: list[float]) -> list[DepreciationStep]:
|
|
"""Charge per period = (units_used / total_expected) * (cost - salvage)."""
|
|
if total_units_expected <= 0:
|
|
return []
|
|
depreciable = cost - salvage_value
|
|
per_unit = depreciable / total_units_expected
|
|
steps = []
|
|
accumulated = 0.0
|
|
for i, units in enumerate(units_per_period):
|
|
amount = round(units * per_unit, 2)
|
|
if accumulated + amount > depreciable:
|
|
amount = round(depreciable - accumulated, 2)
|
|
accumulated = round(accumulated + amount, 2)
|
|
book = round(cost - accumulated, 2)
|
|
steps.append(DepreciationStep(
|
|
period_index=i,
|
|
period_amount=amount,
|
|
accumulated_depreciation=accumulated,
|
|
book_value_at_end=book,
|
|
))
|
|
if accumulated >= depreciable:
|
|
break
|
|
return steps
|