"""Zeitfunktionen. Fachlich gilt durchgängig `Europe/Berlin`.""" from datetime import UTC, date, datetime, timedelta from zoneinfo import ZoneInfo from app.core.config import settings def tz() -> ZoneInfo: return ZoneInfo(settings.timezone) def now() -> datetime: """Aktueller Zeitpunkt in der fachlichen Zeitzone.""" return datetime.now(tz()) def utcnow() -> datetime: """Aktueller Zeitpunkt in UTC – für Zeitstempel in der Datenbank.""" return datetime.now(UTC) def today() -> date: """Heutiges Datum in der fachlichen Zeitzone.""" return now().date() def month_start(day: date) -> date: """Erster Tag des Monats, in dem `day` liegt.""" return day.replace(day=1) def month_end(day: date) -> date: """Letzter Tag des Monats, in dem `day` liegt.""" if day.month == 12: return day.replace(day=31) return day.replace(month=day.month + 1, day=1) - timedelta(days=1) def add_months(day: date, months: int) -> date: """Verschiebt ein Datum um ganze Monate; der Monatserste bleibt Monatserster.""" total = day.month - 1 + months year = day.year + total // 12 month = total % 12 + 1 last_day = month_end(date(year, month, 1)).day return date(year, month, min(day.day, last_day)) # --- Abrechnungsmonat ---------------------------------------------------------- # # Wer am Gehaltstag rechnet, für den beginnt der Monat nicht am Ersten. Ein # Abrechnungsmonat läuft vom Gehaltstag bis zum Vortag des nächsten und trägt # den Namen des Monats, in dem er beginnt: Startet er am 25. September, heißt # er „September“ und endet am 24. Oktober. MIN_MONTH_START_DAY = 1 MAX_MONTH_START_DAY = 31 DEFAULT_MONTH_START_DAY = 1 def month_anchor(day: date, start_day: int) -> date: """Der Gehaltstag im Monat von `day`. Ein Starttag jenseits der Monatslänge rutscht auf den Monatsletzten – so trifft der 31. in jedem Monat den letzten Tag. """ return day.replace(day=min(start_day, month_end(day).day)) def period_start(day: date, start_day: int) -> date: """Beginn des Abrechnungsmonats, in dem `day` liegt.""" anchor = month_anchor(day, start_day) if day >= anchor: return anchor return month_anchor(add_months(month_start(day), -1), start_day) def period_key(day: date, start_day: int) -> date: """Bezeichner des Abrechnungsmonats, in dem `day` liegt – immer ein Monatserster.""" return month_start(period_start(day, start_day)) def period_bounds(month: date, start_day: int) -> tuple[date, date]: """Erster und letzter Tag des Abrechnungsmonats mit dem Bezeichner `month`.""" schluessel = month_start(month) beginn = month_anchor(schluessel, start_day) ende = month_anchor(add_months(schluessel, 1), start_day) - timedelta(days=1) return beginn, ende