From bece120ee379f5144c3e4cc7df8244ae65b50300 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 16:47:31 -0400 Subject: [PATCH] feat(fusion_accounting_assets): prorate service for partial-period depreciation Made-with: Cursor --- fusion_accounting_assets/__manifest__.py | 2 +- fusion_accounting_assets/services/__init__.py | 1 + fusion_accounting_assets/services/prorate.py | 34 ++++++++++ fusion_accounting_assets/tests/__init__.py | 1 + .../tests/test_prorate.py | 65 +++++++++++++++++++ 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 fusion_accounting_assets/services/prorate.py create mode 100644 fusion_accounting_assets/tests/test_prorate.py diff --git a/fusion_accounting_assets/__manifest__.py b/fusion_accounting_assets/__manifest__.py index e383eab8..e5652f2d 100644 --- a/fusion_accounting_assets/__manifest__.py +++ b/fusion_accounting_assets/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Fusion Accounting Assets', - 'version': '19.0.1.0.1', + 'version': '19.0.1.0.2', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented asset management with depreciation schedules.', 'description': """ diff --git a/fusion_accounting_assets/services/__init__.py b/fusion_accounting_assets/services/__init__.py index ebcefb4a..17d56607 100644 --- a/fusion_accounting_assets/services/__init__.py +++ b/fusion_accounting_assets/services/__init__.py @@ -1 +1,2 @@ from . import depreciation_methods +from . import prorate diff --git a/fusion_accounting_assets/services/prorate.py b/fusion_accounting_assets/services/prorate.py new file mode 100644 index 00000000..1957aaff --- /dev/null +++ b/fusion_accounting_assets/services/prorate.py @@ -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}") diff --git a/fusion_accounting_assets/tests/__init__.py b/fusion_accounting_assets/tests/__init__.py index 30d9e7e1..d88f02a5 100644 --- a/fusion_accounting_assets/tests/__init__.py +++ b/fusion_accounting_assets/tests/__init__.py @@ -1 +1,2 @@ from . import test_depreciation_methods +from . import test_prorate diff --git a/fusion_accounting_assets/tests/test_prorate.py b/fusion_accounting_assets/tests/test_prorate.py new file mode 100644 index 00000000..f0e9792a --- /dev/null +++ b/fusion_accounting_assets/tests/test_prorate.py @@ -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] + )