add GUI with customtkinter and build to windows executable
This commit is contained in:
1 parent
34e9aa49d6
commit
6e9d865a9d
7 files changed
+757
No files matched your search
@@ -0,0 +1,622 @@
|
||||
# Build: build.bat ausfuehren (aus dem Projekt-Root).
|
||||
# Ergebnis: dist/AnwesenheitPDF/AnwesenheitPDF.exe + alle Abhaengigkeiten
|
||||
# Build-Artefakte: build/ (temporaer, nicht committen)
|
||||
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import customtkinter as ctk
|
||||
from tkinter import messagebox, filedialog
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from src.config_loader import load_config
|
||||
from src.excel_reader import (
|
||||
extract_attendance,
|
||||
extract_persons,
|
||||
extract_year_from_filename,
|
||||
)
|
||||
from src.pdf_filler import fill_pdf
|
||||
from src.validators import validate
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Module-level helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _resource_path(relative: str) -> Path:
|
||||
"""Absolute path to a bundled asset. Handles PyInstaller onefile mode."""
|
||||
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
||||
return Path(sys._MEIPASS) / relative
|
||||
return Path(__file__).parent / relative
|
||||
|
||||
|
||||
def _runtime_dir() -> Path:
|
||||
"""Root directory for runtime folders (input/, output/, template/)."""
|
||||
if getattr(sys, "frozen", False):
|
||||
return Path(sys.executable).parent
|
||||
return Path(__file__).parent
|
||||
|
||||
|
||||
def _scan_input_folder() -> Optional[Path]:
|
||||
"""Return the single .xlsx in input/, or None if 0 or >1 found."""
|
||||
folder = _runtime_dir() / "input"
|
||||
if not folder.exists():
|
||||
return None
|
||||
files = list(folder.glob("*.xlsx"))
|
||||
return files[0] if len(files) == 1 else None
|
||||
|
||||
|
||||
def _scan_template_folder() -> Optional[Path]:
|
||||
"""Return the single .pdf in template/, or None if 0 or >1 found."""
|
||||
folder = _runtime_dir() / "template"
|
||||
if not folder.exists():
|
||||
return None
|
||||
files = list(folder.glob("*.pdf"))
|
||||
return files[0] if len(files) == 1 else None
|
||||
|
||||
|
||||
def _count_output_pdfs(config: Dict[str, Any]) -> int:
|
||||
"""Count .pdf files in the configured output directory."""
|
||||
out_dir = _runtime_dir() / config.get("output_dir", "output")
|
||||
if not out_dir.exists():
|
||||
return 0
|
||||
return len(list(out_dir.glob("*.pdf")))
|
||||
|
||||
|
||||
def _build_month_keys(config: Dict[str, Any]) -> List[str]:
|
||||
"""Return deduplicated month keys from config (preserves insertion order)."""
|
||||
seen: set = set()
|
||||
keys: List[str] = []
|
||||
for k, v in config.get("month_mapping", {}).items():
|
||||
if v not in seen:
|
||||
seen.add(v)
|
||||
keys.append(k)
|
||||
return keys
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Startup: load config once
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ctk.set_appearance_mode("System")
|
||||
ctk.set_default_color_theme("blue")
|
||||
|
||||
try:
|
||||
_CONFIG: Dict[str, Any] = load_config()
|
||||
_CONFIG_ERROR: Optional[str] = None
|
||||
except Exception as _exc:
|
||||
_CONFIG = {}
|
||||
_CONFIG_ERROR = str(_exc)
|
||||
|
||||
_MONTH_KEYS: List[str] = _build_month_keys(_CONFIG)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Application
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class AnwesenheitApp(ctk.CTk):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.title("Anwesenheit PDF Generator")
|
||||
self.geometry("720x680")
|
||||
self.resizable(False, False)
|
||||
|
||||
icon_path = _resource_path("assets/icon.ico")
|
||||
if icon_path.exists():
|
||||
self.iconbitmap(str(icon_path))
|
||||
|
||||
# Internal state
|
||||
self._excel_path: Optional[Path] = None
|
||||
self._template_path: Optional[Path] = None
|
||||
self._out_dir: Optional[Path] = None
|
||||
|
||||
# Build UI sections
|
||||
self._build_input_section()
|
||||
self._build_button_section()
|
||||
self._build_preview_section()
|
||||
self._build_log_section()
|
||||
|
||||
# Auto-scan after event loop starts
|
||||
self.after(100, self._scan_and_validate)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# UI builders
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_input_section(self) -> None:
|
||||
frame = ctk.CTkFrame(self)
|
||||
frame.pack(fill="x", padx=16, pady=(16, 0))
|
||||
frame.columnconfigure(2, weight=1) # value column expands
|
||||
|
||||
BTN_W = 32
|
||||
|
||||
# Row 0: Excel file
|
||||
ctk.CTkLabel(frame, text="Excel-Datei:", width=110, anchor="w").grid(
|
||||
row=0, column=0, padx=(12, 4), pady=6, sticky="w"
|
||||
)
|
||||
ctk.CTkButton(
|
||||
frame, text="\U0001f4c2", width=BTN_W, height=28,
|
||||
command=self._browse_excel,
|
||||
).grid(row=0, column=1, padx=(0, 6), pady=6)
|
||||
self._lbl_excel = ctk.CTkLabel(frame, text="Suche\u2026", anchor="w")
|
||||
self._lbl_excel.grid(row=0, column=2, padx=(0, 12), pady=6, sticky="ew")
|
||||
|
||||
# Row 1: Template
|
||||
ctk.CTkLabel(frame, text="Template:", width=110, anchor="w").grid(
|
||||
row=1, column=0, padx=(12, 4), pady=6, sticky="w"
|
||||
)
|
||||
ctk.CTkButton(
|
||||
frame, text="\U0001f4c2", width=BTN_W, height=28,
|
||||
command=self._browse_template,
|
||||
).grid(row=1, column=1, padx=(0, 6), pady=6)
|
||||
self._lbl_template = ctk.CTkLabel(frame, text="Suche\u2026", anchor="w")
|
||||
self._lbl_template.grid(row=1, column=2, padx=(0, 12), pady=6, sticky="ew")
|
||||
|
||||
# Row 2: Month dropdown
|
||||
ctk.CTkLabel(frame, text="Monat:", width=110, anchor="w").grid(
|
||||
row=2, column=0, padx=(12, 4), pady=6, sticky="w"
|
||||
)
|
||||
self._month_var = ctk.StringVar(value=_MONTH_KEYS[0] if _MONTH_KEYS else "")
|
||||
self._month_menu = ctk.CTkOptionMenu(
|
||||
frame,
|
||||
values=_MONTH_KEYS or ["\u2013"],
|
||||
variable=self._month_var,
|
||||
command=lambda _: self._on_month_changed(),
|
||||
width=150,
|
||||
)
|
||||
self._month_menu.grid(row=2, column=2, padx=(0, 12), pady=6, sticky="w")
|
||||
|
||||
# Row 3: Output dir
|
||||
out_label = str((_runtime_dir() / _CONFIG.get("output_dir", "output")).resolve())
|
||||
ctk.CTkLabel(frame, text="Ausgabe:", width=110, anchor="w").grid(
|
||||
row=3, column=0, padx=(12, 4), pady=6, sticky="w"
|
||||
)
|
||||
ctk.CTkButton(
|
||||
frame, text="\U0001f4c2", width=BTN_W, height=28,
|
||||
command=self._open_output_folder,
|
||||
).grid(row=3, column=1, padx=(0, 6), pady=6)
|
||||
ctk.CTkLabel(frame, text=out_label, anchor="w").grid(
|
||||
row=3, column=2, padx=(0, 12), pady=6, sticky="ew"
|
||||
)
|
||||
|
||||
# Row 4: Output warning + "Ordner leeren" link (hidden when 0)
|
||||
warning_row = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
warning_row.grid(row=4, column=2, padx=(0, 12), pady=(0, 6), sticky="ew")
|
||||
self._lbl_warning = ctk.CTkLabel(
|
||||
warning_row, text="", anchor="w", text_color="orange"
|
||||
)
|
||||
self._lbl_warning.pack(side="left")
|
||||
self._lbl_clear_link = ctk.CTkLabel(
|
||||
warning_row, text="", anchor="w",
|
||||
text_color="#EF4444", cursor="hand2",
|
||||
)
|
||||
self._lbl_clear_link.pack(side="left", padx=(6, 0))
|
||||
self._lbl_clear_link.bind("<Button-1>", lambda _: self._confirm_clear_output())
|
||||
|
||||
def _build_button_section(self) -> None:
|
||||
frame = ctk.CTkFrame(self)
|
||||
frame.pack(fill="x", padx=16, pady=(8, 0))
|
||||
self._btn_generate = ctk.CTkButton(
|
||||
frame,
|
||||
text="Generieren",
|
||||
state="disabled",
|
||||
command=self._start_generate,
|
||||
width=180,
|
||||
height=40,
|
||||
)
|
||||
self._btn_generate.pack(pady=12)
|
||||
|
||||
def _build_preview_section(self) -> None:
|
||||
frame = ctk.CTkFrame(self)
|
||||
frame.pack(fill="x", padx=16, pady=(8, 0))
|
||||
|
||||
ctk.CTkLabel(frame, text="Vorschau", font=ctk.CTkFont(weight="bold")).grid(
|
||||
row=0, column=0, columnspan=4, padx=12, pady=(8, 2), sticky="w"
|
||||
)
|
||||
|
||||
fields = [
|
||||
("Datei:", "_pv_datei"),
|
||||
("Jahr:", "_pv_jahr"),
|
||||
("Monat:", "_pv_monat"),
|
||||
("Antragsnummer:", "_pv_antrag"),
|
||||
("Ort:", "_pv_ort"),
|
||||
("Erstelldatum:", "_pv_datum"),
|
||||
("Teilnehmer:", "_pv_teilnehmer"),
|
||||
("Fehler:", "_pv_fehler"),
|
||||
]
|
||||
|
||||
for i, (text, attr) in enumerate(fields):
|
||||
row = 1 + i // 2
|
||||
col_lbl = (i % 2) * 2
|
||||
col_val = col_lbl + 1
|
||||
ctk.CTkLabel(frame, text=text, width=110, anchor="w").grid(
|
||||
row=row, column=col_lbl, padx=(12, 2), pady=3, sticky="w"
|
||||
)
|
||||
lbl = ctk.CTkLabel(frame, text="–", anchor="w", width=220)
|
||||
lbl.grid(row=row, column=col_val, padx=(0, 8), pady=3, sticky="w")
|
||||
setattr(self, attr, lbl)
|
||||
|
||||
def _build_log_section(self) -> None:
|
||||
frame = ctk.CTkFrame(self)
|
||||
frame.pack(fill="both", expand=True, padx=16, pady=(8, 16))
|
||||
frame.rowconfigure(1, weight=1)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
|
||||
ctk.CTkLabel(frame, text="Log", font=ctk.CTkFont(weight="bold")).grid(
|
||||
row=0, column=0, padx=12, pady=(8, 2), sticky="w"
|
||||
)
|
||||
self._log_box = ctk.CTkTextbox(
|
||||
frame,
|
||||
font=ctk.CTkFont(family="Courier New", size=11),
|
||||
state="disabled",
|
||||
)
|
||||
self._log_box.grid(row=1, column=0, padx=12, pady=(0, 12), sticky="nsew")
|
||||
# Configure clickable-link tag and bind events on underlying tk.Text
|
||||
tb = self._log_box._textbox
|
||||
tb.tag_config("pdf_link", foreground="#4FC3F7", underline=True)
|
||||
tb.bind("<Button-1>", self._on_log_click)
|
||||
tb.bind("<Motion>", self._on_log_motion)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Log helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _log(self, text: str) -> None:
|
||||
self._log_box.configure(state="normal")
|
||||
self._log_box.insert("end", text + "\n")
|
||||
self._log_box.see("end")
|
||||
self._log_box.configure(state="disabled")
|
||||
|
||||
def _log_ok(self, text: str) -> None:
|
||||
"""Insert an [OK] line styled as a clickable PDF link."""
|
||||
tb = self._log_box._textbox
|
||||
tb.configure(state="normal")
|
||||
tb.insert("end", text + "\n")
|
||||
insert_line = int(tb.index("end").split(".")[0]) - 1
|
||||
tb.tag_add("pdf_link", f"{insert_line}.0", f"{insert_line}.end")
|
||||
tb.see("end")
|
||||
tb.configure(state="disabled")
|
||||
|
||||
def _clear_log(self) -> None:
|
||||
self._log_box.configure(state="normal")
|
||||
self._log_box.delete("1.0", "end")
|
||||
self._log_box.configure(state="disabled")
|
||||
|
||||
def _on_log_click(self, event) -> None:
|
||||
tb = self._log_box._textbox
|
||||
idx = tb.index(f"@{event.x},{event.y}")
|
||||
line_start = tb.index(f"{idx} linestart")
|
||||
line_end = tb.index(f"{idx} lineend")
|
||||
line = tb.get(line_start, line_end).strip()
|
||||
if line.startswith("[OK]") and self._out_dir:
|
||||
# Format: "[OK] filename.pdf (i/total)"
|
||||
rest = line[4:].strip()
|
||||
filename = rest.split(" (")[0].strip()
|
||||
pdf_path = self._out_dir / filename
|
||||
if pdf_path.exists():
|
||||
os.startfile(str(pdf_path))
|
||||
|
||||
def _on_log_motion(self, event) -> None:
|
||||
tb = self._log_box._textbox
|
||||
idx = tb.index(f"@{event.x},{event.y}")
|
||||
line_start = tb.index(f"{idx} linestart")
|
||||
line_end = tb.index(f"{idx} lineend")
|
||||
line = tb.get(line_start, line_end).strip()
|
||||
tb.configure(cursor="hand2" if line.startswith("[OK]") else "")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Output folder
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _open_output_folder(self) -> None:
|
||||
out_dir = _runtime_dir() / _CONFIG.get("output_dir", "output")
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
os.startfile(str(out_dir))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Preview helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _clear_preview(self) -> None:
|
||||
for attr in (
|
||||
"_pv_datei", "_pv_jahr", "_pv_monat", "_pv_antrag",
|
||||
"_pv_ort", "_pv_datum", "_pv_teilnehmer", "_pv_fehler",
|
||||
):
|
||||
getattr(self, attr).configure(text="–", text_color=("gray10", "gray90"))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Output warning
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _refresh_output_warning(self) -> None:
|
||||
count = _count_output_pdfs(_CONFIG)
|
||||
if count > 0:
|
||||
self._lbl_warning.configure(
|
||||
text=f"⚠ {count} PDF-Datei(en) bereits in output/"
|
||||
)
|
||||
self._lbl_clear_link.configure(text="(Ordner leeren)")
|
||||
else:
|
||||
self._lbl_warning.configure(text="")
|
||||
self._lbl_clear_link.configure(text="")
|
||||
|
||||
def _confirm_clear_output(self) -> None:
|
||||
out_dir = _runtime_dir() / _CONFIG.get("output_dir", "output")
|
||||
count = _count_output_pdfs(_CONFIG)
|
||||
if count == 0:
|
||||
return
|
||||
confirmed = messagebox.askyesno(
|
||||
"Ordner leeren",
|
||||
f"{count} PDF-Datei(en) in output/ l\u00f6schen?\n\nDieser Vorgang kann nicht r\u00fcckg\u00e4ngig gemacht werden.",
|
||||
icon="warning",
|
||||
parent=self,
|
||||
)
|
||||
if confirmed:
|
||||
for f in out_dir.glob("*.pdf"):
|
||||
try:
|
||||
f.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
self._refresh_output_warning()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# File browse helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _browse_excel(self) -> None:
|
||||
path = filedialog.askopenfilename(
|
||||
title="Excel-Datei ausw\u00e4hlen",
|
||||
filetypes=[("Excel-Dateien", "*.xlsx"), ("Alle Dateien", "*.*")],
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
self._excel_path = Path(path)
|
||||
self._lbl_excel.configure(
|
||||
text=self._excel_path.name, text_color=("gray10", "gray90")
|
||||
)
|
||||
self._btn_generate.configure(state="disabled")
|
||||
self._clear_preview()
|
||||
self._clear_log()
|
||||
if self._template_path and self._month_var.get():
|
||||
threading.Thread(target=self._run_validate, daemon=True).start()
|
||||
|
||||
def _browse_template(self) -> None:
|
||||
path = filedialog.askopenfilename(
|
||||
title="Template-PDF ausw\u00e4hlen",
|
||||
filetypes=[("PDF-Dateien", "*.pdf"), ("Alle Dateien", "*.*")],
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
self._template_path = Path(path)
|
||||
self._lbl_template.configure(
|
||||
text=self._template_path.name, text_color=("gray10", "gray90")
|
||||
)
|
||||
self._btn_generate.configure(state="disabled")
|
||||
self._clear_preview()
|
||||
self._clear_log()
|
||||
if self._excel_path and self._month_var.get():
|
||||
threading.Thread(target=self._run_validate, daemon=True).start()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Event handlers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _on_month_changed(self) -> None:
|
||||
self._btn_generate.configure(state="disabled")
|
||||
self._clear_preview()
|
||||
self._clear_log()
|
||||
self._scan_and_validate()
|
||||
|
||||
def _scan_and_validate(self) -> None:
|
||||
if _CONFIG_ERROR:
|
||||
self._log(f"[FEHLER] config.json: {_CONFIG_ERROR}")
|
||||
return
|
||||
|
||||
# Scan input/
|
||||
self._excel_path = _scan_input_folder()
|
||||
input_folder = _runtime_dir() / "input"
|
||||
if self._excel_path:
|
||||
self._lbl_excel.configure(
|
||||
text=self._excel_path.name, text_color=("gray10", "gray90")
|
||||
)
|
||||
elif not input_folder.exists() or not list(input_folder.glob("*.xlsx")):
|
||||
self._lbl_excel.configure(
|
||||
text="Keine .xlsx in input/ gefunden", text_color="red"
|
||||
)
|
||||
else:
|
||||
n = len(list(input_folder.glob("*.xlsx")))
|
||||
self._lbl_excel.configure(
|
||||
text=f"Mehrere Dateien gefunden ({n}) — nur eine erlaubt",
|
||||
text_color="red",
|
||||
)
|
||||
|
||||
# Scan template/
|
||||
self._template_path = _scan_template_folder()
|
||||
template_folder = _runtime_dir() / "template"
|
||||
if self._template_path:
|
||||
self._lbl_template.configure(
|
||||
text=self._template_path.name, text_color=("gray10", "gray90")
|
||||
)
|
||||
elif not template_folder.exists() or not list(template_folder.glob("*.pdf")):
|
||||
self._lbl_template.configure(
|
||||
text="Keine .pdf in template/ gefunden", text_color="red"
|
||||
)
|
||||
else:
|
||||
n = len(list(template_folder.glob("*.pdf")))
|
||||
self._lbl_template.configure(
|
||||
text=f"Mehrere Dateien gefunden ({n}) — nur eine erlaubt",
|
||||
text_color="red",
|
||||
)
|
||||
|
||||
# Refresh output warning
|
||||
self._refresh_output_warning()
|
||||
|
||||
# Start validation thread if all inputs are ready
|
||||
if self._excel_path and self._template_path and self._month_var.get():
|
||||
threading.Thread(target=self._run_validate, daemon=True).start()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Validation (background thread)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _run_validate(self) -> None:
|
||||
excel_file = str(self._excel_path)
|
||||
month = self._month_var.get()
|
||||
result: Dict[str, Any] = {
|
||||
"errors": [],
|
||||
"year": 0,
|
||||
"persons": [],
|
||||
"month": month,
|
||||
"exception": None,
|
||||
}
|
||||
|
||||
try:
|
||||
result["errors"] = validate(excel_file, month)
|
||||
except Exception as exc:
|
||||
result["exception"] = str(exc)
|
||||
self.after(0, self._on_validate_done, result)
|
||||
return
|
||||
|
||||
try:
|
||||
result["year"] = extract_year_from_filename(excel_file, _CONFIG)
|
||||
except Exception as exc:
|
||||
result["errors"].append(f"Jahr nicht ermittelbar: {exc}")
|
||||
|
||||
try:
|
||||
result["persons"] = extract_persons(excel_file, month, _CONFIG)
|
||||
except Exception as exc:
|
||||
result["errors"].append(f"Teilnehmer nicht lesbar: {exc}")
|
||||
|
||||
self.after(0, self._on_validate_done, result)
|
||||
|
||||
def _on_validate_done(self, result: Dict[str, Any]) -> None:
|
||||
if result.get("exception"):
|
||||
self._log(f"[FEHLER] {result['exception']}")
|
||||
self._btn_generate.configure(state="disabled")
|
||||
return
|
||||
|
||||
errors: List[str] = result["errors"]
|
||||
year: int = result["year"]
|
||||
persons = result["persons"]
|
||||
month: str = result["month"]
|
||||
|
||||
month_num = (_CONFIG.get("month_mapping") or {}).get(month, "??")
|
||||
default_color = ("gray10", "gray90")
|
||||
|
||||
self._pv_datei.configure(
|
||||
text=self._excel_path.name if self._excel_path else "–",
|
||||
text_color=default_color,
|
||||
)
|
||||
self._pv_jahr.configure(
|
||||
text=str(year) if year else "–", text_color=default_color
|
||||
)
|
||||
self._pv_monat.configure(
|
||||
text=f"{month} ({month_num})", text_color=default_color
|
||||
)
|
||||
self._pv_antrag.configure(
|
||||
text=_CONFIG.get("antragsnummer", "–"), text_color=default_color
|
||||
)
|
||||
self._pv_ort.configure(
|
||||
text=_CONFIG.get("ort", "–"), text_color=default_color
|
||||
)
|
||||
self._pv_datum.configure(
|
||||
text=date.today().strftime("%d.%m.%Y"), text_color=default_color
|
||||
)
|
||||
self._pv_teilnehmer.configure(
|
||||
text=str(len(persons)), text_color=default_color
|
||||
)
|
||||
|
||||
if errors:
|
||||
self._pv_fehler.configure(text=str(len(errors)), text_color="red")
|
||||
self._clear_log()
|
||||
self._log("--- Validierungsfehler ---")
|
||||
for err in errors:
|
||||
self._log(f" [!] {err}")
|
||||
self._btn_generate.configure(state="disabled")
|
||||
else:
|
||||
self._pv_fehler.configure(text="0", text_color=default_color)
|
||||
self._clear_log()
|
||||
self._btn_generate.configure(state="normal")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Generation
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _start_generate(self) -> None:
|
||||
count = _count_output_pdfs(_CONFIG)
|
||||
if count > 0:
|
||||
answer = messagebox.askyesnocancel(
|
||||
"Ausgabe-Verzeichnis nicht leer",
|
||||
f"{count} PDF-Datei(en) bereits in output/ vorhanden.\n\n"
|
||||
"Inhalt vor der Generierung löschen?",
|
||||
parent=self,
|
||||
)
|
||||
if answer is None: # Cancel
|
||||
return
|
||||
if answer: # Yes → delete existing PDFs
|
||||
out_dir = _runtime_dir() / _CONFIG.get("output_dir", "output")
|
||||
for f in out_dir.glob("*.pdf"):
|
||||
try:
|
||||
f.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self._btn_generate.configure(state="disabled")
|
||||
self._clear_log()
|
||||
threading.Thread(target=self._run_generate, daemon=True).start()
|
||||
|
||||
def _run_generate(self) -> None:
|
||||
excel_file = str(self._excel_path)
|
||||
month = self._month_var.get()
|
||||
success_count = 0
|
||||
total = 0
|
||||
|
||||
try:
|
||||
year = extract_year_from_filename(excel_file, _CONFIG)
|
||||
persons = extract_persons(excel_file, month, _CONFIG)
|
||||
total = len(persons)
|
||||
|
||||
out_dir = _runtime_dir() / _CONFIG.get("output_dir", "output")
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._out_dir = out_dir
|
||||
|
||||
for i, person in enumerate(persons, 1):
|
||||
try:
|
||||
attendance = extract_attendance(
|
||||
excel_file, month, person, year, _CONFIG
|
||||
)
|
||||
output_filename = f"{person.filename_safe_name}.pdf"
|
||||
output_path = out_dir / output_filename
|
||||
fill_pdf(attendance, self._template_path, output_path, _CONFIG)
|
||||
self.after(0, self._log_ok, f"[OK] {output_filename} ({i}/{total})")
|
||||
success_count += 1
|
||||
except Exception as exc:
|
||||
self.after(0, self._log, f"[FEHLER] {person.name}: {exc}")
|
||||
|
||||
except Exception as exc:
|
||||
self.after(0, self._log, f"[FEHLER] {exc}")
|
||||
self.after(0, self._on_generate_done, 0, 0)
|
||||
return
|
||||
|
||||
self.after(0, self._on_generate_done, success_count, total)
|
||||
|
||||
def _on_generate_done(self, success: int, total: int) -> None:
|
||||
self._log(f"\nFertig: {success}/{total} PDFs erzeugt.")
|
||||
self._btn_generate.configure(state="normal")
|
||||
self._refresh_output_warning()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = AnwesenheitApp()
|
||||
app.mainloop()
|
||||
Reference in new issue
Block a user