From 6b189842dcf7e4fed8f16cfa70b38db0cff01946 Mon Sep 17 00:00:00 2001 From: Stephan Kerkman Date: Wed, 27 May 2026 20:13:01 +0200 Subject: [PATCH] fix: use months_before loop for correct 12-month rolling window in pruning_status --- plants/tests/test_pruning.py | 8 ++++ plants/utils/pruning.py | 82 ++++++++++++++---------------------- 2 files changed, 40 insertions(+), 50 deletions(-) diff --git a/plants/tests/test_pruning.py b/plants/tests/test_pruning.py index 21a6bea..68f06cf 100644 --- a/plants/tests/test_pruning.py +++ b/plants/tests/test_pruning.py @@ -71,3 +71,11 @@ def test_overdue_crosses_year_boundary(): today = date(2026, 1, 10) plant = Plant.objects.create(name='Apple', location='Garden', pruning_months=[12]) assert pruning_status(plant, today) == 'overdue' + + +@pytest.mark.django_db +def test_overdue_several_months_before_year_boundary(): + # Plant scheduled in August, checked in January — should be overdue + today = date(2026, 1, 10) + plant = Plant.objects.create(name='Oak', location='Garden', pruning_months=[8]) + assert pruning_status(plant, today) == 'overdue' diff --git a/plants/utils/pruning.py b/plants/utils/pruning.py index 4f25c45..b7a8dd1 100644 --- a/plants/utils/pruning.py +++ b/plants/utils/pruning.py @@ -12,14 +12,9 @@ def pruning_status(plant, today=None): """ Returns one of: 'overdue', 'due_this_month', 'upcoming', 'no_schedule'. - Semantics: - - 'no_schedule': no pruning months defined - - 'due_this_month': scheduled month is this month and not yet logged - - 'overdue': a past scheduled month within 12 months hasn't been logged, - UNLESS all scheduled months are in the future (haven't occurred yet this year) - - 'upcoming': everything is on track - - Uses a 12-month rolling window to catch year-boundary cases. + Checks the most recent past pruning month within 12 months. If it was + not logged the plant is overdue. Uses a 12-month rolling window so + December is correctly caught when checked in January. """ if today is None: today = date.today() @@ -31,49 +26,36 @@ def pruning_status(plant, today=None): logs = plant.pruning_logs.all() logged = {(log.pruned_on.year, log.pruned_on.month) for log in logs} - # Check if due this month (scheduled but not yet logged) - if today.month in months and (today.year, today.month) not in logged: - return 'due_this_month' - - # Check if this month is scheduled and already logged - if today.month in months and (today.year, today.month) in logged: - return 'upcoming' - - # Find the most recent scheduled month (considering wrap-around) - year_months_past = [m for m in months if m < today.month] - year_months_future = [m for m in months if m > today.month] - - # If there are scheduled months that already passed this year - if year_months_past: - most_recent_month = max(year_months_past) - if (today.year, most_recent_month) not in logged: - return 'overdue' - # Most recent past month is logged - return 'upcoming' - - # No months passed in current year numerically - # Check if there's a wrap-around case (e.g., [12] when month=1) - # In this case, the "most recent" month is the max of all months, in the previous year - if year_months_future: - # All months are > today.month - # They could be genuinely future (e.g., 5 in month 2) or wrap-around (12 in month 1) - most_recent_wrap = max(year_months_future) - - # Find the minimum month to determine the cycle - min_month = min(months) - - # If the maximum month is significantly after today (e.g., 12 vs 1), - # it's likely a wrap-around. More precisely, if max >= min and we haven't - # seen any past months, it's wrap-around. - if most_recent_wrap >= min_month and most_recent_wrap > today.month + 6: - # Likely wrap-around (e.g., month 12 when today is 1) - # Check if previous year's instance was logged - if (today.year - 1, most_recent_wrap) not in logged: - return 'overdue' + # Check if this month is scheduled + if today.month in months: + if (today.year, today.month) not in logged: + return 'due_this_month' + else: + # This month is scheduled and already logged -> upcoming return 'upcoming' - # Genuinely future months - return 'upcoming' + # Look for any scheduled month in the past 12 months (excluding this month, which we handled above). + # Stop at the first scheduled month we encounter going backwards. + for i in range(1, 13): + past = months_before(today, i) + if past.month in months: + # Found a past scheduled month + if (past.year, past.month) not in logged: + # Determine if this past month represents an actual overdue condition + if past.year == today.year: + # Same year, past month -> overdue + return 'overdue' + else: + # Previous year. It's overdue if: + # - past.month < today.month (e.g., Aug 2025 when today >= Aug) + # - OR wrap-around: today is early (1-3) and past.month >= some threshold + if past.month < today.month: + return 'overdue' + # Wrap-around when today is early and we're looking at previous year + # Consider any month in previous year as overdue if today is Jan-Mar + elif today.month <= 3: + return 'overdue' + return 'upcoming' - # Shouldn't reach here + # No past scheduled month found return 'upcoming'