Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions dcaf/tax/depreciation.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def _build_vdb_candidate_schedule(
period_number = 0

for current_date in dates:
if current_date <= placed_in_service:
if current_date < placed_in_service:
continue
period_number += 1

Expand Down Expand Up @@ -411,7 +411,8 @@ def vdb_schedule(
schedule_dates : Sequence[date] | None, optional
Explicit dates for convention-aware schedule entries. When provided,
depreciation flows are placed on these dates instead of
``placed_in_service + n * frequency``.
``placed_in_service + n * frequency``. Dates before
``placed_in_service`` are ignored; a matching date is included.
valuation_rate : float | None, optional
Annual discount rate used when ``convention`` is
``"best-of-half-year-mid-quarter"``.
Expand Down Expand Up @@ -470,7 +471,7 @@ def vdb_schedule(
... terminal_catch_up=True,
... )
>>> aligned.entries[0].date
datetime.date(2031, 12, 31)
datetime.date(2030, 12, 31)
"""
if isinstance(life, bool) or not isinstance(life, int) or life <= 0:
raise ValueError("life must be a positive integer")
Expand Down Expand Up @@ -508,7 +509,7 @@ def vdb_schedule(
if convention != "none":
if normalized_schedule_dates is None:
delta = time_delta_per_period(frequency)
period_count = life + 1 + (1 if terminal_catch_up else 0)
period_count = life + (1 if terminal_catch_up else 0)
generated_dates: list[date] = []
current_date = placed_in_service
for _ in range(period_count):
Expand Down
21 changes: 19 additions & 2 deletions tests/unit/test_depreciation.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,28 @@ def test_vdb_schedule_half_year_convention_can_add_terminal_catch_up():
)

assert stream.count() == 6
assert stream.entries[0].date == date(2027, 1, 1)
assert stream.entries[0].date == date(2026, 1, 1)
assert stream.entries[-1].date == date(2031, 1, 1)
assert -stream.entries[0].amount == pytest.approx(200.0)
assert -stream.entries[-1].amount == pytest.approx(54.0)
assert sum(entry.amount for entry in stream.entries) == pytest.approx(-1000.0)


def test_vdb_schedule_half_year_convention_without_catch_up_has_life_entries():
"""Including the placed-in-service date must not add an extra schedule period."""
stream = vdb_schedule(
cost_basis=1000.0,
salvage_value=0.0,
placed_in_service=date(2026, 1, 1),
life=5,
convention="half-year",
)

assert [entry.date for entry in stream.entries] == [
date(year, 1, 1) for year in range(2026, 2031)
]


def test_vdb_schedule_mid_quarter_convention_uses_explicit_date_grid():
"""Convention-aware schedules should align entries to the supplied date grid."""
schedule_dates = (
Expand Down Expand Up @@ -248,6 +264,7 @@ def test_vdb_mid_quarter_first_period_by_quarter(placed, expected_first_period):
schedule_dates=tuple(date(year, 12, 31) for year in range(2029, 2037)),
terminal_catch_up=True,
)
assert stream.entries[0].date == date(2030, 12, 31)
assert -stream.entries[0].amount == pytest.approx(expected_first_period)


Expand All @@ -271,7 +288,7 @@ def test_vdb_schedule_best_of_convention_matches_workbook_shape():
schedule_dates = tuple(date(year, 12, 31) for year in range(2030, 2047))
# Workbook models the asset entering service at the start of 2031 (Q1), with the
# first deduction at year-end 2031. Under literal-date quarter semantics that is
# the date 2031-01-01 (skips the 2030-12-31 grid point as an exclusive lower bound).
# the date 2031-01-01, so the preceding 2030-12-31 grid point is skipped.
stream = vdb_schedule(
cost_basis=877824.3662585187,
salvage_value=0.0,
Expand Down