feat(fusion_accounting_assets): prorate service for partial-period depreciation
Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Fusion Accounting Assets',
|
'name': 'Fusion Accounting Assets',
|
||||||
'version': '19.0.1.0.1',
|
'version': '19.0.1.0.2',
|
||||||
'category': 'Accounting/Accounting',
|
'category': 'Accounting/Accounting',
|
||||||
'summary': 'AI-augmented asset management with depreciation schedules.',
|
'summary': 'AI-augmented asset management with depreciation schedules.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
from . import depreciation_methods
|
from . import depreciation_methods
|
||||||
|
from . import prorate
|
||||||
|
|||||||
34
fusion_accounting_assets/services/prorate.py
Normal file
34
fusion_accounting_assets/services/prorate.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""Prorating helpers for first-period and last-period depreciation.
|
||||||
|
|
||||||
|
When an asset starts mid-month, the first period charges only a fraction
|
||||||
|
of the full period_amount. Three conventions:
|
||||||
|
- 'full_month': always charge full month (no proration)
|
||||||
|
- 'days_365': pro-rate by actual days / 365
|
||||||
|
- 'days_period': pro-rate by actual days in period / total days in period
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
ProrateConvention = Literal['full_month', 'days_365', 'days_period']
|
||||||
|
|
||||||
|
|
||||||
|
def prorate_factor(*, period_start: date, period_end: date,
|
||||||
|
asset_start: date,
|
||||||
|
convention: ProrateConvention = 'days_period') -> float:
|
||||||
|
"""Return a 0..1 factor for how much of `period`'s depreciation
|
||||||
|
applies to an asset that started on `asset_start`."""
|
||||||
|
if convention == 'full_month':
|
||||||
|
return 1.0
|
||||||
|
if asset_start <= period_start:
|
||||||
|
return 1.0
|
||||||
|
if asset_start > period_end:
|
||||||
|
return 0.0
|
||||||
|
actual_days = (period_end - asset_start).days + 1
|
||||||
|
if convention == 'days_365':
|
||||||
|
return actual_days / 365.0
|
||||||
|
if convention == 'days_period':
|
||||||
|
period_days = (period_end - period_start).days + 1
|
||||||
|
return actual_days / period_days
|
||||||
|
raise ValueError(f"Unknown convention: {convention}")
|
||||||
@@ -1 +1,2 @@
|
|||||||
from . import test_depreciation_methods
|
from . import test_depreciation_methods
|
||||||
|
from . import test_prorate
|
||||||
|
|||||||
65
fusion_accounting_assets/tests/test_prorate.py
Normal file
65
fusion_accounting_assets/tests/test_prorate.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.tests import tagged
|
||||||
|
from odoo.addons.fusion_accounting_assets.services.prorate import prorate_factor
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestProrate(TransactionCase):
|
||||||
|
|
||||||
|
def test_full_month_convention_always_one(self):
|
||||||
|
f = prorate_factor(
|
||||||
|
period_start=date(2026, 1, 1),
|
||||||
|
period_end=date(2026, 1, 31),
|
||||||
|
asset_start=date(2026, 1, 15),
|
||||||
|
convention='full_month',
|
||||||
|
)
|
||||||
|
self.assertEqual(f, 1.0)
|
||||||
|
|
||||||
|
def test_asset_starts_before_period_full_factor(self):
|
||||||
|
f = prorate_factor(
|
||||||
|
period_start=date(2026, 1, 1),
|
||||||
|
period_end=date(2026, 1, 31),
|
||||||
|
asset_start=date(2025, 12, 1),
|
||||||
|
convention='days_period',
|
||||||
|
)
|
||||||
|
self.assertEqual(f, 1.0)
|
||||||
|
|
||||||
|
def test_asset_starts_after_period_zero_factor(self):
|
||||||
|
f = prorate_factor(
|
||||||
|
period_start=date(2026, 1, 1),
|
||||||
|
period_end=date(2026, 1, 31),
|
||||||
|
asset_start=date(2026, 2, 5),
|
||||||
|
convention='days_period',
|
||||||
|
)
|
||||||
|
self.assertEqual(f, 0.0)
|
||||||
|
|
||||||
|
def test_days_period_mid_month(self):
|
||||||
|
# Jan 16 -> Jan 31 inclusive = 16 days; period = 31 days
|
||||||
|
f = prorate_factor(
|
||||||
|
period_start=date(2026, 1, 1),
|
||||||
|
period_end=date(2026, 1, 31),
|
||||||
|
asset_start=date(2026, 1, 16),
|
||||||
|
convention='days_period',
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(f, 16 / 31, places=5)
|
||||||
|
|
||||||
|
def test_days_365_mid_month(self):
|
||||||
|
# 16 days / 365
|
||||||
|
f = prorate_factor(
|
||||||
|
period_start=date(2026, 1, 1),
|
||||||
|
period_end=date(2026, 1, 31),
|
||||||
|
asset_start=date(2026, 1, 16),
|
||||||
|
convention='days_365',
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(f, 16 / 365.0, places=5)
|
||||||
|
|
||||||
|
def test_unknown_convention_raises(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
prorate_factor(
|
||||||
|
period_start=date(2026, 1, 1),
|
||||||
|
period_end=date(2026, 1, 31),
|
||||||
|
asset_start=date(2026, 1, 15),
|
||||||
|
convention='bogus', # type: ignore[arg-type]
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user