feat(fusion_accounting_assets): 3 depreciation methods (straight, declining, units)
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1 @@
|
||||
from . import depreciation_methods
|
||||
|
||||
116
fusion_accounting_assets/services/depreciation_methods.py
Normal file
116
fusion_accounting_assets/services/depreciation_methods.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user