First Upload
This commit is contained in:
48
CP Tagesrechner Jahresplanung/readme.md
Normal file
48
CP Tagesrechner Jahresplanung/readme.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 🗓️ Termin-Generator für Change-Planung
|
||||
**Version:** v0.10
|
||||
**Autor:** Mike Lindner
|
||||
|
||||
---
|
||||
|
||||
## 📌 Übersicht
|
||||
|
||||
Der Termin-Generator ist ein Desktop-Tool für Windows, das automatisiert wiederkehrende Termine für ein vollständiges Kalenderjahr erzeugt.
|
||||
Es wurde speziell für **IT-Change-Management**, **Wartungsfenster-Planung** und **regulatorische IT-Rhythmen** entwickelt.
|
||||
|
||||
Typische Beispiele:
|
||||
|
||||
- „Jeden zweiten Mittwoch im Monat um 19:00 Uhr“
|
||||
- „Dienstag in der Woche nach dem zweiten Dienstag“
|
||||
- „15. oder nächster Werktag, falls Wochenende“
|
||||
- „Nur in ungeraden ISO-Kalenderwochen“
|
||||
- „Letzter Kalendertag des Quartals“
|
||||
- „Monatlicher Samstag nach dem dritten Donnerstag“
|
||||
|
||||
Die erzeugten Termine können direkt weiterverarbeitet werden (Excel, Jira, Confluence, Planner, ServiceNow, Outlook etc.).
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Funktionen
|
||||
|
||||
| Funktion | Status |
|
||||
|----------|--------|
|
||||
| Wiederkehrende Termine (täglich, wöchentlich, monatlich, quartalsweise) | ✔️ |
|
||||
| Berechnung kompletter Serien für 1 Jahr | ✔️ |
|
||||
| ISO-Kalenderwochen-Filter (ungerade / gerade) | ✔️ |
|
||||
| Monatsregeln (z. B. „2. Mittwoch“, „Fester Tag“, „Folgetag bei Wochenende“) | ✔️ |
|
||||
| Relative Terminlogik („Mittwoch nach 2. Dienstag“, „+1 Woche“) | ✔️ |
|
||||
| Kopieren der erzeugten Serien in die Zwischenablage | ✔️ |
|
||||
| Bedienoberfläche mit selbsterklärenden Beschriftungen und Emojis | ✔️ |
|
||||
| Quartalsfunktionen: letzter Tag im Quartal oder frei definierbar | ✔️ |
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Systemvoraussetzungen
|
||||
|
||||
- Windows mit **Python 3.x**
|
||||
- Kein Administrator-Zugriff notwendig
|
||||
|
||||
Optional (für automatische Zwischenablage-Kopie):
|
||||
|
||||
```bash
|
||||
pip install pyperclip
|
||||
848
CP Tagesrechner Jahresplanung/tagerechner_win_v0.10.py
Normal file
848
CP Tagesrechner Jahresplanung/tagerechner_win_v0.10.py
Normal file
@@ -0,0 +1,848 @@
|
||||
# tagerechner_win_v0_10.py
|
||||
# Zweck: Termine (täglich / wöchentlich / monatlich / quartalsweise) eines Jahres generieren
|
||||
# v0.10 – Quartalsweise erweitert:
|
||||
# - Quartalsweise:
|
||||
# - Option 1: letzter Kalendertag des Quartals
|
||||
# - Option 2: frei wählbarer Kalendertag im Quartalsmonat (z. B. 15.)
|
||||
#
|
||||
# v0.09 – UI-Refactor (deutsch, mit Emojis, besser verständliche Bezeichnungen)
|
||||
#
|
||||
# Logik:
|
||||
# - täglich: jeder Kalendertag
|
||||
# - wöchentlich: bestimmter Wochentag, optional nur ungerade/gerade Kalenderwochen
|
||||
# - monatlich:
|
||||
# 1) 📅 Fester Kalendertag
|
||||
# 2) 📆 Wiederkehrender Wochentag (z. B. 2. Mittwoch)
|
||||
# 3) ➡ Kalendertag mit Verschiebung auf nächsten Werktag (Mo–Fr)
|
||||
# 4) 🏢 Erster Werktag des Monats (Mo–Fr)
|
||||
# 5) ↪ Termin abhängig von anderem Datum (relativer Wochentag, mit Wochen-Offset)
|
||||
# - quartalsweise:
|
||||
# 1) 🏣 Letzter Kalendertag des Quartals
|
||||
# 2) 📅 Fester Kalendertag im Quartalsmonat (z. B. 15. im März/Juni/Sept./Dez.)
|
||||
|
||||
VERSION = "v0.10"
|
||||
|
||||
import re
|
||||
import calendar
|
||||
from datetime import datetime, timedelta, date
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
|
||||
try:
|
||||
import pyperclip
|
||||
HAS_PYPERCLIP = True
|
||||
except Exception:
|
||||
HAS_PYPERCLIP = False
|
||||
|
||||
|
||||
# ---------- Hilfsfunktionen ----------
|
||||
|
||||
def validate_time_format(time_str: str) -> str:
|
||||
"""
|
||||
Erwartet HH:MM. Liefert normalisiert HH:MM:SS (Sekunden = :00).
|
||||
"""
|
||||
if not re.match(r'^\d{2}:\d{2}$', time_str):
|
||||
raise ValueError("Uhrzeit bitte als HH:MM angeben.")
|
||||
h, m = map(int, time_str.split(":"))
|
||||
if not (0 <= h < 24 and 0 <= m < 60):
|
||||
raise ValueError("Uhrzeit ungültig (Stunden 0–23, Minuten 0–59).")
|
||||
return f"{h:02d}:{m:02d}:00"
|
||||
|
||||
|
||||
def nth_weekday_in_month(year: int, month: int, n: int, weekday0: int):
|
||||
"""
|
||||
Liefert das Datum des n-ten weekday0 (0=Mo..6=So) im Monat.
|
||||
Gibt None zurück, wenn es dieses n-te Vorkommen in diesem Monat nicht gibt.
|
||||
"""
|
||||
first_weekday, days_in_month = calendar.monthrange(year, month) # first_weekday: 0=Mo..6=So
|
||||
delta = (weekday0 - first_weekday) % 7
|
||||
day = 1 + delta + 7 * (n - 1)
|
||||
if day > days_in_month:
|
||||
return None
|
||||
return date(year, month, day)
|
||||
|
||||
|
||||
def format_datetime_for_output(d: date, time_hhmmss: str) -> str:
|
||||
return d.strftime("%d.%m.%Y ") + time_hhmmss
|
||||
|
||||
|
||||
def iso_week_number(d: date) -> int:
|
||||
"""ISO-Kalenderwoche (1..53)."""
|
||||
iso = d.isocalendar()
|
||||
return getattr(iso, "week", iso[1])
|
||||
|
||||
|
||||
def week_parity_ok(d: date, parity: str | None) -> bool:
|
||||
"""
|
||||
parity: None | 'odd' | 'even'
|
||||
"""
|
||||
if parity is None:
|
||||
return True
|
||||
w = iso_week_number(d)
|
||||
if parity == 'odd':
|
||||
return (w % 2) == 1
|
||||
if parity == 'even':
|
||||
return (w % 2) == 0
|
||||
return True
|
||||
|
||||
|
||||
def first_workday_of_month(year: int, month: int) -> date:
|
||||
"""
|
||||
1. Werktag (Mo–Fr) des Monats.
|
||||
(Feiertage werden NICHT berücksichtigt.)
|
||||
"""
|
||||
d = date(year, month, 1) # 1. des Monats
|
||||
wd = d.weekday() # Mo=0 .. So=6
|
||||
if wd >= 5: # Sa(5) -> +2, So(6) -> +1
|
||||
d += timedelta(days=(7 - wd))
|
||||
return d
|
||||
|
||||
|
||||
def next_workday(d: date) -> date:
|
||||
"""
|
||||
Nächster Werktag (Mo–Fr) ab d, inkl. d selbst wenn d ein Werktag ist.
|
||||
(Feiertage werden NICHT berücksichtigt.)
|
||||
"""
|
||||
while d.weekday() >= 5: # Sa(5), So(6)
|
||||
d += timedelta(days=1)
|
||||
return d
|
||||
|
||||
|
||||
def weekday_name_de(weekday1: int) -> str:
|
||||
"""Wochentag 1..7 -> deutscher Name."""
|
||||
names = {
|
||||
1: "Montag",
|
||||
2: "Dienstag",
|
||||
3: "Mittwoch",
|
||||
4: "Donnerstag",
|
||||
5: "Freitag",
|
||||
6: "Samstag",
|
||||
7: "Sonntag",
|
||||
}
|
||||
return names.get(weekday1, f"Wochentag {weekday1}")
|
||||
|
||||
|
||||
def ordinal_de(n: int) -> str:
|
||||
"""1 -> '1.', 2 -> '2.' etc."""
|
||||
return f"{n}."
|
||||
|
||||
|
||||
# ---------- Kernlogik ----------
|
||||
|
||||
def generate_dates(year: int,
|
||||
frequency: str,
|
||||
time_hhmmss: str,
|
||||
weekday_for_weekly: int | None = None,
|
||||
weekly_iso_week_parity: str | None = None,
|
||||
monthly_option: int | None = None,
|
||||
day_of_month: int | None = None,
|
||||
week_in_month: int | None = None,
|
||||
weekday_for_monthly: int | None = None,
|
||||
target_weekday_for_monthly: int | None = None,
|
||||
relative_week_offset: int = 0,
|
||||
monthly_iso_week_parity: str | None = None,
|
||||
quarterly_option: int | None = None) -> list[str]:
|
||||
"""
|
||||
frequency: 't' (täglich), 'w' (wöchentlich), 'm' (monatlich), 'q' (quartalsweise)
|
||||
weekday_*: 1=Mo .. 7=So (Benutzer-Sicht). Intern 0..6.
|
||||
monthly_option:
|
||||
1 = 📅 Fester Kalendertag
|
||||
2 = 📆 Wiederkehrender Wochentag (+ optionaler ISO-KW-Filter)
|
||||
3 = ➡ Kalendertag mit Verschiebung auf nächsten Werktag (Mo–Fr)
|
||||
4 = 🏢 Erster Werktag des Monats (Mo–Fr)
|
||||
5 = ↪ Termin abhängig von anderem Datum (relativer Wochentag, mit Wochen-Offset)
|
||||
quarterly_option:
|
||||
1 = 🏣 Letzter Kalendertag des Quartals
|
||||
2 = 📅 Fester Kalendertag im Quartalsmonat (z. B. 15.)
|
||||
"""
|
||||
start_date = date(year, 1, 1)
|
||||
end_date = date(year, 12, 31)
|
||||
out = []
|
||||
|
||||
if frequency == 't':
|
||||
cur = start_date
|
||||
while cur <= end_date:
|
||||
out.append(format_datetime_for_output(cur, time_hhmmss))
|
||||
cur += timedelta(days=1)
|
||||
|
||||
elif frequency == 'w':
|
||||
if weekday_for_weekly is None:
|
||||
raise ValueError("Bitte einen Wochentag für wöchentliche Termine auswählen.")
|
||||
wk = (weekday_for_weekly - 1) % 7 # 0..6
|
||||
# erstes Auftreten im Jahr
|
||||
delta = (wk - start_date.weekday()) % 7
|
||||
cur = start_date + timedelta(days=delta)
|
||||
|
||||
# Falls ISO-Parität gefordert: auf erste passende Woche vorrücken
|
||||
if weekly_iso_week_parity in ('odd', 'even'):
|
||||
while cur <= end_date and not week_parity_ok(cur, weekly_iso_week_parity):
|
||||
cur += timedelta(days=7)
|
||||
step = 14 # Parität bleibt bei +14 erhalten
|
||||
else:
|
||||
step = 7
|
||||
|
||||
while cur <= end_date:
|
||||
if week_parity_ok(cur, weekly_iso_week_parity):
|
||||
out.append(format_datetime_for_output(cur, time_hhmmss))
|
||||
cur += timedelta(days=step)
|
||||
|
||||
elif frequency == 'm':
|
||||
cur = start_date.replace(day=1)
|
||||
while cur <= end_date:
|
||||
if monthly_option == 1:
|
||||
# 📅 Fester Kalendertag
|
||||
if day_of_month is None:
|
||||
raise ValueError("Bitte 'Tag im Monat' angeben (1..31).")
|
||||
days_in_month = calendar.monthrange(cur.year, cur.month)[1]
|
||||
if 1 <= day_of_month <= days_in_month:
|
||||
d = date(cur.year, cur.month, day_of_month)
|
||||
if d <= end_date:
|
||||
out.append(format_datetime_for_output(d, time_hhmmss))
|
||||
|
||||
elif monthly_option == 2:
|
||||
# 📆 Wiederkehrender Wochentag + optionaler ISO-KW-Filter
|
||||
if week_in_month is None or weekday_for_monthly is None:
|
||||
raise ValueError("Bitte Vorkommen (1..5) und Wochentag auswählen.")
|
||||
wk2 = (weekday_for_monthly - 1) % 7 # 0..6
|
||||
d = nth_weekday_in_month(cur.year, cur.month, week_in_month, wk2)
|
||||
if d is not None and d <= end_date:
|
||||
if week_parity_ok(d, monthly_iso_week_parity):
|
||||
out.append(format_datetime_for_output(d, time_hhmmss))
|
||||
|
||||
elif monthly_option == 3:
|
||||
# ➡ Kalendertag mit Verschiebung auf nächsten Werktag (Mo–Fr)
|
||||
if day_of_month is None:
|
||||
raise ValueError("Bitte 'Kalendertag' angeben (1..31).")
|
||||
days_in_month = calendar.monthrange(cur.year, cur.month)[1]
|
||||
if 1 <= day_of_month <= days_in_month:
|
||||
d = date(cur.year, cur.month, day_of_month)
|
||||
d = next_workday(d)
|
||||
if d <= end_date and d.month == cur.month:
|
||||
out.append(format_datetime_for_output(d, time_hhmmss))
|
||||
|
||||
elif monthly_option == 4:
|
||||
# 🏢 Erster Werktag im Monat (Mo–Fr)
|
||||
d = first_workday_of_month(cur.year, cur.month)
|
||||
if d <= end_date:
|
||||
out.append(format_datetime_for_output(d, time_hhmmss))
|
||||
|
||||
elif monthly_option == 5:
|
||||
# ↪ Relativer Wochentag: Ziel-Wochentag nach n-tem Referenz-Wochentag + Wochen-Offset
|
||||
if week_in_month is None or weekday_for_monthly is None or target_weekday_for_monthly is None:
|
||||
raise ValueError("Für 'Termin abhängig von anderem Datum' bitte Vorkommen, Referenz- und Ziel-Wochentag angeben.")
|
||||
base_wk = (weekday_for_monthly - 1) % 7 # Referenz-Wochentag (0..6)
|
||||
target_wk = (target_weekday_for_monthly - 1) % 7
|
||||
ref = nth_weekday_in_month(cur.year, cur.month, week_in_month, base_wk)
|
||||
if ref is not None and ref <= end_date:
|
||||
# delta innerhalb derselben Woche + Wochen-Offset in ganzen Wochen
|
||||
delta_days = (target_wk - base_wk) % 7
|
||||
d = ref + timedelta(days=delta_days + 7 * relative_week_offset)
|
||||
if d.month == cur.month and d <= end_date:
|
||||
out.append(format_datetime_for_output(d, time_hhmmss))
|
||||
|
||||
else:
|
||||
raise ValueError("Bitte eine gültige Monats-Option wählen.")
|
||||
|
||||
# nächster Monat
|
||||
if cur.month == 12:
|
||||
cur = date(cur.year + 1, 1, 1)
|
||||
else:
|
||||
cur = date(cur.year, cur.month + 1, 1)
|
||||
|
||||
elif frequency == 'q':
|
||||
# Quartale: letzter Monat je Quartal = 3, 6, 9, 12
|
||||
quarters = [3, 6, 9, 12]
|
||||
|
||||
if quarterly_option is None or quarterly_option == 1:
|
||||
# 🏣 Letzter Kalendertag des Quartals (wie bisher)
|
||||
for m in quarters:
|
||||
days_in_month = calendar.monthrange(year, m)[1]
|
||||
d = date(year, m, days_in_month)
|
||||
out.append(format_datetime_for_output(d, time_hhmmss))
|
||||
elif quarterly_option == 2:
|
||||
# 📅 Fester Kalendertag im Quartalsmonat (z. B. 15.)
|
||||
if day_of_month is None:
|
||||
raise ValueError("Bitte den Kalendertag im Quartalsmonat angeben (1..31).")
|
||||
for m in quarters:
|
||||
days_in_month = calendar.monthrange(year, m)[1]
|
||||
day = min(day_of_month, days_in_month) # 31 in einem 30-Tage-Monat -> letzter Tag
|
||||
d = date(year, m, day)
|
||||
out.append(format_datetime_for_output(d, time_hhmmss))
|
||||
else:
|
||||
raise ValueError("Unbekannte Quartals-Option.")
|
||||
|
||||
else:
|
||||
raise ValueError("Frequenz unbekannt.")
|
||||
|
||||
return out
|
||||
|
||||
|
||||
# ---------- GUI ----------
|
||||
|
||||
class App(tk.Tk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.title(f"Termin-Generator – Change-Planung {VERSION}")
|
||||
self.geometry("980x780")
|
||||
|
||||
# ---------- Grund-Layout ----------
|
||||
|
||||
# Jahr
|
||||
ttk.Label(self, text="Jahr:").grid(row=0, column=0, padx=10, pady=8, sticky="e")
|
||||
self.year_var = tk.IntVar(value=datetime.now().year)
|
||||
self.year_spin = ttk.Spinbox(self, from_=1900, to=2100, textvariable=self.year_var, width=8)
|
||||
self.year_spin.grid(row=0, column=1, padx=10, pady=8, sticky="w")
|
||||
|
||||
# Frequenz (deutsche Bezeichnungen)
|
||||
ttk.Label(self, text="Wie oft soll der Termin stattfinden?").grid(row=1, column=0, padx=10, pady=8, sticky="e")
|
||||
self.frequency_var = tk.StringVar(value="📆 Monatlich")
|
||||
self.frequency_combo = ttk.Combobox(
|
||||
self,
|
||||
textvariable=self.frequency_var,
|
||||
values=[
|
||||
"🔁 Täglich",
|
||||
"📅 Wöchentlich",
|
||||
"📆 Monatlich",
|
||||
"🏣 Quartalsweise (letzter Kalendertag oder fester Tag)"
|
||||
],
|
||||
state='readonly',
|
||||
width=45
|
||||
)
|
||||
self.frequency_combo.grid(row=1, column=1, padx=10, pady=8, sticky="w")
|
||||
self.frequency_combo.bind('<<ComboboxSelected>>', self._toggle_by_frequency)
|
||||
|
||||
# Uhrzeit
|
||||
ttk.Label(self, text="⏱ Uhrzeit (HH:MM):").grid(row=2, column=0, padx=10, pady=8, sticky="e")
|
||||
self.time_var = tk.StringVar(value="00:00")
|
||||
self.time_entry = ttk.Entry(self, textvariable=self.time_var, width=8)
|
||||
self.time_entry.grid(row=2, column=1, padx=10, pady=8, sticky="w")
|
||||
ttk.Label(self, text="Beispiel: 07:00 oder 18:30").grid(row=2, column=1, padx=100, pady=8, sticky="w")
|
||||
|
||||
# ---------- Wöchentlich ----------
|
||||
|
||||
self.weekly_frame = ttk.LabelFrame(self, text="📅 Wöchentliche Einstellungen")
|
||||
self.weekly_weekday_label = ttk.Label(self.weekly_frame, text="Wochentag auswählen (1=Mo .. 7=So):")
|
||||
self.weekly_weekday_var = tk.IntVar(value=2)
|
||||
self.weekly_weekday_spin = ttk.Spinbox(self.weekly_frame, from_=1, to=7, textvariable=self.weekly_weekday_var, width=6)
|
||||
|
||||
self.weekly_iso_kw_label = ttk.Label(self.weekly_frame, text="Einschränkung nach Kalenderwochen:")
|
||||
self.weekly_iso_kw_var = tk.StringVar(value='Keine Einschränkung')
|
||||
self.weekly_iso_kw_combo = ttk.Combobox(
|
||||
self.weekly_frame,
|
||||
textvariable=self.weekly_iso_kw_var,
|
||||
values=[
|
||||
'Keine Einschränkung',
|
||||
'Nur in ungeraden Kalenderwochen',
|
||||
'Nur in geraden Kalenderwochen'
|
||||
],
|
||||
state='readonly',
|
||||
width=32
|
||||
)
|
||||
|
||||
self.weekly_hint = ttk.Label(
|
||||
self.weekly_frame,
|
||||
text="Hinweis: „Nur in ungeraden/geraden Kalenderwochen“ eignet sich für 14-tägige Rhythmen.",
|
||||
foreground="grey"
|
||||
)
|
||||
|
||||
# ---------- Monatlich ----------
|
||||
|
||||
self.monthly_frame = ttk.LabelFrame(self, text="📆 Monatliche Terminlogik")
|
||||
|
||||
# Options (Reihenfolge: einfach → komplex)
|
||||
self.monthly_option_var = tk.IntVar(value=2) # Default: Wiederkehrender Wochentag
|
||||
|
||||
self.rb_m_fixed = ttk.Radiobutton(
|
||||
self.monthly_frame,
|
||||
text="📅 Fester Kalendertag (z. B. jeden 15.)",
|
||||
variable=self.monthly_option_var,
|
||||
value=1,
|
||||
command=self._toggle_monthly
|
||||
)
|
||||
self.rb_m_nthwd = ttk.Radiobutton(
|
||||
self.monthly_frame,
|
||||
text="📆 Wiederkehrender Wochentag (z. B. 2. Mittwoch im Monat)",
|
||||
variable=self.monthly_option_var,
|
||||
value=2,
|
||||
command=self._toggle_monthly
|
||||
)
|
||||
self.rb_m_followingwd = ttk.Radiobutton(
|
||||
self.monthly_frame,
|
||||
text="➡ Kalendertag, ggf. verschoben auf nächsten Werktag (Mo–Fr)",
|
||||
variable=self.monthly_option_var,
|
||||
value=3,
|
||||
command=self._toggle_monthly
|
||||
)
|
||||
self.rb_m_firstwd = ttk.Radiobutton(
|
||||
self.monthly_frame,
|
||||
text="🏢 Erster Werktag des Monats (Mo–Fr)",
|
||||
variable=self.monthly_option_var,
|
||||
value=4,
|
||||
command=self._toggle_monthly
|
||||
)
|
||||
self.rb_m_relative = ttk.Radiobutton(
|
||||
self.monthly_frame,
|
||||
text="↪ Termin abhängig von anderem Datum (z. B. Mittwoch nach 2. Dienstag)",
|
||||
variable=self.monthly_option_var,
|
||||
value=5,
|
||||
command=self._toggle_monthly
|
||||
)
|
||||
|
||||
# Felder für Optionen 1 & 3 (Kalendertag / Kalendertag + Folgewerktag)
|
||||
self.day_of_month_label = ttk.Label(self.monthly_frame, text="Tag im Monat (1..31):")
|
||||
self.day_of_month_var = tk.IntVar(value=1)
|
||||
self.day_of_month_spin = ttk.Spinbox(self.monthly_frame, from_=1, to=31, textvariable=self.day_of_month_var, width=6)
|
||||
|
||||
# Felder für Option 2 & 5 (n-ter / relativer Wochentag)
|
||||
self.week_in_month_label = ttk.Label(self.monthly_frame, text="Vorkommen im Monat (1..5):")
|
||||
self.week_in_month_var = tk.IntVar(value=2)
|
||||
self.week_in_month_spin = ttk.Spinbox(self.monthly_frame, from_=1, to=5, textvariable=self.week_in_month_var, width=6)
|
||||
|
||||
self.monthly_weekday_label = ttk.Label(self.monthly_frame, text="Wochentag (1=Mo .. 7=So):")
|
||||
self.monthly_weekday_var = tk.IntVar(value=3)
|
||||
self.monthly_weekday_spin = ttk.Spinbox(self.monthly_frame, from_=1, to=7, textvariable=self.monthly_weekday_var, width=6)
|
||||
|
||||
self.monthly_target_weekday_label = ttk.Label(self.monthly_frame, text="Ziel-Wochentag (1=Mo .. 7=So):")
|
||||
self.monthly_target_weekday_var = tk.IntVar(value=3)
|
||||
self.monthly_target_weekday_spin = ttk.Spinbox(self.monthly_frame, from_=1, to=7, textvariable=self.monthly_target_weekday_var, width=6)
|
||||
|
||||
self.monthly_week_offset_label = ttk.Label(self.monthly_frame, text="Wochen-Offset (-4 .. +4):")
|
||||
self.monthly_week_offset_var = tk.IntVar(value=0)
|
||||
self.monthly_week_offset_spin = ttk.Spinbox(self.monthly_frame, from_=-4, to=4, textvariable=self.monthly_week_offset_var, width=6)
|
||||
|
||||
# ISO-KW-Filter nur für Option 2
|
||||
self.iso_kw_filter_label = ttk.Label(self.monthly_frame, text="Einschränkung nach Kalenderwochen (optional):")
|
||||
self.iso_kw_filter_var = tk.StringVar(value='Keine Einschränkung')
|
||||
self.iso_kw_filter_combo = ttk.Combobox(
|
||||
self.monthly_frame,
|
||||
textvariable=self.iso_kw_filter_var,
|
||||
values=[
|
||||
'Keine Einschränkung',
|
||||
'Nur in ungeraden Kalenderwochen',
|
||||
'Nur in geraden Kalenderwochen'
|
||||
],
|
||||
state='readonly',
|
||||
width=32
|
||||
)
|
||||
|
||||
self.monthly_hint = ttk.Label(self.monthly_frame, text="", foreground="grey")
|
||||
|
||||
# ---------- Quartalsweise ----------
|
||||
|
||||
self.quarterly_frame = ttk.LabelFrame(self, text="🏣 Quartalsweise Einstellungen")
|
||||
|
||||
self.quarterly_option_var = tk.IntVar(value=1) # 1 = letzter Kalendertag, 2 = fester Tag
|
||||
self.rb_q_last = ttk.Radiobutton(
|
||||
self.quarterly_frame,
|
||||
text="🏣 Letzter Kalendertag des Quartals",
|
||||
variable=self.quarterly_option_var,
|
||||
value=1,
|
||||
command=self._toggle_quarterly
|
||||
)
|
||||
self.rb_q_fixed = ttk.Radiobutton(
|
||||
self.quarterly_frame,
|
||||
text="📅 Fester Kalendertag im Quartalsmonat (z. B. 15.)",
|
||||
variable=self.quarterly_option_var,
|
||||
value=2,
|
||||
command=self._toggle_quarterly
|
||||
)
|
||||
|
||||
self.quarter_day_label = ttk.Label(self.quarterly_frame, text="Kalendertag im Quartalsmonat (1..31):")
|
||||
self.quarter_day_var = tk.IntVar(value=15)
|
||||
self.quarter_day_spin = ttk.Spinbox(self.quarterly_frame, from_=1, to=31, textvariable=self.quarter_day_var, width=6)
|
||||
|
||||
self.quarter_hint = ttk.Label(
|
||||
self.quarterly_frame,
|
||||
text="Hinweis: Quartalsmonat = März, Juni, September, Dezember. "
|
||||
"Ist der Tag größer als die Monatslänge (z. B. 31.06.), wird der letzte Tag genommen.",
|
||||
foreground="grey",
|
||||
wraplength=750,
|
||||
justify="left"
|
||||
)
|
||||
|
||||
# ---------- Buttons & Ergebnis ----------
|
||||
|
||||
self.run_btn = ttk.Button(self, text="🧮 Berechnen & in Zwischenablage kopieren", command=self.on_run)
|
||||
self.clear_btn = ttk.Button(self, text="🧹 Liste leeren", command=self.on_clear)
|
||||
|
||||
# Regel-Vorschau
|
||||
self.preview_var = tk.StringVar(value="Regel-Vorschau: (noch nichts berechnet)")
|
||||
self.preview_label = ttk.Label(self, textvariable=self.preview_var, foreground="blue")
|
||||
|
||||
# Ergebnis
|
||||
ttk.Label(self, text="Ergebnis:").grid(row=8, column=0, padx=10, pady=(10, 0), sticky="nw")
|
||||
self.result_list = tk.Listbox(self, height=16, width=70)
|
||||
self.result_list.grid(row=8, column=1, padx=10, pady=(10, 0), sticky="nsew")
|
||||
|
||||
# Versionslabel
|
||||
self.version_label = ttk.Label(self, text=f"Version: {VERSION}", foreground="grey")
|
||||
self.version_label.grid(row=9, column=0, padx=10, pady=(4, 8), sticky="w")
|
||||
|
||||
# Layout-Konfiguration
|
||||
self.grid_columnconfigure(1, weight=1)
|
||||
self.grid_rowconfigure(8, weight=1)
|
||||
|
||||
# Frames platzieren
|
||||
self.weekly_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=8, sticky="ew")
|
||||
self.monthly_frame.grid(row=4, column=0, columnspan=2, padx=10, pady=8, sticky="ew")
|
||||
self.quarterly_frame.grid(row=5, column=0, columnspan=2, padx=10, pady=8, sticky="ew")
|
||||
|
||||
# Wöchentlich-Frame Inhalt
|
||||
self.weekly_weekday_label.grid(row=0, column=0, padx=10, pady=6, sticky="w")
|
||||
self.weekly_weekday_spin.grid(row=0, column=1, padx=10, pady=6, sticky="w")
|
||||
self.weekly_iso_kw_label.grid(row=1, column=0, padx=10, pady=6, sticky="w")
|
||||
self.weekly_iso_kw_combo.grid(row=1, column=1, padx=10, pady=6, sticky="w")
|
||||
self.weekly_hint.grid(row=2, column=0, columnspan=2, padx=10, pady=4, sticky="w")
|
||||
|
||||
# Monats-Frame Optionen
|
||||
self.rb_m_fixed.grid(row=0, column=0, columnspan=3, padx=10, pady=(8, 4), sticky="w")
|
||||
self.rb_m_nthwd.grid(row=1, column=0, columnspan=3, padx=10, pady=4, sticky="w")
|
||||
self.rb_m_followingwd.grid(row=2, column=0, columnspan=3, padx=10, pady=4, sticky="w")
|
||||
self.rb_m_firstwd.grid(row=3, column=0, columnspan=3, padx=10, pady=4, sticky="w")
|
||||
self.rb_m_relative.grid(row=4, column=0, columnspan=3, padx=10, pady=4, sticky="w")
|
||||
|
||||
self.monthly_hint.grid(row=9, column=0, columnspan=3, padx=10, pady=(4, 8), sticky="w")
|
||||
|
||||
# Quartals-Frame Inhalt
|
||||
self.rb_q_last.grid(row=0, column=0, columnspan=2, padx=10, pady=(8, 4), sticky="w")
|
||||
self.rb_q_fixed.grid(row=1, column=0, columnspan=2, padx=10, pady=4, sticky="w")
|
||||
self.quarter_hint.grid(row=3, column=0, columnspan=3, padx=10, pady=(4, 8), sticky="w")
|
||||
|
||||
# Buttons & Vorschau
|
||||
self.run_btn.grid(row=6, column=1, padx=10, pady=10, sticky="w")
|
||||
self.clear_btn.grid(row=6, column=1, padx=10, pady=10, sticky="e")
|
||||
self.preview_label.grid(row=7, column=0, columnspan=2, padx=10, pady=(4, 0), sticky="w")
|
||||
|
||||
# Initial aktiv/inaktiv setzen
|
||||
self._toggle_by_frequency(None)
|
||||
self._toggle_monthly()
|
||||
self._toggle_quarterly()
|
||||
|
||||
# ---------- Helper: Mapping Frequenz ----------
|
||||
|
||||
def _get_frequency_code(self) -> str:
|
||||
val = self.frequency_var.get()
|
||||
if val.startswith("🔁"):
|
||||
return 't'
|
||||
if val.startswith("📅"):
|
||||
return 'w'
|
||||
if val.startswith("📆"):
|
||||
return 'm'
|
||||
if val.startswith("🏣"):
|
||||
return 'q'
|
||||
return 't'
|
||||
|
||||
# ---------- UI-Toggles ----------
|
||||
|
||||
def _toggle_by_frequency(self, _evt):
|
||||
f = self._get_frequency_code()
|
||||
|
||||
if f == 'w':
|
||||
self.weekly_frame.grid()
|
||||
else:
|
||||
self.weekly_frame.grid_remove()
|
||||
|
||||
if f == 'm':
|
||||
self.monthly_frame.grid()
|
||||
else:
|
||||
self.monthly_frame.grid_remove()
|
||||
|
||||
if f == 'q':
|
||||
self.quarterly_frame.grid()
|
||||
else:
|
||||
self.quarterly_frame.grid_remove()
|
||||
|
||||
def _hide_monthly_fields(self):
|
||||
for w in (
|
||||
self.day_of_month_label, self.day_of_month_spin,
|
||||
self.week_in_month_label, self.week_in_month_spin,
|
||||
self.monthly_weekday_label, self.monthly_weekday_spin,
|
||||
self.monthly_target_weekday_label, self.monthly_target_weekday_spin,
|
||||
self.monthly_week_offset_label, self.monthly_week_offset_spin,
|
||||
self.iso_kw_filter_label, self.iso_kw_filter_combo
|
||||
):
|
||||
w.grid_remove()
|
||||
|
||||
def _toggle_monthly(self):
|
||||
self._hide_monthly_fields()
|
||||
opt = int(self.monthly_option_var.get())
|
||||
hint = ""
|
||||
|
||||
if opt == 1:
|
||||
# 📅 Fester Kalendertag
|
||||
self.day_of_month_label.config(text="Tag im Monat (1..31):")
|
||||
self.day_of_month_label.grid(row=5, column=0, padx=10, pady=6, sticky="w")
|
||||
self.day_of_month_spin.grid(row=5, column=1, padx=10, pady=6, sticky="w")
|
||||
hint = "Beispiel: jeden 15. im Monat."
|
||||
|
||||
elif opt == 2:
|
||||
# 📆 Wiederkehrender Wochentag
|
||||
self.week_in_month_label.config(text="Vorkommen im Monat (1..5):")
|
||||
self.monthly_weekday_label.config(text="Wochentag (1=Mo .. 7=So):")
|
||||
|
||||
self.week_in_month_label.grid(row=5, column=0, padx=10, pady=6, sticky="w")
|
||||
self.week_in_month_spin.grid(row=5, column=1, padx=10, pady=6, sticky="w")
|
||||
self.monthly_weekday_label.grid(row=6, column=0, padx=10, pady=6, sticky="w")
|
||||
self.monthly_weekday_spin.grid(row=6, column=1, padx=10, pady=6, sticky="w")
|
||||
self.iso_kw_filter_label.grid(row=7, column=0, padx=10, pady=6, sticky="w")
|
||||
self.iso_kw_filter_combo.grid(row=7, column=1, padx=10, pady=6, sticky="w")
|
||||
hint = "Beispiel: 2. Mittwoch im Monat. Optional auf ungerade/gerade Kalenderwochen einschränkbar."
|
||||
|
||||
elif opt == 3:
|
||||
# ➡ Kalendertag + Folgewerktag
|
||||
self.day_of_month_label.config(text="Kalendertag (1..31):")
|
||||
self.day_of_month_label.grid(row=5, column=0, padx=10, pady=6, sticky="w")
|
||||
self.day_of_month_spin.grid(row=5, column=1, padx=10, pady=6, sticky="w")
|
||||
hint = "Wenn der gewählte Tag auf Samstag/Sonntag fällt, wird automatisch der nächste Werktag verwendet."
|
||||
|
||||
elif opt == 4:
|
||||
# 🏢 Erster Werktag
|
||||
hint = "Erster Arbeitstag (Mo–Fr) des Monats. Fällt der 1. auf Sa/So, wird auf den folgenden Montag verschoben."
|
||||
|
||||
elif opt == 5:
|
||||
# ↪ Termin abhängig von anderem Datum
|
||||
self.week_in_month_label.config(text="Referenz: Vorkommen im Monat (1..5):")
|
||||
self.monthly_weekday_label.config(text="Referenz: Wochentag (1=Mo .. 7=So):")
|
||||
|
||||
self.week_in_month_label.grid(row=5, column=0, padx=10, pady=6, sticky="w")
|
||||
self.week_in_month_spin.grid(row=5, column=1, padx=10, pady=6, sticky="w")
|
||||
self.monthly_weekday_label.grid(row=6, column=0, padx=10, pady=6, sticky="w")
|
||||
self.monthly_weekday_spin.grid(row=6, column=1, padx=10, pady=6, sticky="w")
|
||||
self.monthly_target_weekday_label.grid(row=7, column=0, padx=10, pady=6, sticky="w")
|
||||
self.monthly_target_weekday_spin.grid(row=7, column=1, padx=10, pady=6, sticky="w")
|
||||
self.monthly_week_offset_label.grid(row=8, column=0, padx=10, pady=6, sticky="w")
|
||||
self.monthly_week_offset_spin.grid(row=8, column=1, padx=10, pady=6, sticky="w")
|
||||
hint = (
|
||||
"Beispiele: Mittwoch nach dem 2. Dienstag (Offset=0) "
|
||||
"oder Dienstag in der Woche nach dem 2. Dienstag (Offset=1)."
|
||||
)
|
||||
|
||||
self.monthly_hint.config(text=hint)
|
||||
|
||||
def _toggle_quarterly(self):
|
||||
opt = int(self.quarterly_option_var.get())
|
||||
# Standard: Spinner verstecken
|
||||
self.quarter_day_label.grid_remove()
|
||||
self.quarter_day_spin.grid_remove()
|
||||
|
||||
if opt == 2:
|
||||
# Fester Kalendertag im Quartalsmonat
|
||||
self.quarter_day_label.grid(row=2, column=0, padx=10, pady=6, sticky="w")
|
||||
self.quarter_day_spin.grid(row=2, column=1, padx=10, pady=6, sticky="w")
|
||||
|
||||
# ---------- Vorschau-Text ----------
|
||||
|
||||
def _build_rule_preview(self,
|
||||
year: int,
|
||||
frequency_code: str,
|
||||
time_str: str,
|
||||
monthly_option: int | None,
|
||||
day_of_month: int | None,
|
||||
week_in_month: int | None,
|
||||
weekday_for_monthly: int | None,
|
||||
target_weekday_for_monthly: int | None,
|
||||
relative_week_offset: int,
|
||||
weekly_weekday: int | None,
|
||||
weekly_iso_kw_parity: str | None,
|
||||
monthly_iso_parity: str | None,
|
||||
quarterly_option: int | None) -> str:
|
||||
time_short = time_str[:5] # HH:MM
|
||||
txt = f"Regel-Vorschau: "
|
||||
|
||||
if frequency_code == 't':
|
||||
txt += f"täglich um {time_short} im Jahr {year}"
|
||||
|
||||
elif frequency_code == 'w':
|
||||
if weekly_weekday is None:
|
||||
return "Regel-Vorschau: wöchentlich (Wochentag nicht gesetzt)"
|
||||
wname = weekday_name_de(weekly_weekday)
|
||||
txt += f"wöchentlich am {wname} um {time_short} im Jahr {year}"
|
||||
if weekly_iso_kw_parity == 'odd':
|
||||
txt += " (nur in ungeraden Kalenderwochen)"
|
||||
elif weekly_iso_kw_parity == 'even':
|
||||
txt += " (nur in geraden Kalenderwochen)"
|
||||
|
||||
elif frequency_code == 'm':
|
||||
if monthly_option == 1:
|
||||
if day_of_month is None:
|
||||
return "Regel-Vorschau: monatlich – fester Kalendertag (Tag nicht gesetzt)"
|
||||
txt += f"monatlich am {day_of_month}. um {time_short}"
|
||||
elif monthly_option == 2:
|
||||
if week_in_month is None or weekday_for_monthly is None:
|
||||
return "Regel-Vorschau: monatlich – wiederkehrender Wochentag (Angaben unvollständig)"
|
||||
wname = weekday_name_de(weekday_for_monthly)
|
||||
txt += f"monatlich am {ordinal_de(week_in_month)} {wname} um {time_short}"
|
||||
if monthly_iso_parity == 'odd':
|
||||
txt += " (nur in ungeraden Kalenderwochen)"
|
||||
elif monthly_iso_parity == 'even':
|
||||
txt += " (nur in geraden Kalenderwochen)"
|
||||
elif monthly_option == 3:
|
||||
if day_of_month is None:
|
||||
return "Regel-Vorschau: monatlich – Kalendertag mit Folgewerktag (Tag nicht gesetzt)"
|
||||
txt += (
|
||||
f"monatlich am {day_of_month}. bzw. verschoben auf den nächsten Werktag (Mo–Fr) "
|
||||
f"um {time_short}"
|
||||
)
|
||||
elif monthly_option == 4:
|
||||
txt += f"monatlich am ersten Werktag des Monats (Mo–Fr) um {time_short}"
|
||||
elif monthly_option == 5:
|
||||
if week_in_month is None or weekday_for_monthly is None or target_weekday_for_monthly is None:
|
||||
return "Regel-Vorschau: monatlich – Termin abhängig von anderem Datum (Angaben unvollständig)"
|
||||
ref_name = weekday_name_de(weekday_for_monthly)
|
||||
tgt_name = weekday_name_de(target_weekday_for_monthly)
|
||||
txt += (
|
||||
f"monatlich am {tgt_name} "
|
||||
f"in Bezug auf den {ordinal_de(week_in_month)} {ref_name} "
|
||||
f"um {time_short}"
|
||||
)
|
||||
if relative_week_offset == 1:
|
||||
txt += " (eine Woche nach dem Referenztermin)"
|
||||
elif relative_week_offset == -1:
|
||||
txt += " (eine Woche vor dem Referenztermin)"
|
||||
elif relative_week_offset > 1:
|
||||
txt += f" ({relative_week_offset} Wochen nach dem Referenztermin)"
|
||||
elif relative_week_offset < -1:
|
||||
txt += f" ({abs(relative_week_offset)} Wochen vor dem Referenztermin)"
|
||||
|
||||
elif frequency_code == 'q':
|
||||
if quarterly_option is None or quarterly_option == 1:
|
||||
txt += f"quartalsweise am letzten Kalendertag des Quartals um {time_short} im Jahr {year}"
|
||||
elif quarterly_option == 2:
|
||||
if day_of_month is None:
|
||||
return "Regel-Vorschau: quartalsweise – fester Kalendertag im Quartalsmonat (Tag nicht gesetzt)"
|
||||
txt += (
|
||||
f"quartalsweise am {day_of_month}. im Quartalsmonat "
|
||||
f"(März, Juni, September, Dezember) um {time_short} im Jahr {year}"
|
||||
)
|
||||
else:
|
||||
txt += "quartalsweise (unbekannte Option)"
|
||||
|
||||
else:
|
||||
txt += "unbekannte Regel."
|
||||
|
||||
return txt
|
||||
|
||||
# ---------- Aktionen ----------
|
||||
|
||||
def on_run(self):
|
||||
try:
|
||||
year = int(self.year_var.get())
|
||||
frequency_code = self._get_frequency_code()
|
||||
time_str = validate_time_format(self.time_var.get().strip())
|
||||
|
||||
# gemeinsame Variablen
|
||||
weekday_for_weekly = None
|
||||
weekly_iso_parity = None
|
||||
monthly_option = None
|
||||
dom = None
|
||||
wim = None
|
||||
weekday_for_monthly = None
|
||||
target_weekday_for_monthly = None
|
||||
rel_week_offset = 0
|
||||
monthly_iso_parity = None
|
||||
quarterly_option = None
|
||||
|
||||
# Wöchentlich
|
||||
if frequency_code == 'w':
|
||||
weekday_for_weekly = int(self.weekly_weekday_var.get())
|
||||
sel_w = self.weekly_iso_kw_var.get().strip().lower()
|
||||
if "ungeraden" in sel_w:
|
||||
weekly_iso_parity = 'odd'
|
||||
elif "geraden" in sel_w:
|
||||
weekly_iso_parity = 'even'
|
||||
else:
|
||||
weekly_iso_parity = None
|
||||
|
||||
# Monatlich
|
||||
if frequency_code == 'm':
|
||||
monthly_option = int(self.monthly_option_var.get())
|
||||
if monthly_option == 1:
|
||||
dom = int(self.day_of_month_var.get())
|
||||
elif monthly_option == 2:
|
||||
wim = int(self.week_in_month_var.get())
|
||||
weekday_for_monthly = int(self.monthly_weekday_var.get())
|
||||
sel_m = self.iso_kw_filter_var.get().strip().lower()
|
||||
if "ungeraden" in sel_m:
|
||||
monthly_iso_parity = 'odd'
|
||||
elif "geraden" in sel_m:
|
||||
monthly_iso_parity = 'even'
|
||||
else:
|
||||
monthly_iso_parity = None
|
||||
elif monthly_option == 3:
|
||||
dom = int(self.day_of_month_var.get())
|
||||
elif monthly_option == 4:
|
||||
# keine Zusatzfelder
|
||||
pass
|
||||
elif monthly_option == 5:
|
||||
wim = int(self.week_in_month_var.get())
|
||||
weekday_for_monthly = int(self.monthly_weekday_var.get())
|
||||
target_weekday_for_monthly = int(self.monthly_target_weekday_var.get())
|
||||
rel_week_offset = int(self.monthly_week_offset_var.get())
|
||||
|
||||
# Quartalsweise
|
||||
if frequency_code == 'q':
|
||||
quarterly_option = int(self.quarterly_option_var.get())
|
||||
if quarterly_option == 2:
|
||||
dom = int(self.quarter_day_var.get())
|
||||
|
||||
# Vorschau-Text bauen
|
||||
preview = self._build_rule_preview(
|
||||
year=year,
|
||||
frequency_code=frequency_code,
|
||||
time_str=time_str,
|
||||
monthly_option=monthly_option,
|
||||
day_of_month=dom,
|
||||
week_in_month=wim,
|
||||
weekday_for_monthly=weekday_for_monthly,
|
||||
target_weekday_for_monthly=target_weekday_for_monthly,
|
||||
relative_week_offset=rel_week_offset,
|
||||
weekly_weekday=weekday_for_weekly,
|
||||
weekly_iso_kw_parity=weekly_iso_parity,
|
||||
monthly_iso_parity=monthly_iso_parity,
|
||||
quarterly_option=quarterly_option
|
||||
)
|
||||
self.preview_var.set(preview)
|
||||
|
||||
# Termine generieren
|
||||
dates = generate_dates(
|
||||
year=year,
|
||||
frequency=frequency_code,
|
||||
time_hhmmss=time_str,
|
||||
weekday_for_weekly=weekday_for_weekly,
|
||||
weekly_iso_week_parity=weekly_iso_parity,
|
||||
monthly_option=monthly_option,
|
||||
day_of_month=dom,
|
||||
week_in_month=wim,
|
||||
weekday_for_monthly=weekday_for_monthly,
|
||||
target_weekday_for_monthly=target_weekday_for_monthly,
|
||||
relative_week_offset=rel_week_offset,
|
||||
monthly_iso_week_parity=monthly_iso_parity,
|
||||
quarterly_option=quarterly_option
|
||||
)
|
||||
|
||||
self.result_list.delete(0, tk.END)
|
||||
for d in dates:
|
||||
self.result_list.insert(tk.END, d)
|
||||
|
||||
if HAS_PYPERCLIP:
|
||||
pyperclip.copy("\n".join(dates))
|
||||
messagebox.showinfo("Fertig", f"{len(dates)} Einträge generiert und in die Zwischenablage kopiert.")
|
||||
else:
|
||||
messagebox.showinfo(
|
||||
"Fertig",
|
||||
f"{len(dates)} Einträge generiert.\n"
|
||||
f"(Hinweis: pyperclip nicht installiert – nichts kopiert.)"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Fehler", str(e))
|
||||
|
||||
def on_clear(self):
|
||||
self.result_list.delete(0, tk.END)
|
||||
self.preview_var.set("Regel-Vorschau: (noch nichts berechnet)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
App().mainloop()
|
||||
BIN
CP Tagesrechner Jahresplanung/tagesrechner_user-manual.docx
Normal file
BIN
CP Tagesrechner Jahresplanung/tagesrechner_user-manual.docx
Normal file
Binary file not shown.
Reference in New Issue
Block a user