Vscode Upload

master
Sebastian Serfling 2025-10-14 08:38:25 +02:00
parent d5d56edd5c
commit 04eeea5aef
2 changed files with 393 additions and 0 deletions

View File

@ -1,4 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
</project>

View File

@ -0,0 +1,390 @@
# -*- 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("<Button-1>", 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/<workflow>/ 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()