bloombase/plants/utils/pruning.py

79 lines
2.9 KiB
Python

from datetime import date
def months_before(d, n):
"""Return a date on the 1st of the month n months before d."""
m = (d.month - 1 - n) % 12 + 1
y = d.year + (d.month - 1 - n) // 12
return date(y, m, 1)
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.
"""
if today is None:
today = date.today()
months = plant.pruning_months or []
if not months:
return 'no_schedule'
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'
return 'upcoming'
# Genuinely future months
return 'upcoming'
# Shouldn't reach here
return 'upcoming'