diff --git a/CP Tagesrechner Jahresplanung/tagerechner_win_v0.12.py b/CP Tagesrechner Jahresplanung/tagerechner_win_v0.12.py deleted file mode 100644 index 11fb5d6..0000000 --- a/CP Tagesrechner Jahresplanung/tagerechner_win_v0.12.py +++ /dev/null @@ -1,454 +0,0 @@ -# tagerechner_win_v0_13.py -# Zweck: Termine (täglich / wöchentlich / monatlich / quartalsweise) generieren -# -# v0.13 – Bugfix: Feiertags-Logik greift jetzt GLOBAL -# - Fehler behoben, bei dem wöchentliche Termine trotz Checkbox auf Feiertage fielen. -# - Wenn "Feiertage beachten" aktiv ist, werden AUCH feste Termine (z.B. "Jeden Freitag") -# auf den nächsten Werktag verschoben, falls sie auf einen Feiertag fallen. -# -# v0.12 – GUI Overhaul (2 Spalten) - -VERSION = "v0.12 (Beta)" - -import re -import calendar -from datetime import datetime, timedelta, date -import tkinter as tk -from tkinter import ttk - -try: - import pyperclip - HAS_PYPERCLIP = True -except Exception: - HAS_PYPERCLIP = False - - -# ---------- Hilfsfunktionen: Feiertage ---------- - -def calculate_easter_sunday(year: int) -> date: - a = year % 19 - b = year // 100 - c = year % 100 - d = b // 4 - e = b % 4 - f = (b + 8) // 25 - g = (b - f + 1) // 3 - h = (19 * a + b - d - g + 15) % 30 - i = c // 4 - k = c % 4 - l = (32 + 2 * e + 2 * i - h - k) % 7 - m = (a + 11 * h + 22 * l) // 451 - month = (h + l - 7 * m + 114) // 31 - day = ((h + l - 7 * m + 114) % 31) + 1 - return date(year, month, day) - -def is_german_holiday(d: date) -> bool: - year = d.year - fixed_holidays = [(1, 1), (5, 1), (10, 3), (12, 25), (12, 26)] - if (d.month, d.day) in fixed_holidays: - return True - - easter_sun = calculate_easter_sunday(year) - # Karfreitag (-2), Ostermontag (+1), Himmelfahrt (+39), Pfingstmontag (+50) - offsets = [-2, 1, 39, 50] - # offsets.append(60) # Optional: Fronleichnam - - for off in offsets: - if d == easter_sun + timedelta(days=off): - return True - return False - -# ---------- Hilfsfunktionen: Format & Logik ---------- - -def validate_time_format(time_str: str) -> str: - 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.") - return f"{h:02d}:{m:02d}:00" - -def nth_weekday_in_month(year: int, month: int, n: int, weekday0: int): - first_weekday, days_in_month = calendar.monthrange(year, month) - 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_full_output(d: date, time_hhmmss: str) -> str: - w_names = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] - return f"{w_names[d.weekday()]}. {d.strftime('%d.%m.%Y')} {time_hhmmss}" - -def format_clean_output(d: date, time_hhmmss: str) -> str: - return f"{d.strftime('%d.%m.%Y')} {time_hhmmss}" - -def iso_week_number(d: date) -> int: - iso = d.isocalendar() - return getattr(iso, "week", iso[1]) - -def week_parity_ok(d: date, parity: str | None) -> bool: - if parity is None: return True - w = iso_week_number(d) - return (w % 2) == 1 if parity == 'odd' else (w % 2) == 0 - -def next_workday(d: date, check_holidays: bool) -> date: - """Verschiebt Datum auf den nächsten Werktag (Mo-Fr), der kein Feiertag ist.""" - while d.weekday() >= 5 or (check_holidays and is_german_holiday(d)): - d += timedelta(days=1) - return d - -def ensure_valid_date(d: date, check_holidays: bool) -> date: - """ - Wrapper: Prüft, ob d zulässig ist. - Wenn check_holidays=True und d ein Feiertag ist -> Verschieben auf nächsten Werktag. - (Wochenenden bleiben bei festen Terminen oft erhalten, außer es ist explizit next_workday gewünscht. - Aber Feiertage sollten bei 'check_holidays' verschoben werden.) - """ - if check_holidays and is_german_holiday(d): - return next_workday(d, check_holidays=True) - return d - -# ---------- Kernlogik ---------- - -def generate_dates(year: int, frequency: str, time_hhmmss: str, - check_holidays: bool, - weekday_for_weekly=None, weekly_iso_week_parity=None, - monthly_option=None, day_of_month=None, week_in_month=None, - weekday_for_monthly=None, target_weekday_for_monthly=None, - relative_week_offset=0, monthly_iso_week_parity=None, - quarterly_option=None) -> list[tuple]: - - start_date = date(year, 1, 1) - end_date = date(year, 12, 31) - raw_dates = [] - - if frequency == 't': - cur = start_date - while cur <= end_date: - # Auch täglich sollte an Feiertagen springen, wenn gewünscht? - # Interpretation: Täglich ist täglich. Aber wenn check_holidays an ist, skippen wir Feiertage? - # Konsistenter: Wir verschieben nicht (sonst Doppelung), wir skippen bei 't'? - # Change Management Logik: Tägliche Meetings fallen an Feiertagen aus. - if not (check_holidays and is_german_holiday(cur)): - raw_dates.append(cur) - cur += timedelta(days=1) - - elif frequency == 'w': - wk = (weekday_for_weekly - 1) % 7 - delta = (wk - start_date.weekday()) % 7 - cur = start_date + timedelta(days=delta) - - # Initiale Paritäts-Suche - 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 - else: - step = 7 - - while cur <= end_date: - if week_parity_ok(cur, weekly_iso_week_parity): - # NEU: Hier prüfen wir auf Feiertag und verschieben ggf. - actual_date = ensure_valid_date(cur, check_holidays) - raw_dates.append(actual_date) - cur += timedelta(days=step) - - elif frequency == 'm': - cur = start_date.replace(day=1) - while cur <= end_date: - d = None - if monthly_option == 1: # Fester Tag - 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) - # NEU: Check - d = ensure_valid_date(d, check_holidays) - - elif monthly_option == 2: # Wiederkehrender Wochentag - wk2 = (weekday_for_monthly - 1) % 7 - d = nth_weekday_in_month(cur.year, cur.month, week_in_month, wk2) - if d and not week_parity_ok(d, monthly_iso_week_parity): d = None - # NEU: Check - if d: d = ensure_valid_date(d, check_holidays) - - elif monthly_option == 3: # Kalendertag -> Werktag (hatte Logik schon, aber sicherheitshalber) - 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, check_holidays) # Hier war es schon korrekt - if d.month != cur.month: d = None - - elif monthly_option == 4: # 1. Werktag - d = first_workday_of_month(cur.year, cur.month, check_holidays) # Hier war es schon korrekt - - elif monthly_option == 5: # Relativ - base_wk = (weekday_for_monthly - 1) % 7 - target_wk = (target_weekday_for_monthly - 1) % 7 - ref = nth_weekday_in_month(cur.year, cur.month, week_in_month, base_wk) - if ref: - delta_days = (target_wk - base_wk) % 7 - d = ref + timedelta(days=delta_days + 7 * relative_week_offset) - if d.month != cur.month: d = None - # NEU: Check - if d: d = ensure_valid_date(d, check_holidays) - - if d and d <= end_date: - raw_dates.append(d) - - if cur.month == 12: cur = date(cur.year + 1, 1, 1) - else: cur = date(cur.year, cur.month + 1, 1) - - elif frequency == 'q': - quarters = [3, 6, 9, 12] - for m in quarters: - days_in_month = calendar.monthrange(year, m)[1] - d = None - if quarterly_option == 1: - d = date(year, m, days_in_month) - elif quarterly_option == 2: - day = min(day_of_month, days_in_month) - d = date(year, m, day) - - # NEU: Auch Quartals-Termine verschieben bei Feiertag - if d: - d = ensure_valid_date(d, check_holidays) - raw_dates.append(d) - - # Formatierung - final_list = [] - # Sortieren zur Sicherheit, falls durch Verschiebungen was durcheinander gerät (z.B. Fr -> Mo, aber Mo Termin existiert) - raw_dates.sort() - - for d in raw_dates: - disp = format_full_output(d, time_hhmmss) - clip = format_clean_output(d, time_hhmmss) - final_list.append((disp, clip)) - - return final_list - -# ---------- GUI ---------- - -class App(tk.Tk): - def __init__(self): - super().__init__() - self.title(f"Termin-Generator – Change-Planung {VERSION}") - self.geometry("1050x650") - self.minsize(900, 600) - - self.left_frame = ttk.Frame(self, padding="10") - self.left_frame.grid(row=0, column=0, sticky="nsew") - - self.right_frame = ttk.LabelFrame(self, text="Ergebnis", padding="10") - self.right_frame.grid(row=0, column=1, sticky="nsew", padx=(0, 10), pady=10) - - self.grid_columnconfigure(1, weight=1) - self.grid_rowconfigure(0, weight=1) - - # LINKS - self.general_frame = ttk.LabelFrame(self.left_frame, text="Allgemeines", padding="10") - self.general_frame.pack(fill="x", pady=(0, 10)) - - ttk.Label(self.general_frame, text="Jahr:").grid(row=0, column=0, sticky="w", pady=5) - self.year_var = tk.IntVar(value=datetime.now().year) - ttk.Spinbox(self.general_frame, from_=2020, to=2030, textvariable=self.year_var, width=6).grid(row=0, column=1, sticky="w", padx=5) - - self.check_holidays_var = tk.BooleanVar(value=True) - ttk.Checkbutton(self.general_frame, text="🇩🇪 Feiertage beachten", variable=self.check_holidays_var).grid(row=0, column=2, sticky="w", padx=15) - - ttk.Label(self.general_frame, text="Uhrzeit:").grid(row=1, column=0, sticky="w", pady=5) - self.time_var = tk.StringVar(value="00:00") - ttk.Entry(self.general_frame, textvariable=self.time_var, width=8).grid(row=1, column=1, sticky="w", padx=5) - - ttk.Label(self.general_frame, text="Turnus:").grid(row=2, column=0, sticky="w", pady=5) - self.frequency_var = tk.StringVar(value="📆 Monatlich") - freq_cb = ttk.Combobox(self.general_frame, textvariable=self.frequency_var, state='readonly', width=35, - values=["🔁 Täglich", "📅 Wöchentlich", "📆 Monatlich", "🏣 Quartalsweise"]) - freq_cb.grid(row=2, column=1, columnspan=2, sticky="w", padx=5) - freq_cb.bind('<>', self._toggle_by_frequency) - - self.dynamic_container = ttk.Frame(self.left_frame) - self.dynamic_container.pack(fill="both", expand=True) - - # Weekly - self.weekly_frame = ttk.LabelFrame(self.dynamic_container, text="Wöchentliche Optionen", padding="10") - ttk.Label(self.weekly_frame, text="Wochentag:").grid(row=0, column=0, sticky="w", pady=5) - self.weekly_weekday_var = tk.IntVar(value=1) - self.wd_combo = ttk.Combobox(self.weekly_frame, textvariable=self.weekly_weekday_var, state="readonly", width=15, - values=["1 (Mo)", "2 (Di)", "3 (Mi)", "4 (Do)", "5 (Fr)", "6 (Sa)", "7 (So)"]) - self.wd_combo.current(0) - self.wd_combo.grid(row=0, column=1, sticky="w", padx=5) - - ttk.Label(self.weekly_frame, text="Filter:").grid(row=1, column=0, sticky="w", pady=5) - self.weekly_iso_kw_var = tk.StringVar(value='Keine Einschränkung') - ttk.Combobox(self.weekly_frame, textvariable=self.weekly_iso_kw_var, state="readonly", width=25, - values=['Keine Einschränkung', 'Nur ungerade KW', 'Nur gerade KW']).grid(row=1, column=1, sticky="w", padx=5) - - # Monthly - self.monthly_frame = ttk.LabelFrame(self.dynamic_container, text="Monatliche Optionen", padding="10") - self.monthly_option_var = tk.IntVar(value=2) - opts = [(1, "Fester Kalendertag"), (2, "Wiederkehrender Wochentag"), (3, "Kalendertag (Werktag-Verschiebung)"), (4, "Erster Werktag des Monats"), (5, "Relativ zu anderem Datum")] - for val, txt in opts: - ttk.Radiobutton(self.monthly_frame, text=txt, variable=self.monthly_option_var, value=val, command=self._toggle_monthly).pack(anchor="w", pady=2) - - self.monthly_params_frame = ttk.Frame(self.monthly_frame, padding=(0,10)) - self.monthly_params_frame.pack(fill="x") - self.lbl_dom = ttk.Label(self.monthly_params_frame, text="Tag (1-31):") - self.spin_dom = ttk.Spinbox(self.monthly_params_frame, from_=1, to=31, width=5) - self.lbl_wim = ttk.Label(self.monthly_params_frame, text="Vorkommen (1-5):") - self.spin_wim = ttk.Spinbox(self.monthly_params_frame, from_=1, to=5, width=5) - self.lbl_wd = ttk.Label(self.monthly_params_frame, text="Wochentag (1-7):") - self.spin_wd = ttk.Spinbox(self.monthly_params_frame, from_=1, to=7, width=5) - self.lbl_twd = ttk.Label(self.monthly_params_frame, text="Ziel-Tag (1-7):") - self.spin_twd = ttk.Spinbox(self.monthly_params_frame, from_=1, to=7, width=5) - self.lbl_off = ttk.Label(self.monthly_params_frame, text="Offset (Wochen):") - self.spin_off = ttk.Spinbox(self.monthly_params_frame, from_=-4, to=4, width=5) - self.spin_off.set(0) - self.lbl_kw = ttk.Label(self.monthly_params_frame, text="KW-Filter:") - self.cb_kw = ttk.Combobox(self.monthly_params_frame, values=['Kein', 'Ungerade', 'Gerade'], state="readonly", width=10) - self.cb_kw.current(0) - - # Quarterly - self.quarterly_frame = ttk.LabelFrame(self.dynamic_container, text="Quartals-Optionen", padding="10") - self.quarterly_option_var = tk.IntVar(value=1) - ttk.Radiobutton(self.quarterly_frame, text="Letzter Tag des Quartals", variable=self.quarterly_option_var, value=1, command=self._toggle_quarterly).pack(anchor="w") - ttk.Radiobutton(self.quarterly_frame, text="Fester Tag im Quartalsmonat", variable=self.quarterly_option_var, value=2, command=self._toggle_quarterly).pack(anchor="w") - self.quart_day_frame = ttk.Frame(self.quarterly_frame) - self.quart_day_frame.pack(fill="x", pady=5) - ttk.Label(self.quart_day_frame, text="Tag:").pack(side="left") - self.spin_q_dom = ttk.Spinbox(self.quart_day_frame, from_=1, to=31, width=5) - self.spin_q_dom.pack(side="left", padx=5) - self.spin_q_dom.set(15) - - self.preview_var = tk.StringVar(value="...") - ttk.Label(self.left_frame, textvariable=self.preview_var, foreground="blue", wraplength=300).pack(side="bottom", fill="x", pady=10) - - # RECHTS - btn_frame = ttk.Frame(self.right_frame) - btn_frame.pack(fill="x", pady=(0, 10)) - self.run_btn = ttk.Button(btn_frame, text="🚀 Berechnen & Kopieren", command=self.on_run) - self.run_btn.pack(side="left", fill="x", expand=True, padx=(0, 5)) - self.clear_btn = ttk.Button(btn_frame, text="🧹", width=3, command=self.on_clear) - self.clear_btn.pack(side="right") - - list_container = ttk.Frame(self.right_frame) - list_container.pack(fill="both", expand=True) - self.scrollbar = ttk.Scrollbar(list_container, orient="vertical") - self.result_list = tk.Listbox(list_container, yscrollcommand=self.scrollbar.set, font=("Consolas", 10)) - self.scrollbar.config(command=self.result_list.yview) - self.scrollbar.pack(side="right", fill="y") - self.result_list.pack(side="left", fill="both", expand=True) - - self.status_var = tk.StringVar(value="Bereit.") - self.status_bar = ttk.Label(self, textvariable=self.status_var, relief="sunken", anchor="w", padding=(5, 2)) - self.status_bar.grid(row=1, column=0, columnspan=2, sticky="ew") - - self._toggle_by_frequency(None) - self._toggle_monthly() - self._toggle_quarterly() - - def _get_frequency_code(self) -> str: - val = self.frequency_var.get() - if "Täglich" in val: return 't' - if "Wöchentlich" in val: return 'w' - if "Monatlich" in val: return 'm' - if "Quartals" in val: return 'q' - return 't' - - def _toggle_by_frequency(self, _evt): - f = self._get_frequency_code() - self.weekly_frame.pack_forget() - self.monthly_frame.pack_forget() - self.quarterly_frame.pack_forget() - if f == 'w': self.weekly_frame.pack(fill="x", pady=10) - elif f == 'm': self.monthly_frame.pack(fill="x", pady=10) - elif f == 'q': self.quarterly_frame.pack(fill="x", pady=10) - self._update_preview() - - def _clear_monthly_params(self): - for widget in self.monthly_params_frame.winfo_children(): widget.grid_forget() - - def _toggle_monthly(self): - self._clear_monthly_params() - opt = self.monthly_option_var.get() - if opt in (1, 3): - self.lbl_dom.grid(row=0, column=0, padx=5); self.spin_dom.grid(row=0, column=1, padx=5) - elif opt == 2: - self.lbl_wim.grid(row=0, column=0); self.spin_wim.grid(row=0, column=1) - self.lbl_wd.grid(row=0, column=2); self.spin_wd.grid(row=0, column=3) - self.lbl_kw.grid(row=1, column=0); self.cb_kw.grid(row=1, column=1) - elif opt == 5: - self.lbl_wim.grid(row=0, column=0); self.spin_wim.grid(row=0, column=1) - self.lbl_wd.grid(row=0, column=2); self.spin_wd.grid(row=0, column=3) - self.lbl_twd.grid(row=1, column=0); self.spin_twd.grid(row=1, column=1) - self.lbl_off.grid(row=1, column=2); self.spin_off.grid(row=1, column=3) - self._update_preview() - - def _toggle_quarterly(self): - if self.quarterly_option_var.get() == 2: - for child in self.quart_day_frame.winfo_children(): child.configure(state='normal') - else: - for child in self.quart_day_frame.winfo_children(): child.configure(state='disabled') - self._update_preview() - - def _update_preview(self): - self.preview_var.set(f"Modus: {self.frequency_var.get()}") - - def on_run(self): - try: - self.status_var.set("Berechne...") - self.update_idletasks() - - year = self.year_var.get() - freq = self._get_frequency_code() - time_s = validate_time_format(self.time_var.get()) - check_holidays = self.check_holidays_var.get() - - wd_weekly = int(self.wd_combo.get().split()[0]) - kw_par = None - if "ungerade" in self.weekly_iso_kw_var.get().lower(): kw_par = 'odd' - elif "gerade" in self.weekly_iso_kw_var.get().lower(): kw_par = 'even' - - m_opt = self.monthly_option_var.get() - dom = int(self.spin_dom.get() or 1) - wim = int(self.spin_wim.get() or 1) - wd_monthly = int(self.spin_wd.get() or 1) - twd = int(self.spin_twd.get() or 1) - off = int(self.spin_off.get() or 0) - m_kw_par = None - if "Ungerade" in self.cb_kw.get(): m_kw_par = 'odd' - elif "Gerade" in self.cb_kw.get(): m_kw_par = 'even' - - q_opt = self.quarterly_option_var.get() - q_dom = int(self.spin_q_dom.get() or 1) - - results = generate_dates( - year, freq, time_s, check_holidays, - weekday_for_weekly=wd_weekly, weekly_iso_week_parity=kw_par, - monthly_option=m_opt, day_of_month=dom, week_in_month=wim, - weekday_for_monthly=wd_monthly, target_weekday_for_monthly=twd, - relative_week_offset=off, monthly_iso_week_parity=m_kw_par, - quarterly_option=q_opt if freq == 'q' else None - ) - if freq == 'q' and q_opt == 2: - results = generate_dates(year, freq, time_s, check_holidays, quarterly_option=q_opt, day_of_month=q_dom) - - self.result_list.delete(0, tk.END) - clipboard_text = [] - for display_str, clip_str in results: - self.result_list.insert(tk.END, display_str) - clipboard_text.append(clip_str) - - if HAS_PYPERCLIP: - pyperclip.copy("\n".join(clipboard_text)) - self.status_var.set(f"✅ {len(results)} Termine generiert & kopiert!") - else: - self.status_var.set(f"✅ {len(results)} Termine generiert (Kein Clipboard-Modul).") - except Exception as e: - self.status_var.set(f"❌ Fehler: {e}") - - def on_clear(self): - self.result_list.delete(0, tk.END) - self.status_var.set("Liste geleert.") - -if __name__ == "__main__": - App().mainloop() \ No newline at end of file