commit a2a9d7ac5ad604fd384eafd3fc92e1e53d56b29d Author: Sebastian Zell Date: Fri Jan 16 11:23:43 2026 +0000 Initial commit: CSV Processor diff --git a/.abacus.donotdelete b/.abacus.donotdelete new file mode 100644 index 0000000..feaecc3 --- /dev/null +++ b/.abacus.donotdelete @@ -0,0 +1 @@ +gAAAAABpah9sgktIo6WojeILJBTHfR6Y65YiGuG0qoAK_wkpSOJpY133w3Qhh_4zqQPY8sHOUAOL9HFXUWkBA7kBFFZAYMDIxETI-Af7l5LIyNiPhdZXzBI= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71570f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Virtual environments +venv/ +ENV/ +env/ +.venv/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Application specific +csv_processor_config/ + +# NICHT ignorieren: dist/ mit .deb-Datei +# !dist/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bc32fb2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Sebastian Zell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..469b5ad --- /dev/null +++ b/README.md @@ -0,0 +1,217 @@ +# CSV-Processor v2.0 + +Professionelles Werkzeug zur Verarbeitung und Konvertierung von CSV- und Excel-Dateien. + +## Features + +✅ **Import** von CSV, Excel (XLSX/XLS) +✅ **Export** als CSV, Excel (XLSX), OpenDocument (ODS) +✅ **Spaltennamen-Mapping** (z.B. "btr" → "Betrag") +✅ **Datenbereinigung** (leere Zeilen/Spalten entfernen) +✅ **Spaltenauswahl** (nur gewünschte Spalten exportieren) +✅ **Sortierung** nach beliebigen Spalten +✅ **Voreinstellungen** für wiederkehrende Aufgaben + +## Programmversionen + +### CLI-Version (Terminal) +- **Für wen:** Fortgeschrittene Benutzer, Automatisierung +- **Vorteile:** Schnell, scriptfähig, volle Kontrolle +- **Datei:** `csv_processor.py` + +### GUI-Version (Grafisch) +- **Für wen:** Alle Benutzer, besonders Einsteiger +- **Vorteile:** Intuitiv, visuelle Vorschau, einfache Bedienung +- **Datei:** `csv_processor_gui.py` + +## Systemanforderungen + +- Python 3.7 oder höher +- Tkinter (für GUI-Version, normalerweise in Python enthalten) +- Optional: openpyxl (für Excel-Support) +- Optional: odfpy (für ODS-Support) + +## Installation + +### Option 1: Debian-Paket (.deb) + +Für Debian/Ubuntu-basierte Systeme steht ein fertiges Paket bereit: + +```bash +sudo dpkg -i dist/csv-processor-debian.deb +``` + +Nach der Installation: +- CLI-Version: `csv-processor-cli` +- GUI-Version: `csv-processor-gui` oder über Anwendungsmenü + +### Option 2: Direkt mit Python ausführen + +```bash +# Abhängigkeiten installieren +pip install -r requirements.txt + +# CLI-Version starten +python csv_processor.py + +# GUI-Version starten +python csv_processor_gui.py +``` + +## Nutzung + +### CLI-Version + +```bash +python csv_processor.py +``` + +Sie sehen das Hauptmenü: +``` +╔════════════════════════════════════════════════════════════════╗ +║ CSV-PROCESSOR v2.0 - Dateiverarbeitung ║ +╚════════════════════════════════════════════════════════════════╝ + + 1. Neue Datei verarbeiten + 2. Voreinstellungen verwalten + 0. Beenden +``` + +**Schritt-für-Schritt:** +1. Quelldatei auswählen (CSV oder Excel) +2. CSV-Einstellungen konfigurieren (Encoding, Delimiter) +3. Optional: Mapping-Datei für Spaltennamen laden +4. Leere Zeilen/Spalten entfernen +5. Spalten auswählen +6. Optional: Sortierung, Kopf-/Fußzeile +7. Ausgabeformat wählen und speichern +8. Optional: Als Voreinstellung speichern + +### GUI-Version + +```bash +python csv_processor_gui.py +``` + +1. **Datei laden:** Klick auf [Durchsuchen] und Datei auswählen +2. **Einstellungen:** Checkboxen für Kopfzeile, leere Zeilen/Spalten +3. **Spalten:** Im Bereich "Spaltenauswahl" gewünschte Spalten markieren +4. **Vorschau:** Optional [Vorschau] klicken +5. **Speichern:** [Verarbeiten & Speichern] klicken + +### Spaltenauswahl (CLI) + +| Eingabe | Bedeutung | +|---------|-------------------------------| +| `q` | Fertig | +| `alle` | Alle auswählen | +| `keine` | Alle abwählen | +| `3` | Spalte 3 umschalten | +| `1,2,3` | Spalten 1,2,3 umschalten | +| `+5` | Spalte 5 ANwählen | +| `-5` | Spalte 5 ABwählen | +| `1-10` | Spalten 1-10 umschalten | +| `+1-10` | Spalten 1-10 ANwählen | +| `-1-10` | Spalten 1-10 ABwählen | + +## Mapping-Dateien + +Mapping-Dateien übersetzen technische Spaltennamen in lesbare Namen: + +```json +{ + "btr": "Betrag", + "dat": "Datum", + "kto": "Kontonummer", + "empf": "Empfänger" +} +``` + +Speichern Sie diese als `.json`-Datei und laden Sie sie im Programm. + +## Voreinstellungen + +Voreinstellungen speichern Ihre Konfiguration für wiederkehrende Aufgaben: +- Kopfzeile ja/nein +- Leere Zeilen/Spalten entfernen +- Mapping-Datei +- Spaltenauswahl +- Ausgabeformat +- CSV-Export-Einstellungen +- Sortierung + +Voreinstellungen werden im Verzeichnis `csv_processor_config/` gespeichert. + +## Build + +### Linux-Executable erstellen + +```bash +./build_linux.sh +``` + +Erstellt eine standalone-Executable in `dist/`. + +### Debian-Paket erstellen + +```bash +./build_debian.sh +``` + +Erstellt ein `.deb`-Paket für Debian/Ubuntu. + +### Windows-Executable erstellen + +```bash +./build_windows.bat +``` + +Oder unter Linux mit Wine: +```bash +./build_windows_wine.sh +``` + +## Projektstruktur + +``` +csv-processor/ +├── csv_processor.py # CLI-Version +├── csv_processor_gui.py # GUI-Version +├── requirements.txt # Python-Abhängigkeiten +├── README.md # Diese Datei +├── LICENSE # MIT-Lizenz +├── build_linux.sh # Build-Script Linux +├── build_debian.sh # Build-Script Debian-Paket +├── build_windows.bat # Build-Script Windows +└── dist/ # Fertige Pakete + └── csv-processor-debian.deb +``` + +## Tipps & Tricks + +### Große Dateien +- Bei >10.000 Zeilen: 5-30 Sekunden Verarbeitungszeit +- Bei >100.000 Zeilen: CLI-Version ist schneller +- Leere Spalten werden vor der Spaltenauswahl entfernt + +### CSV-Export für Excel +- Delimiter: Semikolon (;) für deutsche Excel-Versionen +- Quoting: Minimal + +### CSV-Export für Datenbanken +- Delimiter: Tab +- Quoting: Minimal +- Keine Kopf-/Fußzeile + +## Lizenz + +MIT License - siehe [LICENSE](LICENSE) + +## Autor + +Sebastian Zell + +--- + +**Version:** 2.0 +**Stand:** Januar 2026 diff --git a/build_debian.sh b/build_debian.sh new file mode 100755 index 0000000..7963ac0 --- /dev/null +++ b/build_debian.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# Build-Script für Debian-Paket (.deb) +# Erstellt ein installierbares Debian-Paket + +set -e + +PKG_NAME="csv-processor" +PKG_VERSION="2.0.0" +PKG_ARCH="amd64" +MAINTAINER="Sebastian Zell " +DESCRIPTION="Professionelles Werkzeug zur Verarbeitung von CSV- und Excel-Dateien" + +echo "========================================" +echo "CSV-Processor - Debian Package Build" +echo "========================================" + +# Prüfe ob PyInstaller installiert ist +if ! command -v pyinstaller &> /dev/null; then + echo "PyInstaller nicht gefunden. Installiere..." + pip install pyinstaller +fi + +# Build-Verzeichnis vorbereiten +echo "Bereite Build-Verzeichnis vor..." +BUILD_DIR="deb_build" +rm -rf "$BUILD_DIR" 2>/dev/null || true +mkdir -p "$BUILD_DIR/DEBIAN" +mkdir -p "$BUILD_DIR/usr/bin" +mkdir -p "$BUILD_DIR/usr/share/applications" +mkdir -p "$BUILD_DIR/usr/share/$PKG_NAME" + +# Executables erstellen +echo "" +echo "Erstelle Executables mit PyInstaller..." + +# CLI-Version +pyinstaller --onefile --name csv-processor-cli --clean csv_processor.py +cp dist/csv-processor-cli "$BUILD_DIR/usr/bin/" + +# GUI-Version +pyinstaller --onefile --name csv-processor-gui --clean csv_processor_gui.py +cp dist/csv-processor-gui "$BUILD_DIR/usr/bin/" + +# Rechte setzen +chmod 755 "$BUILD_DIR/usr/bin/csv-processor-cli" +chmod 755 "$BUILD_DIR/usr/bin/csv-processor-gui" + +# Desktop-Eintrag erstellen +cat > "$BUILD_DIR/usr/share/applications/csv-processor.desktop" << EOF +[Desktop Entry] +Name=CSV-Processor +Comment=Professionelle CSV/Excel-Verarbeitung +Exec=csv-processor-gui +Icon=accessories-text-editor +Terminal=false +Type=Application +Categories=Office;Utility; +EOF + +# DEBIAN/control erstellen +cat > "$BUILD_DIR/DEBIAN/control" << EOF +Package: $PKG_NAME +Version: $PKG_VERSION +Section: utils +Priority: optional +Architecture: $PKG_ARCH +Maintainer: $MAINTAINER +Description: $DESCRIPTION + CSV-Processor ist ein professionelles Werkzeug zur Verarbeitung + und Konvertierung von CSV- und Excel-Dateien. Es bietet + Spaltennamen-Mapping, Datenbereinigung, Spaltenauswahl und + Voreinstellungen für wiederkehrende Aufgaben. +EOF + +# Debian-Paket bauen +echo "" +echo "Erstelle Debian-Paket..." +dpkg-deb --build "$BUILD_DIR" "dist/${PKG_NAME}_${PKG_VERSION}_${PKG_ARCH}.deb" + +# Aufräumen +rm -rf "$BUILD_DIR" +rm -rf build/ +rm -f *.spec + +echo "" +echo "========================================" +echo "Build abgeschlossen!" +echo "========================================" +echo "" +echo "Erstelltes Paket:" +ls -la dist/*.deb +echo "" +echo "Installation mit:" +echo " sudo dpkg -i dist/${PKG_NAME}_${PKG_VERSION}_${PKG_ARCH}.deb" diff --git a/build_linux.sh b/build_linux.sh new file mode 100755 index 0000000..a16604d --- /dev/null +++ b/build_linux.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Build-Script für Linux-Executable +# Erstellt standalone-Executables mit PyInstaller + +set -e + +echo "========================================" +echo "CSV-Processor - Linux Build" +echo "========================================" + +# Prüfe ob PyInstaller installiert ist +if ! command -v pyinstaller &> /dev/null; then + echo "PyInstaller nicht gefunden. Installiere..." + pip install pyinstaller +fi + +# Verzeichnisse vorbereiten +echo "Bereite Build-Verzeichnis vor..." +rm -rf build/ dist/*.spec 2>/dev/null || true +mkdir -p dist + +# CLI-Version bauen +echo "" +echo "Baue CLI-Version..." +pyinstaller --onefile \ + --name csv-processor-cli \ + --clean \ + csv_processor.py + +# GUI-Version bauen +echo "" +echo "Baue GUI-Version..." +pyinstaller --onefile \ + --name csv-processor-gui \ + --windowed \ + --clean \ + csv_processor_gui.py + +echo "" +echo "========================================" +echo "Build abgeschlossen!" +echo "========================================" +echo "" +echo "Erstellte Dateien:" +ls -la dist/csv-processor-* +echo "" +echo "Ausführen mit:" +echo " ./dist/csv-processor-cli" +echo " ./dist/csv-processor-gui" diff --git a/build_windows.bat b/build_windows.bat new file mode 100644 index 0000000..161dcd6 --- /dev/null +++ b/build_windows.bat @@ -0,0 +1,48 @@ +@echo off +REM Build-Script für Windows-Executable +REM Erstellt standalone-Executables mit PyInstaller + +echo ======================================== +echo CSV-Processor - Windows Build +echo ======================================== + +REM Prüfe ob Python installiert ist +python --version >nul 2>&1 +if errorlevel 1 ( + echo Python nicht gefunden. Bitte installieren Sie Python 3.7+ + pause + exit /b 1 +) + +REM Prüfe/Installiere PyInstaller +pip show pyinstaller >nul 2>&1 +if errorlevel 1 ( + echo PyInstaller nicht gefunden. Installiere... + pip install pyinstaller +) + +REM Verzeichnisse vorbereiten +echo. +echo Bereite Build-Verzeichnis vor... +if exist build rmdir /s /q build +if not exist dist mkdir dist + +REM CLI-Version bauen +echo. +echo Baue CLI-Version... +pyinstaller --onefile --name csv-processor-cli --clean csv_processor.py + +REM GUI-Version bauen +echo. +echo Baue GUI-Version... +pyinstaller --onefile --name csv-processor-gui --windowed --clean csv_processor_gui.py + +echo. +echo ======================================== +echo Build abgeschlossen! +echo ======================================== +echo. +echo Erstellte Dateien befinden sich in: dist\ +dir dist\csv-processor-*.exe +echo. +pause diff --git a/csv_processor.py b/csv_processor.py new file mode 100644 index 0000000..2ca27d8 --- /dev/null +++ b/csv_processor.py @@ -0,0 +1,1546 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +CSV-Processor v2.0: Erweiterte Verarbeitung und Formatierung von CSV/Excel-Dateien +mit Voreinstellungsverwaltung und Multi-Format-Export +""" + +import csv +import json +import os +import sys +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional, Any, Tuple +import re + +# Optional: Externe Bibliotheken für Excel/ODT Support +try: + import openpyxl + from openpyxl.styles import Font, PatternFill + EXCEL_SUPPORT = True +except ImportError: + EXCEL_SUPPORT = False + +try: + from odf.opendocument import OpenDocumentSpreadsheet + from odf.table import Table, TableRow, TableCell + from odf.text import P + ODT_SUPPORT = True +except ImportError: + ODT_SUPPORT = False + + +class CSVProcessor: + def __init__(self): + self.config_dir = Path("csv_processor_config") + self.config_dir.mkdir(exist_ok=True) + self.current_config = {} + + def clear_screen(self): + """Bildschirm löschen für bessere Übersicht""" + os.system('cls' if os.name == 'nt' else 'clear') + + def print_header(self, text: str): + """Formatierte Überschrift ausgeben""" + print("\n" + "="*60) + print(f" {text}") + print("="*60 + "\n") + + def get_available_presets(self) -> List[str]: + """Liste aller verfügbaren Voreinstellungen""" + presets = [] + for file in self.config_dir.glob("*.json"): + presets.append(file.stem) + return presets + + def load_preset(self, preset_name: str) -> Dict: + """Voreinstellung laden""" + preset_file = self.config_dir / f"{preset_name}.json" + if preset_file.exists(): + with open(preset_file, 'r', encoding='utf-8') as f: + return json.load(f) + return {} + + def save_preset(self, preset_name: str, config: Dict): + """Voreinstellung speichern""" + preset_file = self.config_dir / f"{preset_name}.json" + with open(preset_file, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=2, ensure_ascii=False) + print(f"\n✓ Voreinstellung '{preset_name}' gespeichert.") + + def delete_preset(self, preset_name: str): + """Voreinstellung löschen""" + preset_file = self.config_dir / f"{preset_name}.json" + if preset_file.exists(): + preset_file.unlink() + print(f"\n✓ Voreinstellung '{preset_name}' gelöscht.") + else: + print(f"\n✗ Voreinstellung '{preset_name}' nicht gefunden.") + + def ask_yes_no(self, question: str, default: Optional[bool] = None) -> bool: + """Ja/Nein Frage stellen""" + if default is not None: + question += f" (Standard: {'Ja' if default else 'Nein'})" + + while True: + answer = input(f"{question} [j/n]: ").lower().strip() + if answer in ['j', 'ja', 'y', 'yes']: + return True + elif answer in ['n', 'nein', 'no']: + return False + elif answer == '' and default is not None: + return default + else: + print("Bitte antworten Sie mit 'j' für Ja oder 'n' für Nein.") + + def select_file(self, prompt: str, extension: str = "*") -> Optional[Path]: + """Datei auswählen""" + while True: + filename = input(f"{prompt}: ").strip() + if not filename: + return None + + path = Path(filename) + if path.exists(): + return path + else: + print(f"Datei '{filename}' nicht gefunden. Bitte erneut versuchen.") + if not self.ask_yes_no("Möchten Sie es erneut versuchen?", True): + return None + + def load_column_mappings(self, mapping_file: Optional[Path]) -> Dict[str, str]: + """Spaltennamen-Zuordnungen aus JSON laden""" + if not mapping_file or not mapping_file.exists(): + return {} + + try: + with open(mapping_file, 'r', encoding='utf-8') as f: + return json.load(f) + except json.JSONDecodeError: + print(f"Fehler beim Lesen der Mapping-Datei: {mapping_file}") + return {} + + def detect_csv_format(self, filepath: Path) -> Tuple[str, str, str]: + """CSV-Format automatisch erkennen""" + encodings = ['utf-8', 'latin-1', 'cp1252'] + + for encoding in encodings: + try: + with open(filepath, 'r', encoding=encoding) as f: + sample = f.read(2048) + + sniffer = csv.Sniffer() + dialect = sniffer.sniff(sample) + delimiter = dialect.delimiter + + if dialect.quoting == csv.QUOTE_ALL: + quoting = 'ALLE' + elif dialect.quoting == csv.QUOTE_MINIMAL: + quoting = 'MINIMAL' + elif dialect.quoting == csv.QUOTE_NONNUMERIC: + quoting = 'NICHT_NUMERISCH' + else: + quoting = 'KEINE' + + return encoding, delimiter, quoting + except: + continue + + return 'utf-8', ';', 'MINIMAL' + + def parse_csv_line(self, line: str, delimiter: str, quoting: str) -> List[str]: + """Eine CSV-Zeile mit gegebenen Einstellungen parsen""" + quoting_map = { + 'ALLE': csv.QUOTE_ALL, + 'MINIMAL': csv.QUOTE_MINIMAL, + 'NICHT_NUMERISCH': csv.QUOTE_NONNUMERIC, + 'KEINE': csv.QUOTE_NONE + } + + try: + reader = csv.reader([line], delimiter=delimiter, quoting=quoting_map.get(quoting, csv.QUOTE_MINIMAL)) + return next(reader) + except: + return line.split(delimiter) + + def configure_csv_import(self, filepath: Path) -> Tuple[str, str, str]: + """Interaktive CSV-Import-Konfiguration""" + detected_encoding, detected_delimiter, detected_quoting = self.detect_csv_format(filepath) + + delimiter_names = { + ';': 'Semikolon (;)', + ',': 'Komma (,)', + '\t': 'Tab', + '|': 'Pipe (|)', + ' ': 'Leerzeichen' + } + + with open(filepath, 'r', encoding=detected_encoding) as f: + test_lines = [f.readline().strip() for _ in range(3)] + + print("\n" + "="*70) + print(" CSV-IMPORT KONFIGURATION") + print("="*70) + print(f"\nDatei: {filepath.name}") + print(f"Erkanntes Encoding: {detected_encoding}") + print(f"Erkannter Delimiter: {delimiter_names.get(detected_delimiter, detected_delimiter)}") + print(f"Erkanntes Quoting: {detected_quoting}") + + current_encoding = detected_encoding + current_delimiter = detected_delimiter + current_quoting = detected_quoting + + while True: + print("\n" + "-"*70) + print(" VORSCHAU DER ERSTEN ZEILEN:") + print("-"*70) + + for i, line in enumerate(test_lines, 1): + if line: + parsed = self.parse_csv_line(line, current_delimiter, current_quoting) + print(f"\nZeile {i}:") + print(f" Rohtext: {line[:80]}...") + print(f" Geparst: {len(parsed)} Felder") + for j, field in enumerate(parsed[:5], 1): + print(f" Feld {j}: '{field}'") + if len(parsed) > 5: + print(f" ... und {len(parsed)-5} weitere Felder") + + print("\n" + "-"*70) + print(" AKTUELLE EINSTELLUNGEN:") + print("-"*70) + print(f" 1. Encoding: {current_encoding}") + print(f" 2. Delimiter: {delimiter_names.get(current_delimiter, current_delimiter)}") + print(f" 3. Quoting: {current_quoting}") + print("\n 0. ✓ Diese Einstellungen verwenden") + print("-"*70) + + choice = input("\nWas möchten Sie ändern? [0-3]: ").strip() + + if choice == '0': + break + elif choice == '1': + print("\nVerfügbare Encodings:") + encodings = ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1'] + for i, enc in enumerate(encodings, 1): + print(f" {i}. {enc}") + enc_choice = input("Ihre Wahl: ").strip() + try: + idx = int(enc_choice) - 1 + if 0 <= idx < len(encodings): + current_encoding = encodings[idx] + with open(filepath, 'r', encoding=current_encoding) as f: + test_lines = [f.readline().strip() for _ in range(3)] + except: + pass + + elif choice == '2': + print("\nVerfügbare Delimiters:") + print(" 1. Semikolon (;)") + print(" 2. Komma (,)") + print(" 3. Tab") + print(" 4. Pipe (|)") + print(" 5. Leerzeichen") + delim_choice = input("Ihre Wahl: ").strip() + delim_map = {'1': ';', '2': ',', '3': '\t', '4': '|', '5': ' '} + if delim_choice in delim_map: + current_delimiter = delim_map[delim_choice] + + elif choice == '3': + print("\nQuoting-Optionen:") + print(" 1. MINIMAL - Nur Felder mit Sonderzeichen werden quoted") + print(" 2. ALLE - Alle Felder werden in Anführungszeichen gesetzt") + print(" 3. NICHT_NUMERISCH - Alle nicht-numerischen Felder werden quoted") + print(" 4. KEINE - Keine Anführungszeichen") + quot_choice = input("Ihre Wahl: ").strip() + quot_map = {'1': 'MINIMAL', '2': 'ALLE', '3': 'NICHT_NUMERISCH', '4': 'KEINE'} + if quot_choice in quot_map: + current_quoting = quot_map[quot_choice] + + print("\n✓ Import-Einstellungen bestätigt") + return current_encoding, current_delimiter, current_quoting + + def read_csv(self, filepath: Path, has_header: Optional[bool] = None, + encoding: str = None, delimiter: str = None, quoting: str = None) -> Tuple[List[str], List[Dict], bool, Dict]: + """CSV-Datei lesen""" + if encoding is None or delimiter is None: + encoding, delimiter, quoting = self.configure_csv_import(filepath) + + quoting_map = { + 'ALLE': csv.QUOTE_ALL, + 'MINIMAL': csv.QUOTE_MINIMAL, + 'NICHT_NUMERISCH': csv.QUOTE_NONNUMERIC, + 'KEINE': csv.QUOTE_NONE + } + + csv_quoting = quoting_map.get(quoting, csv.QUOTE_MINIMAL) + + try: + with open(filepath, 'r', encoding=encoding) as f: + if has_header is None: + has_header = self.ask_yes_no("\nHat die CSV-Datei eine Kopfzeile mit Spaltennamen?", True) + + if has_header: + reader = csv.DictReader(f, delimiter=delimiter, quoting=csv_quoting) + headers = list(reader.fieldnames) + data = list(reader) + else: + reader = csv.reader(f, delimiter=delimiter, quoting=csv_quoting) + rows = list(reader) + if rows: + headers = [f"Spalte_{i+1}" for i in range(len(rows[0]))] + data = [] + for row in rows: + row_dict = {headers[i]: row[i] if i < len(row) else '' + for i in range(len(headers))} + data.append(row_dict) + else: + headers = [] + data = [] + + import_settings = { + 'encoding': encoding, + 'delimiter': delimiter, + 'quoting': quoting + } + + return headers, data, has_header, import_settings + except Exception as e: + print(f"Fehler beim Lesen der CSV: {e}") + raise + + def read_excel(self, filepath: Path) -> Tuple[Optional[List[str]], List[Dict], bool, Dict]: + """Excel-Datei lesen""" + if not EXCEL_SUPPORT: + print("Fehler: openpyxl ist nicht installiert. Installieren Sie es mit: pip install openpyxl") + return None, [], False, {} + + try: + wb = openpyxl.load_workbook(filepath, data_only=True) + + sheet_names = wb.sheetnames + if len(sheet_names) > 1: + print("\nVerfügbare Tabellenblätter:") + for i, name in enumerate(sheet_names, 1): + print(f" {i}. {name}") + + while True: + choice = input("Nummer des Tabellenblatts: ").strip() + try: + idx = int(choice) - 1 + if 0 <= idx < len(sheet_names): + ws = wb[sheet_names[idx]] + break + except ValueError: + pass + print("Ungültige Eingabe.") + else: + ws = wb.active + + has_header = self.ask_yes_no("Hat die Datei eine Kopfzeile mit Spaltennamen?", True) + + data = [] + headers = None + + for i, row in enumerate(ws.iter_rows(values_only=True)): + if i == 0 and has_header: + headers = [str(cell) if cell is not None else f"Spalte_{j+1}" + for j, cell in enumerate(row)] + else: + if headers is None: + headers = [f"Spalte_{j+1}" for j in range(len(row))] + + row_dict = {} + for j, cell in enumerate(row): + if j < len(headers): + row_dict[headers[j]] = str(cell) if cell is not None else '' + data.append(row_dict) + + import_settings = {} + + return headers, data, has_header, import_settings + + except Exception as e: + print(f"Fehler beim Lesen der Excel-Datei: {e}") + return None, [], False, {} + + def apply_column_mappings(self, headers: List[str], data: List[Dict], mappings: Dict[str, str]) -> Tuple[List[str], List[Dict], Dict[str, str]]: + """Spaltennamen umbenennen und Original-Namen behalten""" + new_headers = [mappings.get(h, h) for h in headers] + original_names = {new_h: old_h for old_h, new_h in zip(headers, new_headers)} + + new_data = [] + for row in data: + new_row = {} + for old_h, new_h in zip(headers, new_headers): + new_row[new_h] = row.get(old_h, '') + new_data.append(new_row) + + return new_headers, new_data, original_names + + def remove_empty_columns(self, headers: List[str], data: List[Dict]) -> Tuple[List[str], List[Dict]]: + """Leere Spalten entfernen""" + non_empty_headers = [] + + for header in headers: + has_values = any(row.get(header, '').strip() for row in data) + if has_values: + non_empty_headers.append(header) + + filtered_data = [] + for row in data: + filtered_row = {h: row.get(h, '') for h in non_empty_headers} + filtered_data.append(filtered_row) + + return non_empty_headers, filtered_data + + def remove_empty_rows(self, data: List[Dict]) -> List[Dict]: + """Komplett leere Zeilen entfernen""" + filtered_data = [] + for row in data: + has_values = any(str(v).strip() for v in row.values()) + if has_values: + filtered_data.append(row) + return filtered_data + + def analyze_row_completeness(self, headers: List[str], data: List[Dict]) -> Dict[int, int]: + """Analysiere Zeilen nach Anzahl ausgefüllter Felder""" + analysis = {} + for idx, row in enumerate(data): + filled_count = sum(1 for v in row.values() if str(v).strip()) + analysis[idx] = filled_count + return analysis + + def filter_rows_by_filled_fields(self, headers: List[str], data: List[Dict]) -> List[Dict]: + """Zeilen mit zu wenig Informationen filtern""" + total_columns = len(headers) + analysis = self.analyze_row_completeness(headers, data) + + print(f"\nAnalyse der Datenvollständigkeit:") + print(f"Gesamtanzahl Spalten: {total_columns}") + print(f"Gesamtanzahl Zeilen: {len(data)}") + + from collections import Counter + counts = Counter(analysis.values()) + + print("\nVerteilung ausgefüllter Felder pro Zeile:") + for filled_count in sorted(counts.keys()): + print(f" {filled_count} Felder ausgefüllt: {counts[filled_count]} Zeilen") + + print(f"\nBei wie vielen oder weniger ausgefüllten Feldern sollen Zeilen entfernt werden?") + while True: + try: + threshold = int(input(f"Schwellenwert (0-{total_columns}): ").strip()) + if 0 <= threshold <= total_columns: + break + print(f"Bitte eine Zahl zwischen 0 und {total_columns} eingeben.") + except ValueError: + print("Ungültige Eingabe.") + + rows_to_remove = [idx for idx, count in analysis.items() if count <= threshold] + + if rows_to_remove: + print(f"\n{len(rows_to_remove)} Zeilen haben {threshold} oder weniger ausgefüllte Felder:") + for i, idx in enumerate(rows_to_remove[:10]): + print(f" Zeile {idx + 1}: {analysis[idx]} Felder ausgefüllt") + sample = {k: v for k, v in data[idx].items() if str(v).strip()} + print(f" Daten: {sample}") + + if len(rows_to_remove) > 10: + print(f" ... und {len(rows_to_remove) - 10} weitere Zeilen") + + if self.ask_yes_no(f"\nMöchten Sie diese {len(rows_to_remove)} Zeilen löschen?"): + filtered_data = [row for idx, row in enumerate(data) if idx not in rows_to_remove] + print(f"✓ {len(rows_to_remove)} Zeilen entfernt") + return filtered_data + else: + print(f"Keine Zeilen mit {threshold} oder weniger ausgefüllten Feldern gefunden.") + + return data + + def select_columns(self, headers: List[str], original_names: Dict[str, str] = None, + preselected: Optional[List[str]] = None) -> List[str]: + """Spalten für Export auswählen mit interaktiver An-/Abwahl""" + GREEN = '\033[92m' + RED = '\033[91m' + RESET = '\033[0m' + BOLD = '\033[1m' + + if preselected and all(h in headers for h in preselected): + selected = set(preselected) + else: + selected = set(headers) + + def show_columns(): + print("\n" + "="*70) + print(" SPALTENAUSWAHL") + print("="*70) + print(f"{GREEN}● = Angewählt{RESET} | {RED}○ = Abgewählt{RESET}") + print("-"*70) + + for i, header in enumerate(headers, 1): + symbol = f"{GREEN}●{RESET}" if header in selected else f"{RED}○{RESET}" + + if original_names and header in original_names: + original = original_names[header] + if original != header: + display = f"{original} {BOLD}→ {header}{RESET}" + else: + display = header + else: + display = header + + print(f" {symbol} {i:3d}. {display}") + + print("-"*70) + print(f"Aktuell ausgewählt: {len(selected)} von {len(headers)} Spalten") + print("="*70) + + def parse_selection(input_str: str) -> set: + indices = set() + parts = input_str.split(',') + + for part in parts: + part = part.strip() + if '-' in part: + try: + start, end = part.split('-') + start_idx = int(start.strip()) - 1 + end_idx = int(end.strip()) - 1 + if 0 <= start_idx < len(headers) and 0 <= end_idx < len(headers): + for i in range(start_idx, end_idx + 1): + if 0 <= i < len(headers): + indices.add(i) + except ValueError: + pass + else: + try: + idx = int(part) - 1 + if 0 <= idx < len(headers): + indices.add(idx) + except ValueError: + pass + + return indices + + while True: + show_columns() + + print("\nOptionen:") + print(" [Nummern] - Spalten an/abwählen (z.B. '1,2,3' oder '1-5,10-15')") + print(" + [Nummern] - Spalten ANwählen (z.B. '+1,2,3' oder '+10-20')") + print(" - [Nummern] - Spalten ABwählen (z.B. '-1,2,3' oder '-5-10')") + print(" alle - Alle Spalten anwählen") + print(" keine - Alle Spalten abwählen") + print(" q - Auswahl beenden und fortfahren") + + choice = input("\nIhre Wahl: ").strip() + + if choice.lower() == 'q': + if selected: + break + else: + print(f"\n{RED}Fehler: Mindestens eine Spalte muss ausgewählt sein!{RESET}") + + elif choice.lower() == 'alle': + selected = set(headers) + print(f"{GREEN}✓ Alle Spalten angewählt{RESET}") + + elif choice.lower() == 'keine': + selected = set() + print(f"{RED}✓ Alle Spalten abgewählt{RESET}") + + elif choice.startswith('+'): + indices = parse_selection(choice[1:]) + for idx in indices: + selected.add(headers[idx]) + print(f"{GREEN}✓ {len(indices)} Spalte(n) angewählt{RESET}") + + elif choice.startswith('-'): + indices = parse_selection(choice[1:]) + for idx in indices: + selected.discard(headers[idx]) + print(f"{RED}✓ {len(indices)} Spalte(n) abgewählt{RESET}") + + else: + indices = parse_selection(choice) + for idx in indices: + if headers[idx] in selected: + selected.discard(headers[idx]) + else: + selected.add(headers[idx]) + if indices: + print(f"✓ {len(indices)} Spalte(n) umgeschaltet") + + return [h for h in headers if h in selected] + + def sort_data(self, data: List[Dict], sort_column: str, data_type: str) -> List[Dict]: + """Daten nach Spalte sortieren""" + def get_sort_key(row): + value = row.get(sort_column, '') + + if data_type == 'datum': + formats = ['%d.%m.%Y', '%Y-%m-%d', '%d/%m/%Y', '%m/%d/%Y'] + for fmt in formats: + try: + return datetime.strptime(value, fmt) + except ValueError: + continue + return datetime.min + + elif data_type == 'zeit': + formats = ['%H:%M:%S', '%H:%M'] + for fmt in formats: + try: + return datetime.strptime(value, fmt).time() + except ValueError: + continue + return datetime.min.time() + + elif data_type == 'dezimalzahl': + try: + value = value.replace(',', '.') + return float(value) + except ValueError: + return 0.0 + + else: + return value.lower() + + return sorted(data, key=get_sort_key) + + def configure_csv_export(self) -> Tuple[str, str]: + """CSV-Export-Einstellungen konfigurieren""" + print("\n" + "="*70) + print(" CSV-EXPORT KONFIGURATION") + print("="*70) + + print("\nDelimiter wählen:") + print(" 1. Semikolon (;) - Standard für deutsche Excel-Versionen") + print(" 2. Komma (,) - Internationaler Standard") + print(" 3. Tab - Gut für Import in andere Programme") + + while True: + delim_choice = input("Ihre Wahl [1-3]: ").strip() + delim_map = {'1': ';', '2': ',', '3': '\t'} + if delim_choice in delim_map: + delimiter = delim_map[delim_choice] + break + print("Ungültige Eingabe.") + + print("\nQuoting (Anführungszeichen) wählen:") + print(" 1. MINIMAL - Nur Felder mit Sonderzeichen (empfohlen)") + print(" 2. ALLE - Alle Felder in Anführungszeichen") + print(" 3. NICHT_NUMERISCH - Nur Text-Felder") + print(" 4. KEINE - Keine Anführungszeichen (kann Probleme verursachen)") + + while True: + quot_choice = input("Ihre Wahl [1-4]: ").strip() + quot_map = {'1': 'MINIMAL', '2': 'ALLE', '3': 'NICHT_NUMERISCH', '4': 'KEINE'} + if quot_choice in quot_map: + quoting = quot_map[quot_choice] + break + print("Ungültige Eingabe.") + + delimiter_names = {';': 'Semikolon', ',': 'Komma', '\t': 'Tab'} + print("\n" + "-"*70) + print(f"Gewählte Einstellungen:") + print(f" Delimiter: {delimiter_names.get(delimiter, delimiter)}") + print(f" Quoting: {quoting}") + print("-"*70) + + return delimiter, quoting + + def write_csv(self, filepath: Path, headers: List[str], data: List[Dict], + header_text: str = "", footer_text: str = "", + delimiter: str = ';', quoting: str = 'MINIMAL'): + """CSV-Datei schreiben""" + quoting_map = { + 'ALLE': csv.QUOTE_ALL, + 'MINIMAL': csv.QUOTE_MINIMAL, + 'NICHT_NUMERISCH': csv.QUOTE_NONNUMERIC, + 'KEINE': csv.QUOTE_NONE + } + + csv_quoting = quoting_map.get(quoting, csv.QUOTE_MINIMAL) + + with open(filepath, 'w', encoding='utf-8', newline='') as f: + if header_text: + f.write(f"# {header_text}\n") + + writer = csv.DictWriter(f, fieldnames=headers, delimiter=delimiter, + quoting=csv_quoting, quotechar='"') + writer.writeheader() + writer.writerows(data) + + if footer_text: + f.write(f"# {footer_text}\n") + + print(f"✓ CSV-Datei erfolgreich gespeichert: {filepath}") + + def write_excel(self, filepath: Path, headers: List[str], data: List[Dict], + header_text: str = "", footer_text: str = ""): + """Excel-Datei schreiben""" + if not EXCEL_SUPPORT: + print("Fehler: openpyxl ist nicht installiert. Installieren Sie es mit: pip install openpyxl") + return + + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "Daten" + + row_num = 1 + + if header_text: + ws.cell(row=row_num, column=1, value=f"# {header_text}") + ws.cell(row=row_num, column=1).font = Font(italic=True) + row_num += 1 + + for col_num, header in enumerate(headers, 1): + cell = ws.cell(row=row_num, column=col_num, value=header) + cell.font = Font(bold=True) + cell.fill = PatternFill(start_color="CCCCCC", end_color="CCCCCC", fill_type="solid") + row_num += 1 + + for row_data in data: + for col_num, header in enumerate(headers, 1): + ws.cell(row=row_num, column=col_num, value=row_data.get(header, '')) + row_num += 1 + + if footer_text: + ws.cell(row=row_num, column=1, value=f"# {footer_text}") + ws.cell(row=row_num, column=1).font = Font(italic=True) + + for column in ws.columns: + max_length = 0 + column_letter = column[0].column_letter + for cell in column: + try: + if len(str(cell.value)) > max_length: + max_length = len(cell.value) + except: + pass + adjusted_width = min(max_length + 2, 50) + ws.column_dimensions[column_letter].width = adjusted_width + + wb.save(filepath) + print(f"✓ Excel-Datei erfolgreich gespeichert: {filepath}") + + def write_ods(self, filepath: Path, headers: List[str], data: List[Dict], + header_text: str = "", footer_text: str = ""): + """OpenDocument Spreadsheet (ODS) schreiben""" + if not ODT_SUPPORT: + print("Fehler: odfpy ist nicht installiert. Installieren Sie es mit: pip install odfpy") + return + + doc = OpenDocumentSpreadsheet() + table = Table(name="Daten") + + if header_text: + row = TableRow() + cell = TableCell() + cell.addElement(P(text=f"# {header_text}")) + row.addElement(cell) + table.addElement(row) + + row = TableRow() + for header in headers: + cell = TableCell() + cell.addElement(P(text=header)) + row.addElement(cell) + table.addElement(row) + + for row_data in data: + row = TableRow() + for header in headers: + cell = TableCell() + cell.addElement(P(text=str(row_data.get(header, '')))) + row.addElement(cell) + table.addElement(row) + + if footer_text: + row = TableRow() + cell = TableCell() + cell.addElement(P(text=f"# {footer_text}")) + row.addElement(cell) + table.addElement(row) + + doc.spreadsheet.addElement(table) + doc.save(filepath) + print(f"✓ ODS-Datei erfolgreich gespeichert: {filepath}") + + def select_output_format(self) -> str: + """Ausgabeformat auswählen""" + formats = ["csv"] + print("\nVerfügbare Ausgabeformate:") + print(" 1. CSV") + + format_num = 2 + if EXCEL_SUPPORT: + formats.append("xlsx") + print(f" {format_num}. Excel (XLSX)") + format_num += 1 + + if ODT_SUPPORT: + formats.append("ods") + print(f" {format_num}. OpenDocument (ODS)") + + while True: + choice = input("Ihre Wahl: ").strip() + try: + idx = int(choice) - 1 + if 0 <= idx < len(formats): + return formats[idx] + except ValueError: + if choice.lower() in formats: + return choice.lower() + print("Ungültige Eingabe.") + + def manage_presets(self): + """Voreinstellungen verwalten""" + while True: + self.clear_screen() + print("╔═══════════════════════════════════════════════════════╗") + print("║ VOREINSTELLUNGEN VERWALTEN ║") + print("╚═══════════════════════════════════════════════════════╝") + + presets = self.get_available_presets() + + if presets: + print("\nVerfügbare Voreinstellungen:") + for i, preset in enumerate(presets, 1): + print(f" {i}. {preset}") + else: + print("\n⚠ Keine Voreinstellungen vorhanden.") + + print("\nOptionen:") + print(" 1. Voreinstellung bearbeiten") + print(" 2. Voreinstellung löschen") + print(" 0. Zurück zum Hauptmenü") + + choice = input("\nIhre Wahl: ").strip() + + if choice == '0': + return + + elif choice == '1' and presets: + while True: + preset_choice = input("\nNummer oder Name der Voreinstellung: ").strip() + + try: + idx = int(preset_choice) - 1 + if 0 <= idx < len(presets): + preset_name = presets[idx] + else: + print("Ungültige Nummer.") + continue + except ValueError: + preset_name = preset_choice + + if preset_name in presets: + self.edit_preset(preset_name) + break + else: + print(f"Voreinstellung '{preset_name}' nicht gefunden.") + + elif choice == '2' and presets: + while True: + preset_choice = input("\nNummer oder Name der zu löschenden Voreinstellung: ").strip() + + try: + idx = int(preset_choice) - 1 + if 0 <= idx < len(presets): + preset_name = presets[idx] + else: + print("Ungültige Nummer.") + continue + except ValueError: + preset_name = preset_choice + + if preset_name in presets: + if self.ask_yes_no(f"Voreinstellung '{preset_name}' wirklich löschen?"): + self.delete_preset(preset_name) + input("\nDrücken Sie Enter um fortzufahren...") + break + else: + print(f"Voreinstellung '{preset_name}' nicht gefunden.") + + def edit_preset(self, preset_name: str): + """Voreinstellung bearbeiten""" + config = self.load_preset(preset_name) + + print(f"\n✓ Voreinstellung '{preset_name}' geladen.") + print("\nSie können nun die Einstellungen Schritt für Schritt durchgehen und ändern.") + print("Die aktuellen Werte werden als Vorschläge angezeigt.") + input("\nDrücken Sie Enter um zu beginnen...") + + config = self.configure_settings(config, edit_mode=True) + + self.clear_screen() + print("╔═══════════════════════════════════════════════════════╗") + print("║ VOREINSTELLUNG SPEICHERN ║") + print("╚═══════════════════════════════════════════════════════╝") + + print("\nMöchten Sie die Änderungen speichern?") + print(f" 1. Unter gleichem Namen überschreiben ('{preset_name}')") + print(f" 2. Als neue Voreinstellung speichern") + print(f" 3. Änderungen verwerfen") + + while True: + save_choice = input("\nIhre Wahl [1-3]: ").strip() + + if save_choice == '1': + self.save_preset(preset_name, config) + input("\nDrücken Sie Enter um fortzufahren...") + return + + elif save_choice == '2': + new_name = input("\nName für die neue Voreinstellung: ").strip() + if new_name: + self.save_preset(new_name, config) + input("\nDrücken Sie Enter um fortzufahren...") + return + + elif save_choice == '3': + print("\nÄnderungen verworfen.") + input("\nDrücken Sie Enter um fortzufahren...") + return + + def configure_settings(self, config: Dict = None, edit_mode: bool = False) -> Dict: + """Einstellungen konfigurieren (für Bearbeitung oder neue Voreinstellung)""" + if config is None: + config = {} + + self.clear_screen() + print("╔═══════════════════════════════════════════════════════╗") + print("║ EINSTELLUNGEN KONFIGURIEREN ║") + print("╚═══════════════════════════════════════════════════════╝") + + # 1. Kopfzeile vorhanden? + print("\n" + "="*60) + print(" 1. Kopfzeile") + print("="*60) + current = config.get('has_header') + if current is not None: + print(f"Aktuell: {'Ja' if current else 'Nein'}") + config['has_header'] = self.ask_yes_no("Hat die Datei normalerweise eine Kopfzeile?", current) + + # 2. Mapping-Datei + print("\n" + "="*60) + print(" 2. Spaltennamen-Mapping") + print("="*60) + current_mapping = config.get('mapping_file') + if current_mapping: + print(f"Aktuell: {current_mapping}") + + if self.ask_yes_no("Möchten Sie eine Mapping-Datei verwenden?", current_mapping is not None): + mapping_file = self.select_file("Pfad zur Mapping-JSON-Datei") + if mapping_file: + config['mapping_file'] = str(mapping_file) + else: + config['mapping_file'] = None + + # 3. Leere Zeilen entfernen + print("\n" + "="*60) + print(" 3. Leere Zeilen") + print("="*60) + current = config.get('remove_empty_rows') + if current is not None: + print(f"Aktuell: {'Ja' if current else 'Nein'}") + config['remove_empty_rows'] = self.ask_yes_no("Leere Zeilen entfernen?", current) + + # 4. Unvollständige Zeilen filtern + print("\n" + "="*60) + print(" 4. Unvollständige Zeilen") + print("="*60) + current = config.get('filter_incomplete_rows') + if current is not None: + print(f"Aktuell: {'Ja' if current else 'Nein'}") + config['filter_incomplete_rows'] = self.ask_yes_no("Zeilen mit zu wenig Daten analysieren?", current) + + # 5. Leere Spalten entfernen + print("\n" + "="*60) + print(" 5. Leere Spalten") + print("="*60) + current = config.get('remove_empty') + if current is not None: + print(f"Aktuell: {'Ja' if current else 'Nein'}") + config['remove_empty'] = self.ask_yes_no("Leere Spalten entfernen?", current) + + # 6. Spaltenauswahl + print("\n" + "="*60) + print(" 6. Spaltenauswahl") + print("="*60) + + if 'selected_columns' in config and config['selected_columns']: + print(f"Aktuell gespeicherte Spalten: {len(config['selected_columns'])}") + print("\nGespeicherte Spalten:") + for i, col in enumerate(config['selected_columns'], 1): + print(f" {i}. {col}") + + if self.ask_yes_no("\nMöchten Sie die Spaltenauswahl ändern?"): + print("\nBitte laden Sie eine Beispieldatei, um Spalten auszuwählen:") + example_file = self.select_file("Pfad zur Beispieldatei (CSV/Excel)") + + if example_file: + try: + file_ext = example_file.suffix.lower() + if file_ext in ['.xlsx', '.xls']: + ex_headers, ex_data, ex_has_header, _ = self.read_excel(example_file) + else: + ex_headers, ex_data, ex_has_header, _ = self.read_csv(example_file) + + if config.get('mapping_file'): + mapping_file = Path(config['mapping_file']) + if mapping_file.exists(): + mappings = self.load_column_mappings(mapping_file) + ex_headers, ex_data, ex_original_names = self.apply_column_mappings(ex_headers, ex_data, mappings) + else: + ex_original_names = {h: h for h in ex_headers} + else: + ex_original_names = {h: h for h in ex_headers} + + config['selected_columns'] = self.select_columns(ex_headers, ex_original_names, config['selected_columns']) + + except Exception as e: + print(f"Fehler beim Laden der Beispieldatei: {e}") + else: + print("Keine Spalten gespeichert.") + if self.ask_yes_no("Möchten Sie Spalten auswählen?"): + print("\nBitte laden Sie eine Beispieldatei, um Spalten auszuwählen:") + example_file = self.select_file("Pfad zur Beispieldatei (CSV/Excel)") + + if example_file: + try: + file_ext = example_file.suffix.lower() + if file_ext in ['.xlsx', '.xls']: + ex_headers, ex_data, ex_has_header, _ = self.read_excel(example_file) + else: + ex_headers, ex_data, ex_has_header, _ = self.read_csv(example_file) + + if config.get('mapping_file'): + mapping_file = Path(config['mapping_file']) + if mapping_file.exists(): + mappings = self.load_column_mappings(mapping_file) + ex_headers, ex_data, ex_original_names = self.apply_column_mappings(ex_headers, ex_data, mappings) + else: + ex_original_names = {h: h for h in ex_headers} + else: + ex_original_names = {h: h for h in ex_headers} + + config['selected_columns'] = self.select_columns(ex_headers, ex_original_names, None) + + except Exception as e: + print(f"Fehler beim Laden der Beispieldatei: {e}") + + # 7. Kopf- und Fußzeilen + print("\n" + "="*60) + print(" 7. Kopf- und Fußzeilen") + print("="*60) + current = config.get('add_header_footer') + if current is not None: + print(f"Aktuell: {'Ja' if current else 'Nein'}") + if current: + print(f" Kopfzeile: {config.get('header_text', '')}") + print(f" Fußzeile: {config.get('footer_text', '')}") + + if self.ask_yes_no("Kopf-/Fußzeile hinzufügen?", current): + header_text = input("Kopfzeile (Enter für keine): ").strip() + footer_text = input("Fußzeile (Enter für keine): ").strip() + config['add_header_footer'] = True + config['header_text'] = header_text + config['footer_text'] = footer_text + else: + config['add_header_footer'] = False + + # 8. Sortierung + print("\n" + "="*60) + print(" 8. Sortierung") + print("="*60) + current_sort = config.get('sort_column') + if current_sort: + print(f"Aktuell: Nach '{current_sort}' ({config.get('sort_type', 'string')})") + + if self.ask_yes_no("Daten sortieren?", current_sort is not None): + sort_column = input("Spaltenname für Sortierung: ").strip() + if sort_column: + print("\nDatentyp für Sortierung:") + print(" 1. Text (string)") + print(" 2. Datum") + print(" 3. Zeit") + print(" 4. Dezimalzahl") + + type_map = {'1': 'string', '2': 'datum', '3': 'zeit', '4': 'dezimalzahl'} + data_type = type_map.get(input("Ihre Wahl [1-4]: ").strip(), 'string') + + config['sort_column'] = sort_column + config['sort_type'] = data_type + else: + config['sort_column'] = None + + # 9. Ausgabeformat + print("\n" + "="*60) + print(" 9. Ausgabeformat") + print("="*60) + current_format = config.get('output_format', 'csv') + print(f"Aktuell: {current_format.upper()}") + + formats = ["csv"] + print("\nVerfügbare Ausgabeformate:") + print(" 1. CSV") + + format_num = 2 + if EXCEL_SUPPORT: + formats.append("xlsx") + print(f" {format_num}. Excel (XLSX)") + format_num += 1 + + if ODT_SUPPORT: + formats.append("ods") + print(f" {format_num}. OpenDocument (ODS)") + + while True: + choice = input("Ihre Wahl: ").strip() + try: + idx = int(choice) - 1 + if 0 <= idx < len(formats): + config['output_format'] = formats[idx] + break + except ValueError: + if choice.lower() in formats: + config['output_format'] = choice.lower() + break + print("Ungültige Eingabe.") + + # 10. CSV-Export-Einstellungen (nur wenn CSV gewählt) + if config['output_format'] == 'csv': + print("\n" + "="*60) + print(" 10. CSV-Export-Einstellungen") + print("="*60) + + current_delim = config.get('export_delimiter') + current_quot = config.get('export_quoting') + + if current_delim and current_quot: + delimiter_names = {';': 'Semikolon', ',': 'Komma', '\t': 'Tab'} + print(f"Aktuell: Delimiter={delimiter_names.get(current_delim, current_delim)}, Quoting={current_quot}") + + if self.ask_yes_no("CSV-Export-Einstellungen konfigurieren?", True): + export_delimiter, export_quoting = self.configure_csv_export() + config['export_delimiter'] = export_delimiter + config['export_quoting'] = export_quoting + + return config + + def main_menu(self): + """Hauptmenü anzeigen""" + while True: + self.clear_screen() + print("╔═══════════════════════════════════════════════════════╗") + print("║ CSV-PROCESSOR v2.0 - Dateiverarbeitung ║") + print("╚═══════════════════════════════════════════════════════╝") + + print("\nVerfügbare Features:") + print(f" Import: CSV{', Excel (XLSX/XLS)' if EXCEL_SUPPORT else ''}") + print(f" Export: CSV{', Excel (XLSX)' if EXCEL_SUPPORT else ''}{', OpenDocument (ODS)' if ODT_SUPPORT else ''}") + + if not EXCEL_SUPPORT: + print(f" ⚠ Excel-Support: pip install openpyxl") + if not ODT_SUPPORT: + print(f" ⚠ ODS-Support: pip install odfpy") + + print("\n" + "="*60) + print(" HAUPTMENÜ") + print("="*60) + print(" 1. Neue Datei verarbeiten") + print(" 2. Voreinstellungen verwalten") + print(" 0. Beenden") + + choice = input("\nIhre Wahl: ").strip() + + if choice == '1': + self.run() + elif choice == '2': + self.manage_presets() + elif choice == '0': + print("\nAuf Wiedersehen!") + sys.exit(0) + else: + print("\nUngültige Eingabe.") + input("Drücken Sie Enter um fortzufahren...") + + def run(self): + """Hauptprogramm ausführen""" + self.clear_screen() + print("╔═══════════════════════════════════════════════════════╗") + print("║ DATEI VERARBEITEN ║") + print("╚═══════════════════════════════════════════════════════╝") + + config = {} + use_preset = False + preset_loaded_file = False + + # 1. Voreinstellungen abfragen + presets = self.get_available_presets() + if presets: + self.print_header("Schritt 1: Voreinstellungen") + print("Verfügbare Voreinstellungen:") + for i, preset in enumerate(presets, 1): + print(f" {i}. {preset}") + + if self.ask_yes_no("\nMöchten Sie eine Voreinstellung laden?"): + while True: + choice = input("Nummer oder Name der Voreinstellung: ").strip() + + try: + idx = int(choice) - 1 + if 0 <= idx < len(presets): + preset_name = presets[idx] + else: + print("Ungültige Nummer.") + continue + except ValueError: + preset_name = choice + + if preset_name in presets: + config = self.load_preset(preset_name) + use_preset = True + print(f"✓ Voreinstellung '{preset_name}' geladen.") + + # Zeige was geladen wurde + if 'source_file' in config and config['source_file']: + print(f" → Quelldatei vorbelegt: {config['source_file']}") + if 'mapping_file' in config and config['mapping_file']: + print(f" → Mapping vorbelegt: {config['mapping_file']}") + if 'selected_columns' in config and config['selected_columns']: + print(f" → Spaltenauswahl: {len(config['selected_columns'])} Spalten") + + print("\nℹ Leere Spalten werden automatisch übersprungen (vor Spaltenauswahl)") + + break + else: + print(f"Voreinstellung '{preset_name}' nicht gefunden.") + + # 2. Quell-Datei + self.print_header("Schritt 2: Quelldatei auswählen") + + # Wenn Voreinstellung Quelldatei enthält, diese verwenden + if use_preset and 'source_file' in config and config['source_file']: + source_path = Path(config['source_file']) + if source_path.exists(): + print(f"Verwende Quelldatei aus Voreinstellung: {source_path}") + if self.ask_yes_no("Diese Datei verwenden?", True): + source_file = source_path + preset_loaded_file = True + else: + source_file = None + else: + print(f"⚠ Gespeicherte Quelldatei nicht gefunden: {config['source_file']}") + source_file = None + else: + source_file = None + + # Wenn keine Datei aus Voreinstellung oder Benutzer will andere Datei + if not source_file: + while not source_file: + source_file = self.select_file("Pfad zur Quelldatei (CSV/XLSX/XLS/ODS)") + if not source_file: + print("Eine Quelldatei ist erforderlich!") + + # Datei lesen + file_ext = source_file.suffix.lower() + has_header = config.get('has_header', None) if use_preset else None + + try: + if file_ext in ['.xlsx', '.xls']: + headers, data, has_header, import_settings = self.read_excel(source_file) + else: + import_encoding = config.get('import_encoding') if use_preset else None + import_delimiter = config.get('import_delimiter') if use_preset else None + import_quoting = config.get('import_quoting') if use_preset else None + + headers, data, has_header, import_settings = self.read_csv( + source_file, has_header, import_encoding, import_delimiter, import_quoting + ) + + config['import_encoding'] = import_settings['encoding'] + config['import_delimiter'] = import_settings['delimiter'] + config['import_quoting'] = import_settings['quoting'] + + if headers is None: + print("Fehler beim Lesen der Datei.") + return + + config['has_header'] = has_header + print(f"\n✓ Datei geladen: {len(headers)} Spalten, {len(data)} Zeilen") + except Exception as e: + print(f"Fehler beim Lesen der Datei: {e}") + import traceback + traceback.print_exc() + return + + # 3. Spaltennamen-Mapping + self.print_header("Schritt 3: Spaltennamen umbenennen") + original_names = {} + + if use_preset and 'mapping_file' in config: + mapping_file = Path(config['mapping_file']) if config['mapping_file'] else None + if mapping_file and mapping_file.exists(): + print(f"Verwende Mapping-Datei aus Voreinstellung: {mapping_file}") + mappings = self.load_column_mappings(mapping_file) + else: + if mapping_file: + print(f"⚠ Mapping-Datei {mapping_file} nicht gefunden.") + mappings = {} + else: + if self.ask_yes_no("Möchten Sie Spaltennamen umbenennen?"): + mapping_file = self.select_file("Pfad zur Mapping-JSON-Datei") + mappings = self.load_column_mappings(mapping_file) + if mapping_file: + config['mapping_file'] = str(mapping_file) + else: + mappings = {} + + if mappings: + headers, data, original_names = self.apply_column_mappings(headers, data, mappings) + print(f"✓ {len(mappings)} Spaltennamen umbenannt") + else: + original_names = {h: h for h in headers} + + # 4. Leere Zeilen entfernen + self.print_header("Schritt 4: Leere Zeilen") + if use_preset and 'remove_empty_rows' in config: + remove_empty_rows = config['remove_empty_rows'] + print(f"Verwende Voreinstellung: {'Ja' if remove_empty_rows else 'Nein'}") + else: + remove_empty_rows = self.ask_yes_no("Sollen komplett leere Zeilen entfernt werden?") + config['remove_empty_rows'] = remove_empty_rows + + if remove_empty_rows: + original_count = len(data) + data = self.remove_empty_rows(data) + removed_count = original_count - len(data) + print(f"✓ {removed_count} leere Zeilen entfernt") + + # 5. Zeilen mit zu wenig Daten filtern + self.print_header("Schritt 5: Unvollständige Zeilen") + if use_preset and 'filter_incomplete_rows' in config: + filter_incomplete = config['filter_incomplete_rows'] + print(f"Verwende Voreinstellung: {'Ja' if filter_incomplete else 'Nein'}") + else: + filter_incomplete = self.ask_yes_no("Möchten Sie Zeilen mit zu wenig Informationen analysieren/entfernen?") + config['filter_incomplete_rows'] = filter_incomplete + + if filter_incomplete: + data = self.filter_rows_by_filled_fields(headers, data) + + # 6. Leere Spalten entfernen (VOR Spaltenauswahl!) + self.print_header("Schritt 6: Leere Spalten") + if use_preset and 'remove_empty' in config: + remove_empty = config['remove_empty'] + print(f"Verwende Voreinstellung: {'Ja' if remove_empty else 'Nein'}") + else: + remove_empty = self.ask_yes_no("Sollen leere Spalten entfernt werden?") + config['remove_empty'] = remove_empty + + if remove_empty: + original_count = len(headers) + headers, data = self.remove_empty_columns(headers, data) + removed_count = original_count - len(headers) + print(f"✓ {removed_count} leere Spalten entfernt") + + # WICHTIG: original_names aktualisieren + original_names = {h: original_names[h] for h in headers if h in original_names} + + # 7. Spaltenauswahl + self.print_header("Schritt 7: Spaltenauswahl") + + # Bei Voreinstellung: Nur vorhandene (nicht-leere) Spalten verwenden + if use_preset and 'selected_columns' in config: + preselected = [col for col in config['selected_columns'] if col in headers] + + if preselected: + selected_columns = preselected + print(f"Verwende gespeicherte Spaltenauswahl:") + print(f"(Nur nicht-leere Spalten werden berücksichtigt)") + for i, col in enumerate(selected_columns, 1): + orig = original_names.get(col, col) + if orig != col: + print(f" {i}. {orig} → {col}") + else: + print(f" {i}. {col}") + print(f"\n✓ {len(selected_columns)} Spalten ausgewählt") + + # Zeige übersprungene Spalten + skipped = [col for col in config['selected_columns'] if col not in headers] + if skipped: + print(f"\nℹ Übersprungene Spalten (leer oder nicht vorhanden): {len(skipped)}") + for col in skipped[:5]: + print(f" - {col}") + if len(skipped) > 5: + print(f" ... und {len(skipped)-5} weitere") + else: + print("⚠ Alle gespeicherten Spalten sind leer oder nicht vorhanden.") + print("Bitte neue Auswahl treffen:") + selected_columns = self.select_columns(headers, original_names, None) + config['selected_columns'] = selected_columns + else: + selected_columns = self.select_columns(headers, original_names, None) + config['selected_columns'] = selected_columns + + # Daten filtern + filtered_data = [] + for row in data: + filtered_row = {col: row.get(col, '') for col in selected_columns} + filtered_data.append(filtered_row) + data = filtered_data + + # 8. Kopf- und Fußzeilen + self.print_header("Schritt 8: Kopf- und Fußzeilen") + header_text = "" + footer_text = "" + + if use_preset and 'add_header_footer' in config: + add_header_footer = config['add_header_footer'] + if add_header_footer: + header_text = config.get('header_text', '') + footer_text = config.get('footer_text', '') + print(f"Verwende Kopfzeile: {header_text}") + print(f"Verwende Fußzeile: {footer_text}") + else: + if self.ask_yes_no("Möchten Sie eine Kopf- oder Fußzeile hinzufügen?"): + header_text = input("Kopfzeile (Enter für keine): ").strip() + footer_text = input("Fußzeile (Enter für keine): ").strip() + config['add_header_footer'] = True + config['header_text'] = header_text + config['footer_text'] = footer_text + else: + config['add_header_footer'] = False + + # 9. Sortierung + self.print_header("Schritt 9: Sortierung") + if use_preset and 'sort_column' in config: + sort_column = config['sort_column'] + if sort_column and sort_column in selected_columns: + data_type = config.get('sort_type', 'string') + print(f"Sortiere nach '{sort_column}' ({data_type})") + data = self.sort_data(data, sort_column, data_type) + else: + if self.ask_yes_no("Möchten Sie die Daten sortieren?"): + print("\nVerfügbare Spalten zum Sortieren:") + for i, col in enumerate(selected_columns, 1): + print(f" {i}. {col}") + + while True: + choice = input("Nummer der Spalte: ").strip() + try: + idx = int(choice) - 1 + if 0 <= idx < len(selected_columns): + sort_column = selected_columns[idx] + break + except ValueError: + pass + print("Ungültige Eingabe.") + + print("\nDatentyp für Sortierung:") + print(" 1. Text (string)") + print(" 2. Datum") + print(" 3. Zeit") + print(" 4. Dezimalzahl") + + type_map = {'1': 'string', '2': 'datum', '3': 'zeit', '4': 'dezimalzahl'} + data_type = type_map.get(input("Ihre Wahl [1-4]: ").strip(), 'string') + + data = self.sort_data(data, sort_column, data_type) + config['sort_column'] = sort_column + config['sort_type'] = data_type + print(f"✓ Daten nach '{sort_column}' sortiert") + else: + config['sort_column'] = None + + # 10. Ausgabeformat und Zieldatei + self.print_header("Schritt 10: Zieldatei speichern") + + if use_preset and 'output_format' in config: + output_format = config['output_format'] + print(f"Ausgabeformat aus Voreinstellung: {output_format.upper()}") + else: + output_format = self.select_output_format() + config['output_format'] = output_format + + while True: + target_path = input(f"Name/Pfad der Zieldatei (ohne Endung): ").strip() + if target_path: + target_file = Path(target_path).with_suffix(f'.{output_format}') + + if target_file.exists(): + if self.ask_yes_no(f"Datei {target_file} existiert bereits. Überschreiben?"): + break + else: + break + + # Datei schreiben + if output_format == 'csv': + if use_preset and 'export_delimiter' in config and 'export_quoting' in config: + export_delimiter = config['export_delimiter'] + export_quoting = config['export_quoting'] + delimiter_names = {';': 'Semikolon', ',': 'Komma', '\t': 'Tab'} + print(f"\nVerwende Export-Einstellungen aus Voreinstellung:") + print(f" Delimiter: {delimiter_names.get(export_delimiter, export_delimiter)}") + print(f" Quoting: {export_quoting}") + else: + export_delimiter, export_quoting = self.configure_csv_export() + config['export_delimiter'] = export_delimiter + config['export_quoting'] = export_quoting + + self.write_csv(target_file, selected_columns, data, header_text, footer_text, + export_delimiter, export_quoting) + elif output_format == 'xlsx': + self.write_excel(target_file, selected_columns, data, header_text, footer_text) + elif output_format == 'ods': + self.write_ods(target_file, selected_columns, data, header_text, footer_text) + + print("\n" + "="*60) + print(" Verarbeitung abgeschlossen!") + print("="*60) + + # Voreinstellung speichern + if not use_preset: + if self.ask_yes_no("\nMöchten Sie diese Einstellungen als Voreinstellung speichern?"): + # Fragen ob Quelldatei mitgespeichert werden soll + save_source = False + if self.ask_yes_no("Pfad zur Quelldatei mit speichern? (Datei wird dann automatisch geladen)", False): + config['source_file'] = str(source_file) + save_source = True + + preset_name = input("Name für die Voreinstellung: ").strip() + if preset_name: + self.save_preset(preset_name, config) + print(f"\nGespeichert:") + print(f" - Einstellungen: Ja") + print(f" - Mapping: {'Ja' if config.get('mapping_file') else 'Nein'}") + print(f" - Quelldatei: {'Ja' if save_source else 'Nein'}") + print(f" - Spaltenauswahl: {len(config['selected_columns'])} Spalten") + + input("\nDrücken Sie Enter um zum Hauptmenü zurückzukehren...") + + +def main(): + """Hauptfunktion""" + processor = CSVProcessor() + + try: + processor.main_menu() + except KeyboardInterrupt: + print("\n\nProgramm wurde durch Benutzer beendet.") + sys.exit(0) + except Exception as e: + print(f"\nFehler: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/csv_processor_gui.py b/csv_processor_gui.py new file mode 100644 index 0000000..ffd4d8c --- /dev/null +++ b/csv_processor_gui.py @@ -0,0 +1,829 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +CSV-Processor GUI v2.0: Grafische Oberfläche für CSV/Excel-Verarbeitung +""" + +import tkinter as tk +from tkinter import ttk, filedialog, messagebox, scrolledtext, simpledialog +import csv +import json +import os +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +# Optional: Excel Support +try: + import openpyxl + from openpyxl.styles import Font, PatternFill + EXCEL_SUPPORT = True +except ImportError: + EXCEL_SUPPORT = False + +try: + from odf.opendocument import OpenDocumentSpreadsheet + from odf.table import Table, TableRow, TableCell + from odf.text import P + ODT_SUPPORT = True +except ImportError: + ODT_SUPPORT = False + + +class CSVProcessorGUI: + def __init__(self, root): + self.root = root + self.root.title("CSV-Processor v2.0") + self.root.geometry("900x700") + + self.config_dir = Path("csv_processor_config") + self.config_dir.mkdir(exist_ok=True) + + self.current_config = {} # Für temporäre Voreinstellungs-Daten + self.headers = [] + self.data = [] + self.original_names = {} + self.selected_columns = set() + + self.setup_ui() + + def setup_ui(self): + """Hauptoberfläche erstellen""" + # Menüleiste + menubar = tk.Menu(self.root) + self.root.config(menu=menubar) + + # Datei-Menü + file_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Datei", menu=file_menu) + file_menu.add_command(label="Datei öffnen", command=self.load_file) + file_menu.add_command(label="Verarbeiten & Speichern", command=self.process_and_save) + file_menu.add_separator() + file_menu.add_command(label="Beenden", command=self.root.quit) + + # Voreinstellungen-Menü + preset_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Voreinstellungen", menu=preset_menu) + preset_menu.add_command(label="Laden", command=self.load_preset) + preset_menu.add_command(label="Speichern", command=self.save_preset) + preset_menu.add_command(label="Verwalten", command=self.manage_presets) + + # Hilfe-Menü + help_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Hilfe", menu=help_menu) + help_menu.add_command(label="Über", command=self.show_about) + + # Hauptcontainer + main_frame = ttk.Frame(self.root, padding="10") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + main_frame.columnconfigure(0, weight=1) + main_frame.rowconfigure(4, weight=1) + + # Datei-Auswahl + file_frame = ttk.LabelFrame(main_frame, text="Quelldatei", padding="10") + file_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5) + file_frame.columnconfigure(1, weight=1) + + ttk.Label(file_frame, text="Datei:").grid(row=0, column=0, sticky=tk.W) + self.file_entry = ttk.Entry(file_frame, width=50) + self.file_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5) + ttk.Button(file_frame, text="Durchsuchen", command=self.browse_file).grid(row=0, column=2) + ttk.Button(file_frame, text="Laden", command=self.load_file).grid(row=0, column=3, padx=5) + + # Einstellungen + settings_frame = ttk.LabelFrame(main_frame, text="Verarbeitungseinstellungen", padding="10") + settings_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=5) + settings_frame.columnconfigure(1, weight=1) + + row = 0 + self.has_header_var = tk.BooleanVar(value=True) + ttk.Checkbutton(settings_frame, text="Datei hat Kopfzeile", + variable=self.has_header_var).grid(row=row, column=0, sticky=tk.W, columnspan=2) + + row += 1 + self.remove_empty_rows_var = tk.BooleanVar(value=True) + ttk.Checkbutton(settings_frame, text="Leere Zeilen entfernen", + variable=self.remove_empty_rows_var).grid(row=row, column=0, sticky=tk.W, columnspan=2) + + row += 1 + self.remove_empty_cols_var = tk.BooleanVar(value=True) + ttk.Checkbutton(settings_frame, text="Leere Spalten entfernen (automatisch vor Spaltenauswahl)", + variable=self.remove_empty_cols_var).grid(row=row, column=0, sticky=tk.W, columnspan=3) + + # Info-Label + info_label = ttk.Label(settings_frame, + text="ℹ Leere Spalten werden vor der Spaltenauswahl entfernt", + foreground="blue", font=('TkDefaultFont', 8)) + info_label.grid(row=row+1, column=0, columnspan=3, sticky=tk.W, pady=(0,5)) + + row += 2 + ttk.Label(settings_frame, text="Mapping-Datei:").grid(row=row, column=0, sticky=tk.W) + self.mapping_entry = ttk.Entry(settings_frame) + self.mapping_entry.grid(row=row, column=1, sticky=(tk.W, tk.E), padx=5) + ttk.Button(settings_frame, text="...", command=self.browse_mapping, width=3).grid(row=row, column=2) + + row += 1 + ttk.Label(settings_frame, text="Ausgabeformat:").grid(row=row, column=0, sticky=tk.W) + self.format_var = tk.StringVar(value="csv") + format_frame = ttk.Frame(settings_frame) + format_frame.grid(row=row, column=1, sticky=tk.W, padx=5) + ttk.Radiobutton(format_frame, text="CSV", variable=self.format_var, value="csv").pack(side=tk.LEFT) + if EXCEL_SUPPORT: + ttk.Radiobutton(format_frame, text="Excel", variable=self.format_var, value="xlsx").pack(side=tk.LEFT, padx=10) + if ODT_SUPPORT: + ttk.Radiobutton(format_frame, text="ODS", variable=self.format_var, value="ods").pack(side=tk.LEFT) + + # Spaltenauswahl + columns_frame = ttk.LabelFrame(main_frame, text="Spaltenauswahl", padding="10") + columns_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5) + + button_frame = ttk.Frame(columns_frame) + button_frame.pack(fill=tk.X) + ttk.Button(button_frame, text="Alle auswählen", command=self.select_all_columns).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="Alle abwählen", command=self.deselect_all_columns).pack(side=tk.LEFT) + ttk.Button(button_frame, text="Auswahl umkehren", command=self.invert_selection).pack(side=tk.LEFT, padx=5) + + # Spalten-Liste mit Checkboxen + list_frame = ttk.Frame(columns_frame) + list_frame.pack(fill=tk.BOTH, expand=True, pady=5) + + scrollbar = ttk.Scrollbar(list_frame) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + self.columns_listbox = tk.Listbox(list_frame, selectmode=tk.MULTIPLE, + yscrollcommand=scrollbar.set, height=10) + self.columns_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + scrollbar.config(command=self.columns_listbox.yview) + + # Vorschau + preview_frame = ttk.LabelFrame(main_frame, text="Datenvorschau", padding="10") + preview_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=5) + + self.info_label = ttk.Label(preview_frame, text="Keine Datei geladen") + self.info_label.pack() + + # Status und Log + log_frame = ttk.LabelFrame(main_frame, text="Status", padding="10") + log_frame.grid(row=4, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5) + + self.log_text = scrolledtext.ScrolledText(log_frame, height=10, state='disabled') + self.log_text.pack(fill=tk.BOTH, expand=True) + + # Aktionsbuttons + action_frame = ttk.Frame(main_frame) + action_frame.grid(row=5, column=0, sticky=(tk.W, tk.E), pady=10) + + ttk.Button(action_frame, text="Verarbeiten & Speichern", + command=self.process_and_save).pack(side=tk.RIGHT, padx=5) + ttk.Button(action_frame, text="Vorschau", command=self.show_preview).pack(side=tk.RIGHT) + + self.log("CSV-Processor GUI v2.0 gestartet") + self.log(f"Excel-Support: {'Ja' if EXCEL_SUPPORT else 'Nein'}") + self.log(f"ODS-Support: {'Ja' if ODT_SUPPORT else 'Nein'}") + + def log(self, message): + """Nachricht im Log anzeigen""" + self.log_text.config(state='normal') + self.log_text.insert(tk.END, f"{datetime.now().strftime('%H:%M:%S')} - {message}\n") + self.log_text.see(tk.END) + self.log_text.config(state='disabled') + + def browse_file(self): + """Datei zum Öffnen auswählen""" + filetypes = [ + ("Alle unterstützten Dateien", "*.csv *.xlsx *.xls"), + ("CSV Dateien", "*.csv"), + ] + if EXCEL_SUPPORT: + filetypes.append(("Excel Dateien", "*.xlsx *.xls")) + filetypes.append(("Alle Dateien", "*.*")) + + filename = filedialog.askopenfilename( + title="Quelldatei auswählen", + filetypes=filetypes + ) + if filename: + self.file_entry.delete(0, tk.END) + self.file_entry.insert(0, filename) + + def browse_mapping(self): + """Mapping-Datei auswählen""" + filename = filedialog.askopenfilename( + title="Mapping-Datei auswählen", + filetypes=[("JSON Dateien", "*.json"), ("Alle Dateien", "*.*")] + ) + if filename: + self.mapping_entry.delete(0, tk.END) + self.mapping_entry.insert(0, filename) + + def load_file(self): + """Datei laden""" + filepath = self.file_entry.get() + if not filepath: + messagebox.showwarning("Warnung", "Bitte wählen Sie zuerst eine Datei aus.") + return + + path = Path(filepath) + if not path.exists(): + messagebox.showerror("Fehler", f"Datei nicht gefunden: {filepath}") + return + + try: + self.log(f"Lade Datei: {path.name}") + + # Datei laden + file_ext = path.suffix.lower() + if file_ext in ['.xlsx', '.xls']: + self.headers, self.data = self.read_excel(path) + else: + self.headers, self.data = self.read_csv(path) + + # Mapping anwenden + mapping_file = self.mapping_entry.get() + if mapping_file and Path(mapping_file).exists(): + mappings = self.load_column_mappings(Path(mapping_file)) + self.headers, self.data, self.original_names = self.apply_column_mappings( + self.headers, self.data, mappings + ) + self.log(f"Mapping angewendet: {len(mappings)} Spalten umbenannt") + else: + self.original_names = {h: h for h in self.headers} + + # Spalten in Liste anzeigen + self.update_columns_list() + + # Wenn Voreinstellung mit Spaltenauswahl geladen wurde, anwenden + if hasattr(self, 'current_config') and 'selected_columns' in self.current_config: + self.apply_column_selection_from_preset(self.current_config['selected_columns']) + # Config zurücksetzen nach Anwendung + self.current_config = {} + + # Info aktualisieren + self.info_label.config(text=f"Geladen: {len(self.headers)} Spalten, {len(self.data)} Zeilen") + self.log(f"Erfolgreich geladen: {len(self.headers)} Spalten, {len(self.data)} Zeilen") + + # Warnung anzeigen wenn "Leere Spalten entfernen" aktiv ist + if self.remove_empty_cols_var.get(): + self.log("ℹ Leere Spalten werden beim Verarbeiten automatisch entfernt") + + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Laden: {str(e)}") + self.log(f"FEHLER: {str(e)}") + + def read_csv(self, filepath: Path) -> Tuple[List[str], List[Dict]]: + """CSV-Datei lesen""" + encodings = ['utf-8', 'latin-1', 'cp1252'] + + for encoding in encodings: + try: + with open(filepath, 'r', encoding=encoding) as f: + first_line = f.readline() + f.seek(0) + + sniffer = csv.Sniffer() + delimiter = sniffer.sniff(first_line).delimiter + + if self.has_header_var.get(): + reader = csv.DictReader(f, delimiter=delimiter) + headers = list(reader.fieldnames) + data = list(reader) + else: + reader = csv.reader(f, delimiter=delimiter) + rows = list(reader) + if rows: + headers = [f"Spalte_{i+1}" for i in range(len(rows[0]))] + data = [] + for row in rows: + row_dict = {headers[i]: row[i] if i < len(row) else '' + for i in range(len(headers))} + data.append(row_dict) + else: + headers = [] + data = [] + + return headers, data + except UnicodeDecodeError: + continue + except Exception: + continue + + raise Exception(f"Konnte Datei {filepath} nicht lesen") + + def read_excel(self, filepath: Path) -> Tuple[List[str], List[Dict]]: + """Excel-Datei lesen""" + if not EXCEL_SUPPORT: + raise Exception("Excel-Support nicht verfügbar. Installieren Sie: pip install openpyxl") + + wb = openpyxl.load_workbook(filepath, data_only=True) + ws = wb.active + + data = [] + headers = None + + for i, row in enumerate(ws.iter_rows(values_only=True)): + if i == 0 and self.has_header_var.get(): + headers = [str(cell) if cell is not None else f"Spalte_{j+1}" + for j, cell in enumerate(row)] + else: + if headers is None: + headers = [f"Spalte_{j+1}" for j in range(len(row))] + + row_dict = {} + for j, cell in enumerate(row): + if j < len(headers): + row_dict[headers[j]] = str(cell) if cell is not None else '' + data.append(row_dict) + + return headers, data + + def load_column_mappings(self, mapping_file: Path) -> Dict[str, str]: + """Spaltennamen-Zuordnungen aus JSON laden""" + try: + with open(mapping_file, 'r', encoding='utf-8') as f: + return json.load(f) + except: + return {} + + def apply_column_mappings(self, headers: List[str], data: List[Dict], + mappings: Dict[str, str]) -> Tuple[List[str], List[Dict], Dict[str, str]]: + """Spaltennamen umbenennen""" + new_headers = [mappings.get(h, h) for h in headers] + original_names = {new_h: old_h for old_h, new_h in zip(headers, new_headers)} + + new_data = [] + for row in data: + new_row = {} + for old_h, new_h in zip(headers, new_headers): + new_row[new_h] = row.get(old_h, '') + new_data.append(new_row) + + return new_headers, new_data, original_names + + def update_columns_list(self): + """Spaltenliste aktualisieren""" + self.columns_listbox.delete(0, tk.END) + self.selected_columns = set(self.headers) # Alle initial ausgewählt + + for i, header in enumerate(self.headers): + orig = self.original_names.get(header, header) + if orig != header: + display = f"☑ {orig} → {header}" + else: + display = f"☑ {header}" + + self.columns_listbox.insert(tk.END, display) + self.columns_listbox.selection_set(i) + + def select_all_columns(self): + """Alle Spalten auswählen""" + self.columns_listbox.selection_set(0, tk.END) + self.selected_columns = set(self.headers) + + def deselect_all_columns(self): + """Alle Spalten abwählen""" + self.columns_listbox.selection_clear(0, tk.END) + self.selected_columns = set() + + def invert_selection(self): + """Auswahl umkehren""" + for i in range(len(self.headers)): + if self.columns_listbox.selection_includes(i): + self.columns_listbox.selection_clear(i) + else: + self.columns_listbox.selection_set(i) + + def get_selected_columns(self) -> List[str]: + """Aktuell ausgewählte Spalten ermitteln""" + selected_indices = self.columns_listbox.curselection() + return [self.headers[i] for i in selected_indices] + + def process_data(self) -> Tuple[List[str], List[Dict]]: + """Daten verarbeiten""" + if not self.headers or not self.data: + raise Exception("Keine Daten geladen") + + headers = self.headers[:] + data = self.data[:] + + # 1. Leere Zeilen entfernen + if self.remove_empty_rows_var.get(): + original_count = len(data) + data = [row for row in data if any(str(v).strip() for v in row.values())] + removed = original_count - len(data) + if removed > 0: + self.log(f"Leere Zeilen entfernt: {removed}") + + # 2. Leere Spalten entfernen (VOR der Spaltenauswahl!) + if self.remove_empty_cols_var.get(): + non_empty_headers = [] + for header in headers: + if any(row.get(header, '').strip() for row in data): + non_empty_headers.append(header) + + removed = len(headers) - len(non_empty_headers) + if removed > 0: + self.log(f"Leere Spalten entfernt: {removed}") + # Zeige welche Spalten entfernt wurden + removed_cols = [h for h in headers if h not in non_empty_headers] + for col in removed_cols[:5]: + orig = self.original_names.get(col, col) + if orig != col: + self.log(f" - {orig} → {col} (leer)") + else: + self.log(f" - {col} (leer)") + if len(removed_cols) > 5: + self.log(f" ... und {len(removed_cols)-5} weitere") + + headers = non_empty_headers + data = [{h: row.get(h, '') for h in headers} for row in data] + + # 3. Spaltenauswahl anwenden (arbeitet mit nicht-leeren Spalten) + selected = self.get_selected_columns() + + # Nur Spalten verwenden, die noch existieren (nicht leer) UND ausgewählt sind + final_columns = [h for h in headers if h in selected] + + if not final_columns: + raise Exception("Keine Spalten ausgewählt oder alle ausgewählten Spalten sind leer") + + # Zeige Info über übersprungene Spalten + skipped_empty = [h for h in selected if h not in headers] + skipped_deselected = [h for h in headers if h not in selected] + + if skipped_empty: + self.log(f"Übersprungene Spalten (leer): {len(skipped_empty)}") + if skipped_deselected: + self.log(f"Abgewählte Spalten (rot): {len(skipped_deselected)}") + + headers = final_columns + data = [{h: row.get(h, '') for h in headers} for row in data] + + self.log(f"Finale Verarbeitung: {len(headers)} Spalten, {len(data)} Zeilen") + return headers, data + + def show_preview(self): + """Vorschau der verarbeiteten Daten""" + try: + headers, data = self.process_data() + + preview_window = tk.Toplevel(self.root) + preview_window.title("Datenvorschau") + preview_window.geometry("800x400") + + frame = ttk.Frame(preview_window, padding="10") + frame.pack(fill=tk.BOTH, expand=True) + + # Treeview für Tabelle + tree = ttk.Treeview(frame, columns=headers, show='headings') + + for col in headers: + tree.heading(col, text=col) + tree.column(col, width=100) + + for row in data[:100]: # Max 100 Zeilen + values = [row.get(col, '') for col in headers] + tree.insert('', tk.END, values=values) + + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=tree.yview) + tree.configure(yscrollcommand=scrollbar.set) + + tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + info_label = ttk.Label(preview_window, + text=f"Zeige erste {min(len(data), 100)} von {len(data)} Zeilen") + info_label.pack(pady=5) + + except Exception as e: + messagebox.showerror("Fehler", f"Fehler bei Vorschau: {str(e)}") + + def process_and_save(self): + """Daten verarbeiten und speichern""" + try: + headers, data = self.process_data() + + # Zieldatei wählen + output_format = self.format_var.get() + + filetypes = [] + if output_format == 'csv': + filetypes = [("CSV Dateien", "*.csv")] + elif output_format == 'xlsx': + filetypes = [("Excel Dateien", "*.xlsx")] + elif output_format == 'ods': + filetypes = [("ODS Dateien", "*.ods")] + filetypes.append(("Alle Dateien", "*.*")) + + filename = filedialog.asksaveasfilename( + title="Datei speichern", + defaultextension=f".{output_format}", + filetypes=filetypes + ) + + if not filename: + return + + # Speichern + target_file = Path(filename) + + if output_format == 'csv': + self.write_csv(target_file, headers, data) + elif output_format == 'xlsx': + self.write_excel(target_file, headers, data) + elif output_format == 'ods': + self.write_ods(target_file, headers, data) + + self.log(f"Datei gespeichert: {target_file.name}") + messagebox.showinfo("Erfolg", f"Datei erfolgreich gespeichert:\n{target_file}") + + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Speichern: {str(e)}") + self.log(f"FEHLER: {str(e)}") + + def write_csv(self, filepath: Path, headers: List[str], data: List[Dict]): + """CSV-Datei schreiben""" + with open(filepath, 'w', encoding='utf-8', newline='') as f: + writer = csv.DictWriter(f, fieldnames=headers, delimiter=';') + writer.writeheader() + writer.writerows(data) + + def write_excel(self, filepath: Path, headers: List[str], data: List[Dict]): + """Excel-Datei schreiben""" + if not EXCEL_SUPPORT: + raise Exception("Excel-Support nicht verfügbar") + + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "Daten" + + # Kopfzeile + for col_num, header in enumerate(headers, 1): + cell = ws.cell(row=1, column=col_num, value=header) + cell.font = Font(bold=True) + cell.fill = PatternFill(start_color="CCCCCC", end_color="CCCCCC", fill_type="solid") + + # Daten + for row_num, row_data in enumerate(data, 2): + for col_num, header in enumerate(headers, 1): + ws.cell(row=row_num, column=col_num, value=row_data.get(header, '')) + + wb.save(filepath) + + def write_ods(self, filepath: Path, headers: List[str], data: List[Dict]): + """ODS-Datei schreiben""" + if not ODT_SUPPORT: + raise Exception("ODS-Support nicht verfügbar") + + doc = OpenDocumentSpreadsheet() + table = Table(name="Daten") + + # Kopfzeile + row = TableRow() + for header in headers: + cell = TableCell() + cell.addElement(P(text=header)) + row.addElement(cell) + table.addElement(row) + + # Daten + for row_data in data: + row = TableRow() + for header in headers: + cell = TableCell() + cell.addElement(P(text=str(row_data.get(header, '')))) + row.addElement(cell) + table.addElement(row) + + doc.spreadsheet.addElement(table) + doc.save(filepath) + + def load_preset(self): + """Voreinstellung laden""" + presets = [f.stem for f in self.config_dir.glob("*.json")] + + if not presets: + messagebox.showinfo("Info", "Keine Voreinstellungen vorhanden.") + return + + dialog = PresetDialog(self.root, "Voreinstellung laden", presets) + if dialog.result: + preset_file = self.config_dir / f"{dialog.result}.json" + try: + with open(preset_file, 'r', encoding='utf-8') as f: + config = json.load(f) + + # Einstellungen anwenden + if 'has_header' in config: + self.has_header_var.set(config['has_header']) + if 'remove_empty_rows' in config: + self.remove_empty_rows_var.set(config['remove_empty_rows']) + if 'remove_empty' in config: + self.remove_empty_cols_var.set(config['remove_empty']) + + # Mapping-Datei vorbelegen + if 'mapping_file' in config and config['mapping_file']: + self.mapping_entry.delete(0, tk.END) + self.mapping_entry.insert(0, config['mapping_file']) + self.log(f"Mapping-Datei vorbelegt: {config['mapping_file']}") + + if 'output_format' in config: + self.format_var.set(config['output_format']) + + # Quelldatei vorbelegen (falls gespeichert) + if 'source_file' in config and config['source_file']: + source_path = Path(config['source_file']) + if source_path.exists(): + self.file_entry.delete(0, tk.END) + self.file_entry.insert(0, config['source_file']) + self.log(f"Quelldatei vorbelegt: {source_path.name}") + + # Datei automatisch laden + self.load_file() + + # Spaltenauswahl anwenden (nach dem Laden!) + if 'selected_columns' in config and self.headers: + self.apply_column_selection_from_preset(config['selected_columns']) + else: + self.log(f"⚠ Gespeicherte Quelldatei nicht gefunden: {config['source_file']}") + elif 'selected_columns' in config: + # Keine Quelldatei, aber Spaltenauswahl gespeichert + # Wird angewendet sobald Datei geladen wird + self.current_config = config + self.log(f"Spaltenauswahl wird angewendet sobald Datei geladen wird") + + self.log(f"Voreinstellung geladen: {dialog.result}") + messagebox.showinfo("Erfolg", + f"Voreinstellung '{dialog.result}' geladen.\n\n" + "Hinweis: Leere Spalten werden beim Verarbeiten\n" + "automatisch übersprungen (vor Spaltenauswahl).") + + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Laden: {str(e)}") + + def apply_column_selection_from_preset(self, selected_columns: List[str]): + """Spaltenauswahl aus Voreinstellung anwenden""" + saved_selection = set(selected_columns) + + # Alle abwählen + self.columns_listbox.selection_clear(0, tk.END) + + # Nur gespeicherte Spalten auswählen (die existieren) + selected_count = 0 + for i, header in enumerate(self.headers): + if header in saved_selection: + self.columns_listbox.selection_set(i) + selected_count += 1 + + skipped_count = len(saved_selection) - selected_count + + self.log(f"Spaltenauswahl angewendet: {selected_count} Spalten ausgewählt") + if skipped_count > 0: + self.log(f" ⚠ {skipped_count} Spalten nicht verfügbar (leer oder nicht vorhanden)") + + # Info-Text anzeigen + self.info_label.config( + text=f"Geladen: {len(self.headers)} Spalten, {len(self.data)} Zeilen | " + f"Ausgewählt: {selected_count} Spalten" + ) + + def save_preset(self): + """Voreinstellung speichern""" + name = simpledialog.askstring("Voreinstellung speichern", "Name der Voreinstellung:") + if not name: + return + + # Fragen ob Quelldatei mitgespeichert werden soll + save_source = False + if self.file_entry.get(): + save_source = messagebox.askyesno( + "Quelldatei speichern?", + "Möchten Sie den Pfad zur Quelldatei in der Voreinstellung speichern?\n\n" + "Ja = Datei wird beim Laden der Voreinstellung automatisch geladen\n" + "Nein = Nur Einstellungen werden gespeichert" + ) + + config = { + 'has_header': self.has_header_var.get(), + 'remove_empty_rows': self.remove_empty_rows_var.get(), + 'remove_empty': self.remove_empty_cols_var.get(), + 'mapping_file': self.mapping_entry.get(), + 'output_format': self.format_var.get(), + 'selected_columns': self.get_selected_columns() + } + + # Quelldatei optional speichern + if save_source: + config['source_file'] = self.file_entry.get() + + preset_file = self.config_dir / f"{name}.json" + try: + with open(preset_file, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=2, ensure_ascii=False) + + info_text = f"Voreinstellung '{name}' gespeichert.\n\n" + info_text += f"Enthält:\n" + info_text += f"- Einstellungen: Ja\n" + info_text += f"- Mapping: {'Ja' if config['mapping_file'] else 'Nein'}\n" + info_text += f"- Quelldatei: {'Ja' if save_source else 'Nein'}\n" + info_text += f"- Spaltenauswahl: {len(config['selected_columns'])} Spalten" + + self.log(f"Voreinstellung gespeichert: {name}") + messagebox.showinfo("Erfolg", info_text) + + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Speichern: {str(e)}") + + def manage_presets(self): + """Voreinstellungen verwalten""" + PresetManagerWindow(self.root, self.config_dir) + + def show_about(self): + """Über-Dialog""" + messagebox.showinfo( + "Über CSV-Processor", + "CSV-Processor v2.0\n\n" + "Professionelle CSV/Excel-Verarbeitung\n\n" + f"Excel-Support: {'Ja' if EXCEL_SUPPORT else 'Nein'}\n" + f"ODS-Support: {'Ja' if ODT_SUPPORT else 'Nein'}" + ) + + +class PresetDialog(simpledialog.Dialog): + """Dialog zur Auswahl einer Voreinstellung""" + def __init__(self, parent, title, presets): + self.presets = presets + self.result = None + super().__init__(parent, title) + + def body(self, master): + tk.Label(master, text="Voreinstellung auswählen:").pack(pady=5) + + self.listbox = tk.Listbox(master) + for preset in self.presets: + self.listbox.insert(tk.END, preset) + self.listbox.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + return self.listbox + + def apply(self): + selection = self.listbox.curselection() + if selection: + self.result = self.presets[selection[0]] + + +class PresetManagerWindow: + """Fenster zur Verwaltung von Voreinstellungen""" + def __init__(self, parent, config_dir): + self.config_dir = config_dir + self.window = tk.Toplevel(parent) + self.window.title("Voreinstellungen verwalten") + self.window.geometry("400x300") + + frame = ttk.Frame(self.window, padding="10") + frame.pack(fill=tk.BOTH, expand=True) + + tk.Label(frame, text="Voreinstellungen:").pack(anchor=tk.W) + + self.listbox = tk.Listbox(frame) + self.listbox.pack(fill=tk.BOTH, expand=True, pady=5) + + button_frame = ttk.Frame(frame) + button_frame.pack(fill=tk.X) + + ttk.Button(button_frame, text="Löschen", command=self.delete_preset).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="Schließen", command=self.window.destroy).pack(side=tk.RIGHT) + + self.refresh_list() + + def refresh_list(self): + """Liste aktualisieren""" + self.listbox.delete(0, tk.END) + for preset in self.config_dir.glob("*.json"): + self.listbox.insert(tk.END, preset.stem) + + def delete_preset(self): + """Voreinstellung löschen""" + selection = self.listbox.curselection() + if not selection: + messagebox.showwarning("Warnung", "Bitte wählen Sie eine Voreinstellung aus.") + return + + preset_name = self.listbox.get(selection[0]) + if messagebox.askyesno("Bestätigung", f"Voreinstellung '{preset_name}' wirklich löschen?"): + preset_file = self.config_dir / f"{preset_name}.json" + preset_file.unlink() + self.refresh_list() + messagebox.showinfo("Erfolg", f"Voreinstellung '{preset_name}' gelöscht.") + + +def main(): + root = tk.Tk() + app = CSVProcessorGUI(root) + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/dist/csv-processor-debian.deb b/dist/csv-processor-debian.deb new file mode 100644 index 0000000..07b2089 Binary files /dev/null and b/dist/csv-processor-debian.deb differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fb8704a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +# CSV-Processor Abhängigkeiten + +# Excel-Support (XLSX/XLS) +openpyxl>=3.0.0 + +# OpenDocument-Support (ODS) +odfpy>=1.4.0