dodil/erp-brick-timesheets

DODIL ERP brick: timesheets

src/brick_timesheets/domain/reminders.py

"""The chase (TS-15): weeks nobody submitted and weeks nobody approved.

Plain Python. A reminder is keyed (kind, ref, business_date): the business date is the date in business_timezone, so two passes the same business date send
once and tomorrow's pass sends again. Business days are weekdays (a public holiday is the customer's own calendar, not code here)."""
from datetime import date, datetime, time, timedelta
from zoneinfo import ZoneInfo

DAYS = ("MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN")


def business_days_since(since: date, today: date) -> int:
    """Weekdays after `since` up to and including `today`."""
    n, d = 0, since
    while d < today:
        d += timedelta(days=1)
        if d.weekday() < 5:
            n += 1
    return n


def due_at(week_start: date, submit_due: str, tz: str) -> datetime:
    """When a week's sheet is due: the first `DAY HH:MM` (submit_due, default MON 10:00) after the week ended, in business_timezone."""
    day, _, clock = (submit_due or "MON 10:00").strip().partition(" ")
    weekday = DAYS.index(day.upper()) if day.upper() in DAYS else 0
    try:
        hh, mm = (int(x) for x in clock.split(":"))
        at = time(hh, mm)
    except ValueError:
        at = time(10, 0)
    d = week_start + timedelta(days=7)
    while d.weekday() != weekday:
        d += timedelta(days=1)
    return datetime.combine(d, at, tzinfo=ZoneInfo(tz))


def key(kind: str, ref: str, business_date: date) -> str:
    return f"{kind}:{ref}:{business_date.isoformat()}"