# -*- coding: utf-8 -*- """ ZLS-Export Leitfaden für Loco-Soft GUI mit Schritt-Screenshots je Workflow, CSV-Header-Prüfung via JSON, Umbenennen und Kopieren auf den Desktop, danach zurück zum Start. Voraussetzungen (installieren in der venv): pip install customtkinter pillow Optional für EXE: pip install pyinstaller """ import os import sys import json import glob import shutil import datetime import traceback import tkinter.filedialog as fd import tkinter.messagebox as mb import customtkinter as ctk from PIL import Image, ImageTk APP_TITLE = "Das ist ein Leitfaden zu Exportieren der ZLS Liste an Loco-Soft" # Namen der Workflows -> Ordnernamen unter ./assets WORKFLOWS = [ ("Kundenadressen", "kundenadressen"), ("Fahrzeugdaten", "fahrzeugdaten"), ("Bestandsfahrzeuge", "bestandsfahrzeuge"), ("Reifenlager", "reifenlager"), ("Rechnungsablage", "rechnungsablage"), ] # Standard-Schema (falls assets/schema.json fehlt) # Du kannst das hier anpassen oder eine eigene schema.json in ./assets ablegen. DEFAULT_SCHEMA = { # Prüf-Logik: erstes zutreffendes Pattern gewinnt # "pattern": kann ein Teil des Dateinamens sein (case-insensitive) # "header": entweder String (exakte 1. Zeile) ODER Liste der Spalten (wird zu CSV-Header zusammengefügt) "rules": [ {"pattern": "kunden", "header": ["KundenNr", "Name", "Straße", "PLZ", "Ort", "Telefon", "E-Mail"]}, {"pattern": "fahrzeug", "header": ["FahrzeugID", "Marke", "Modell", "FIN", "EZ", "KM"]}, {"pattern": "bestand", "header": ["BestandID", "Marke", "Modell", "Status", "Standort"]}, {"pattern": "reifen", "header": ["KundenNr", "SatzNr", "Dimension", "Profil", "Lagerplatz"]}, {"pattern": "rechnung", "header": ["RechnungsNr", "Datum", "KundenNr", "Betrag", "Steuer", "Gesamt"]}, ], # Wenn True: Header-Vergleich ist case-insensitive und trimmt Leerzeichen "normalize": True, # CSV-Delimiter-Erwartung für Header-Zeile (nur für den Vergleich) "delimiter": "," } def resource_path(*relative): """Findet Pfade auch im PyInstaller-Bundle.""" if hasattr(sys, "_MEIPASS"): base = sys._MEIPASS # PyInstaller temp folder else: base = os.path.abspath(os.path.dirname(__file__)) return os.path.join(base, *relative) def load_schema(): path = resource_path("assets", "schema.json") if os.path.isfile(path): try: with open(path, "r", encoding="utf-8") as f: return json.load(f) except Exception: print("WARN: Konnte assets/schema.json nicht laden, verwende DEFAULT_SCHEMA.", file=sys.stderr) return DEFAULT_SCHEMA def list_screenshots(workflow_folder): # akzeptierte Bildformate exts = ("*.png", "*.jpg", "*.jpeg", "*.webp", "*.bmp") files = [] for e in exts: files.extend(glob.glob(os.path.join(workflow_folder, e))) files.sort() return files def format_timestamp(dt=None): dt = dt or datetime.datetime.now() # ddmmyyyy_hhmm return dt.strftime("%d%m%Y_%H%M") def normalize_header(text): # Entfernt BOM, trims, vereinheitlicht Leerzeichen t = text.replace("\ufeff", "").strip() # Einheitliche Kommata t = ",".join([p.strip() for p in t.split(",")]) return t def header_from_list(cols, delimiter=","): return delimiter.join([str(c).strip() for c in cols]) def match_rule(filename, rules): low = filename.lower() for rule in rules: if str(rule.get("pattern", "")).lower() in low: return rule return None def validate_csv_first_line(file_path, schema): """ Prüft die erste Zeile gegen das Schema. Rückgabe: (ok: bool, message: str) """ try: with open(file_path, "r", encoding="utf-8", errors="replace") as f: first_line = f.readline().rstrip("\n\r") except Exception as e: return False, f"Fehler beim Lesen: {e}" rules = schema.get("rules", []) delimiter = schema.get("delimiter", ",") normalize = bool(schema.get("normalize", True)) rule = match_rule(os.path.basename(file_path), rules) if not rule: return False, "Keine passende Regel (pattern) im Schema gefunden." expected = rule.get("header") if isinstance(expected, list): expected = header_from_list(expected, delimiter=delimiter) expected = expected or "" actual = first_line if normalize: expected = normalize_header(expected) actual = normalize_header(actual) ok = (actual == expected) msg = "Header OK" if ok else f"Header NICHT OK\nErwartet: {expected}\nGefunden: {actual}" return ok, msg def ensure_desktop_target(): desktop = os.path.join(os.path.expanduser("~"), "Desktop") target = os.path.join(desktop, "ZLS_Export") os.makedirs(target, exist_ok=True) return target class WorkflowViewer(ctk.CTkFrame): def __init__(self, master, title, folder, on_finished, *args, **kwargs): super().__init__(master, *args, **kwargs) self.master = master self.title = title self.folder = folder self.on_finished = on_finished self.schema = load_schema() self.images = [] self.image_paths = list_screenshots(self.folder) self.idx = 0 # UI self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(1, weight=1) self.lbl_title = ctk.CTkLabel(self, text=title, font=ctk.CTkFont(size=22, weight="bold")) self.lbl_title.grid(row=0, column=0, pady=(8, 4), padx=12) self.canvas = ctk.CTkLabel(self, text="", fg_color="transparent") self.canvas.grid(row=1, column=0, sticky="nsew", padx=12, pady=8) # Navigationsleiste nav = ctk.CTkFrame(self) nav.grid(row=2, column=0, sticky="ew", padx=12, pady=(0, 8)) nav.grid_columnconfigure((0, 1, 2, 3), weight=1) self.btn_prev = ctk.CTkButton(nav, text="◀ Zurück", command=self.prev_step) self.btn_prev.grid(row=0, column=0, padx=6, pady=6, sticky="w") self.lbl_pos = ctk.CTkLabel(nav, text="–/–") self.lbl_pos.grid(row=0, column=1, padx=6, pady=6) self.btn_next = ctk.CTkButton(nav, text="Weiter ▶", command=self.next_step) self.btn_next.grid(row=0, column=2, padx=6, pady=6, sticky="e") self.btn_abort = ctk.CTkButton(nav, text="Zum Start", fg_color="grey30", command=self.back_to_home) self.btn_abort.grid(row=0, column=3, padx=6, pady=6, sticky="e") # Bild-Klick (für "letzter Screenshot geklickt") self.canvas.bind("", self.on_image_click) self.load_images() self.update_view() def load_images(self): self.images = [] max_w = 1100 max_h = 650 if not self.image_paths: # Platzhalter placeholder = Image.new("RGB", (900, 500), color=(245, 245, 245)) self.images.append(ImageTk.PhotoImage(placeholder)) self.image_paths = ["(Kein Screenshot gefunden)"] else: for p in self.image_paths: try: img = Image.open(p) img.thumbnail((max_w, max_h), Image.LANCZOS) self.images.append(ImageTk.PhotoImage(img)) except Exception: # Platzhalter bei Fehler ph = Image.new("RGB", (900, 500), color=(245, 245, 245)) self.images.append(ImageTk.PhotoImage(ph)) def update_view(self): self.canvas.configure(image=self.images[self.idx]) pos = f"Schritt {self.idx+1} von {len(self.images)}" # Bildunterschrift als Tooltip-Ersatz (Dateiname) caption = os.path.basename(self.image_paths[self.idx]) self.lbl_pos.configure(text=f"{pos} — {caption}") self.btn_prev.configure(state=("normal" if self.idx > 0 else "disabled")) self.btn_next.configure(state=("normal" if self.idx < len(self.images)-1 else "disabled")) def prev_step(self): if self.idx > 0: self.idx -= 1 self.update_view() def next_step(self): if self.idx < len(self.images) - 1: self.idx += 1 self.update_view() def on_image_click(self, _event): # Wenn letzter Screenshot geklickt wird → Exportfrage if self.idx == len(self.images) - 1: go = mb.askyesno("Export starten?", "Möchtest du jetzt den Export prüfen, umbenennen und kopieren?") if go: self.run_export_flow() def run_export_flow(self): try: source_dir = fd.askdirectory(title="Ordner mit exportierten CSV-Dateien wählen") if not source_dir: return csv_files = sorted(glob.glob(os.path.join(source_dir, "*.csv"))) if not csv_files: mb.showerror("Keine CSVs gefunden", "Im gewählten Ordner wurden keine .csv-Dateien gefunden.") return schema = self.schema results = [] all_ok = True # Prüfung & Umbenennen timestamp = format_timestamp() renamed_paths = [] for f in csv_files: ok, msg = validate_csv_first_line(f, schema) results.append((os.path.basename(f), ok, msg)) if not ok: all_ok = False # Meldung zu den Prüfergebnissen summary_lines = [] for name, ok, msg in results: status = "✅ OK" if ok else "❌ FEHLER" summary_lines.append(f"{status} {name}\n{msg}") mb.showinfo("Prüfergebnis", "\n\n".join(summary_lines)) if not all_ok: proceed = mb.askyesno("Trotzdem fortfahren?", "Nicht alle Dateien sind OK. Möchtest du trotzdem mit Umbenennen und Kopieren fortfahren?") if not proceed: return # Umbenennen (im Quellordner) & Sammeln temp_paths = [] for f in csv_files: base = os.path.basename(f) new_name = f"{timestamp}_{base}" new_path = os.path.join(os.path.dirname(f), new_name) # Umbenennen nur, wenn noch nicht bereits geprefixt if not base.startswith(timestamp + "_"): os.rename(f, new_path) temp_paths.append(new_path) else: temp_paths.append(f) # war schon umbenannt renamed_paths = temp_paths # Zielordner auf dem Desktop target_dir = ensure_desktop_target() for p in renamed_paths: shutil.copy2(p, target_dir) mb.showinfo("Fertig", f"Dateien wurden nach:\n{target_dir}\nkopiert.") except Exception as e: traceback.print_exc() mb.showerror("Fehler", f"Es ist ein Fehler aufgetreten:\n{e}") finally: # Zurück zum Start self.on_finished() def back_to_home(self): self.on_finished() class App(ctk.CTk): def __init__(self): super().__init__() ctk.set_appearance_mode("system") # "light" / "dark" / "system" ctk.set_default_color_theme("blue") self.title("ZLS → Loco-Soft Leitfaden") self.geometry("1200x800") self.minsize(1000, 700) self.container = ctk.CTkFrame(self) self.container.pack(fill="both", expand=True) self.show_home() def clear(self): for w in self.container.winfo_children(): w.destroy() def show_home(self): self.clear() frame = ctk.CTkFrame(self.container) frame.pack(fill="both", expand=True, padx=16, pady=16) frame.grid_columnconfigure((0, 1, 2), weight=1) lbl = ctk.CTkLabel(frame, text=APP_TITLE, font=ctk.CTkFont(size=24, weight="bold")) lbl.grid(row=0, column=0, columnspan=3, pady=(8, 16)) sub = ctk.CTkLabel(frame, text="Bitte einen Bereich wählen, um den Schritt-für-Schritt-Leitfaden zu öffnen.") sub.grid(row=1, column=0, columnspan=3, pady=(0, 16)) # Buttons in einem Grid r, c = 2, 0 for display_name, folder in WORKFLOWS: btn = ctk.CTkButton( frame, height=64, text=display_name, command=lambda d=display_name, f=folder: self.open_workflow(d, f), ) btn.grid(row=r, column=c, padx=10, pady=10, sticky="ew") c += 1 if c > 2: c = 0 r += 1 info = ctk.CTkLabel( frame, text=("Hinweis:\n" "Lege deine Schritt-Screenshots in Unterordnern unter ./assets// ab.\n" "Optionale CSV-Header-Vorgaben in ./assets/schema.json."), justify="left", ) info.grid(row=r+1, column=0, columnspan=3, pady=(20, 0)) def open_workflow(self, display_name, folder_name): abs_folder = resource_path("assets", folder_name) if not os.path.isdir(abs_folder): os.makedirs(abs_folder, exist_ok=True) self.clear() viewer = WorkflowViewer( master=self.container, title=display_name, folder=abs_folder, on_finished=self.show_home ) viewer.pack(fill="both", expand=True, padx=8, pady=8) if __name__ == "__main__": app = App() app.mainloop()