From a4c5ed3308604dda77a030fe085e65227fdb5a7a Mon Sep 17 00:00:00 2001 From: "sebastian.zell" Date: Sun, 19 Oct 2025 14:47:35 +0000 Subject: [PATCH] Dateien nach "csv_processor" hochladen --- csv_processor/anleitung.txt | 891 ++++++++++++++ csv_processor/csv-processor-debian.deb | Bin 0 -> 23418 bytes csv_processor/csv_processor.py | 1546 ++++++++++++++++++++++++ csv_processor/csv_processor_gui.py | 829 +++++++++++++ csv_processor/mietermerkmale.json | 504 ++++++++ 5 files changed, 3770 insertions(+) create mode 100644 csv_processor/anleitung.txt create mode 100644 csv_processor/csv-processor-debian.deb create mode 100644 csv_processor/csv_processor.py create mode 100644 csv_processor/csv_processor_gui.py create mode 100644 csv_processor/mietermerkmale.json diff --git a/csv_processor/anleitung.txt b/csv_processor/anleitung.txt new file mode 100644 index 0000000..b257ed0 --- /dev/null +++ b/csv_processor/anleitung.txt @@ -0,0 +1,891 @@ +# CSV-Processor v2.0 - Benutzerhandbuch + +## Inhaltsverzeichnis + +1. [Überblick](#überblick) +2. [Installation](#installation) +3. [Programmversionen](#programmversionen) +4. [CLI-Version: Schritt-für-Schritt](#cli-version) +5. [GUI-Version: Schritt-für-Schritt](#gui-version) +6. [Voreinstellungen nutzen](#voreinstellungen-nutzen) +7. [Mapping-Dateien erstellen](#mapping-dateien-erstellen) +8. [Häufige Anwendungsfälle](#häufige-anwendungsfälle) +9. [Tipps & Tricks](#tipps--tricks) +10. [Fehlerbehebung](#fehlerbehebung) + +--- + +## Überblick + +Der CSV-Processor ist ein professionelles Werkzeug zur Verarbeitung und Konvertierung von CSV- und Excel-Dateien. Er ermöglicht: + +✅ **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 + +--- + +## Installation + +Das Programm wurde bereits auf Ihrem System installiert. Falls nicht, wenden Sie sich an die IT. + +### Programme starten: + +**CLI-Version (Terminal):** +```bash +csv-processor-cli +``` + +**GUI-Version (Grafisch):** +- Im Anwendungsmenü unter "Office" → "CSV-Processor" +- Oder im Terminal: `csv-processor-gui` + +--- + +## Programmversionen + +### CLI-Version (Terminal) +- **Für wen:** Fortgeschrittene Benutzer, Automatisierung +- **Vorteile:** Schnell, scriptfähig, volle Kontrolle +- **Start:** `csv-processor-cli` + +### GUI-Version (Grafisch) +- **Für wen:** Alle Benutzer, besonders Einsteiger +- **Vorteile:** Intuitiv, visuelle Vorschau, einfache Bedienung +- **Start:** `csv-processor-gui` oder über Anwendungsmenü + +**Empfehlung:** Für tägliche Arbeit die GUI-Version, für wiederkehrende Aufgaben mit Voreinstellungen beide geeignet. + +--- + +## CLI-Version: Schritt-für-Schritt + +### Programmstart + +```bash +csv-processor-cli +``` + +Sie sehen das Hauptmenü: +``` +╔═══════════════════════════════════════════════════════╗ +║ CSV-PROCESSOR v2.0 - Dateiverarbeitung ║ +╚═══════════════════════════════════════════════════════╝ + +Verfügbare Features: + Import: CSV, Excel (XLSX/XLS) + Export: CSV, Excel (XLSX), OpenDocument (ODS) + +══════════════════════════════════════════════════════════ + HAUPTMENÜ +══════════════════════════════════════════════════════════ + 1. Neue Datei verarbeiten + 2. Voreinstellungen verwalten + 0. Beenden + +Ihre Wahl: +``` + +### Erste Verwendung (ohne Voreinstellung) + +#### Schritt 1: Datei verarbeiten wählen +``` +Ihre Wahl: 1 +``` + +#### Schritt 2: Keine Voreinstellung +``` +Möchten Sie eine Voreinstellung laden? [j/n]: n +``` + +#### Schritt 3: Quelldatei angeben +``` +Pfad zur Quelldatei (CSV/XLSX/XLS/ODS): /home/benutzer/Downloads/daten.csv +``` + +**Bei CSV:** Sie sehen eine interaktive Konfiguration: +``` +══════════════════════════════════════════════════════════ + CSV-IMPORT KONFIGURATION +══════════════════════════════════════════════════════════ + +Datei: daten.csv +Erkanntes Encoding: utf-8 +Erkannter Delimiter: Semikolon (;) +Erkanntes Quoting: MINIMAL + +────────────────────────────────────────────────────────── + VORSCHAU DER ERSTEN ZEILEN: +────────────────────────────────────────────────────────── + +Zeile 1: + Rohtext: Name;Betrag;Datum;Kategorie... + Geparst: 4 Felder + Feld 1: 'Name' + Feld 2: 'Betrag' + ... + +────────────────────────────────────────────────────────── + AKTUELLE EINSTELLUNGEN: +────────────────────────────────────────────────────────── + 1. Encoding: utf-8 + 2. Delimiter: Semikolon (;) + 3. Quoting: MINIMAL + + 0. ✓ Diese Einstellungen verwenden +────────────────────────────────────────────────────────── + +Was möchten Sie ändern? [0-3]: 0 +``` + +**Tipp:** Prüfen Sie die Vorschau! Wenn die Felder korrekt getrennt sind, drücken Sie `0`. + +#### Schritt 4: Kopfzeile bestätigen +``` +Hat die CSV-Datei eine Kopfzeile mit Spaltennamen? [j/n]: j +``` + +#### Schritt 5: Mapping anwenden (optional) +``` +Möchten Sie Spaltennamen umbenennen? [j/n]: j +Pfad zur Mapping-JSON-Datei: /usr/share/csv-processor/mietermerkmale.json +✓ 5 Spaltennamen umbenannt +``` + +#### Schritt 6: Leere Zeilen +``` +Sollen komplett leere Zeilen entfernt werden? [j/n]: j +✓ 3 leere Zeilen entfernt +``` + +#### Schritt 7: Unvollständige Zeilen (optional) +``` +Möchten Sie Zeilen mit zu wenig Informationen analysieren/entfernen? [j/n]: n +``` + +#### Schritt 8: Leere Spalten +``` +Sollen leere Spalten entfernt werden? [j/n]: j +✓ 2 leere Spalten entfernt +``` + +**Wichtig:** Leere Spalten werden VOR der Spaltenauswahl entfernt! + +#### Schritt 9: Spaltenauswahl + +Sie sehen eine Liste aller verfügbaren (nicht-leeren) Spalten: + +``` +══════════════════════════════════════════════════════════ + SPALTENAUSWAHL +══════════════════════════════════════════════════════════ +● = Angewählt | ○ = Abgewählt +────────────────────────────────────────────────────────── + ● 1. btr → Betrag + ● 2. dat → Datum + ● 3. kto → Kontonummer + ● 4. empf → Empfänger + ● 5. verw → Verwendungszweck +────────────────────────────────────────────────────────── +Aktuell ausgewählt: 5 von 5 Spalten +══════════════════════════════════════════════════════════ + +Optionen: + [Nummern] - Spalten an/abwählen (z.B. '1,2,3' oder '1-5,10-15') + + [Nummern] - Spalten ANwählen (z.B. '+1,2,3') + - [Nummern] - Spalten ABwählen (z.B. '-1,2,3') + alle - Alle Spalten anwählen + keine - Alle Spalten abwählen + q - Auswahl beenden und fortfahren + +Ihre Wahl: +``` + +**Beispiele:** +- `3` - Spalte 3 umschalten (an→ab oder ab→an) +- `-3,5` - Spalten 3 und 5 ABwählen (rot) +- `+1-10` - Spalten 1 bis 10 ANwählen (grün) +- `1-5,10-15` - Spalten 1-5 und 10-15 umschalten +- `q` - Fertig + +``` +Ihre Wahl: -3 +✓ 1 Spalte(n) abgewählt + +Ihre Wahl: q +``` + +#### Schritt 10: Kopf-/Fußzeile (optional) +``` +Möchten Sie eine Kopf- oder Fußzeile hinzufügen? [j/n]: j +Kopfzeile (Enter für keine): Export vom 19.10.2025 +Fußzeile (Enter für keine): Erstellt mit CSV-Processor +``` + +#### Schritt 11: Sortierung (optional) +``` +Möchten Sie die Daten sortieren? [j/n]: j + +Verfügbare Spalten zum Sortieren: + 1. Betrag + 2. Datum + 3. Empfänger + 4. Verwendungszweck + +Nummer der Spalte: 2 + +Datentyp für Sortierung: + 1. Text (string) + 2. Datum + 3. Zeit + 4. Dezimalzahl + +Ihre Wahl [1-4]: 2 +✓ Daten nach 'Datum' sortiert +``` + +#### Schritt 12: Ausgabeformat wählen +``` +Verfügbare Ausgabeformate: + 1. CSV + 2. Excel (XLSX) + 3. OpenDocument (ODS) + +Ihre Wahl: 1 +``` + +**Bei CSV-Export:** +``` +CSV-EXPORT KONFIGURATION + +Delimiter wählen: + 1. Semikolon (;) - Standard für deutsche Excel-Versionen + 2. Komma (,) - Internationaler Standard + 3. Tab - Gut für Import in andere Programme + +Ihre Wahl [1-3]: 1 + +Quoting (Anführungszeichen) wählen: + 1. MINIMAL - Nur Felder mit Sonderzeichen (empfohlen) + 2. ALLE - Alle Felder in Anführungszeichen + 3. NICHT_NUMERISCH - Nur Text-Felder + 4. KEINE - Keine Anführungszeichen + +Ihre Wahl [1-4]: 1 +``` + +#### Schritt 13: Zieldatei speichern +``` +Name/Pfad der Zieldatei (ohne Endung): ausgabe +✓ CSV-Datei erfolgreich gespeichert: ausgabe.csv +``` + +#### Schritt 14: Als Voreinstellung speichern +``` +Möchten Sie diese Einstellungen als Voreinstellung speichern? [j/n]: j +Name für die Voreinstellung: meine_standard_export +✓ Voreinstellung 'meine_standard_export' gespeichert. +``` + +**Fertig!** Sie sind zurück im Hauptmenü. + +### Mit Voreinstellung arbeiten + +Beim nächsten Mal: + +``` +Ihre Wahl: 1 + +Verfügbare Voreinstellungen: + 1. meine_standard_export + 2. mietermerkmale + +Möchten Sie eine Voreinstellung laden? [j/n]: j +Nummer oder Name der Voreinstellung: 1 +✓ Voreinstellung 'meine_standard_export' geladen. +``` + +Dann nur noch: +1. Quelldatei angeben +2. Zieldatei angeben +3. **Fertig!** + +Alle anderen Schritte werden automatisch durchgeführt! 🚀 + +--- + +## GUI-Version: Schritt-für-Schritt + +### Programmstart + +```bash +csv-processor-gui +``` + +Oder über das Anwendungsmenü: **Office** → **CSV-Processor** + +### Hauptfenster + +![GUI Overview](diagram) + +``` +┌─────────────────────────────────────────────────────────┐ +│ Datei Voreinstellungen Hilfe │ +├─────────────────────────────────────────────────────────┤ +│ ┌─ Quelldatei ────────────────────────────────────────┐ │ +│ │ Datei: [________________] [Durchsuchen] [Laden] │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Verarbeitungseinstellungen ─────────────────────────┐│ +│ │ ☑ Datei hat Kopfzeile ││ +│ │ ☑ Leere Zeilen entfernen ││ +│ │ ☑ Leere Spalten entfernen (vor Spaltenauswahl) ││ +│ │ Mapping-Datei: [________________] [...] ││ +│ │ Ausgabeformat: ⦿ CSV ○ Excel ○ ODS ││ +│ └─────────────────────────────────────────────────────┘│ +│ │ +│ ┌─ Spaltenauswahl ─────────────────────────────────────┐│ +│ │ [Alle auswählen] [Alle abwählen] [Auswahl umkehren] ││ +│ │ ┌──────────────────────────────────────────────────┐││ +│ │ │ ☑ btr → Betrag │││ +│ │ │ ☑ dat → Datum │││ +│ │ │ ☑ empf → Empfänger │││ +│ │ │ ☐ alte_spalte (nicht gemappt) │││ +│ │ └──────────────────────────────────────────────────┘││ +│ └─────────────────────────────────────────────────────┘│ +│ │ +│ ┌─ Status ──────────────────────────────────────────────┐│ +│ │ 12:34:56 - Datei geladen: 5 Spalten, 100 Zeilen ││ +│ │ 12:34:57 - Leere Spalten entfernt: 2 ││ +│ └─────────────────────────────────────────────────────┘│ +│ │ +│ [Vorschau] [Verarbeiten & Speichern] │ +└─────────────────────────────────────────────────────────┘ +``` + +### Schritt-für-Schritt Anleitung + +#### 1. Datei laden + +**Option A: Durchsuchen** +1. Klick auf **[Durchsuchen]** +2. Datei auswählen (CSV oder Excel) +3. **[Öffnen]** klicken + +**Option B: Pfad eingeben** +1. Pfad ins Feld eingeben: `/home/benutzer/Downloads/daten.csv` +2. **[Laden]** klicken + +**Ergebnis:** +``` +Status: +12:34:56 - Lade Datei: daten.csv +12:34:56 - Erfolgreich geladen: 8 Spalten, 234 Zeilen +``` + +#### 2. Einstellungen prüfen + +Alle wichtigen Einstellungen sind als Checkboxen verfügbar: + +- **☑ Datei hat Kopfzeile** - Fast immer aktiviert +- **☑ Leere Zeilen entfernen** - Empfohlen +- **☑ Leere Spalten entfernen** - Empfohlen (passiert vor Spaltenauswahl!) + +#### 3. Mapping-Datei wählen (optional) + +1. Klick auf **[...]** neben "Mapping-Datei" +2. Datei auswählen (z.B. `mietermerkmale.json`) +3. **[Öffnen]** +4. Datei neu laden mit **[Laden]** + +**Ergebnis:** Spaltennamen werden umbenannt, z.B.: +- `btr → Betrag` +- `dat → Datum` + +#### 4. Spalten auswählen + +Im Bereich "Spaltenauswahl" sehen Sie alle verfügbaren Spalten. + +**Alle Spalten sind standardmäßig ausgewählt (☑).** + +**Spalten abwählen:** +- Klick auf Spalte → wird abgewählt (☐) +- Klick nochmal → wird wieder ausgewählt (☑) + +**Schnellaktionen:** +- **[Alle auswählen]** - Alle Spalten auf ☑ +- **[Alle abwählen]** - Alle Spalten auf ☐ +- **[Auswahl umkehren]** - ☑↔☐ + +**Mehrfachauswahl:** +- **Strg + Klick** - Mehrere Spalten einzeln auswählen +- **Shift + Klick** - Bereich auswählen + +**Wichtig:** Nur grüne Spalten (☑) werden exportiert. Zusätzlich werden leere Spalten automatisch übersprungen! + +#### 5. Vorschau (optional) + +Klick auf **[Vorschau]** um die verarbeiteten Daten zu sehen: + +``` +┌─ Datenvorschau ──────────────────────────────────────┐ +│ Betrag │ Datum │ Empfänger │ Verwendung │ +├───────────┼────────────┼──────────────┼─────────────┤ +│ 150.50 │ 01.10.2025 │ Müller GmbH │ Miete │ +│ 75.00 │ 03.10.2025 │ Stadtwerke │ Strom │ +│ ... │ ... │ ... │ ... │ +└──────────────────────────────────────────────────────┘ +Zeige erste 100 von 234 Zeilen + [Schließen] +``` + +#### 6. Verarbeiten & Speichern + +Klick auf **[Verarbeiten & Speichern]** + +1. Dateiname eingeben: `ausgabe` +2. Speicherort wählen +3. **[Speichern]** + +**Status-Meldungen:** +``` +12:45:10 - Leere Zeilen entfernt: 3 +12:45:10 - Leere Spalten entfernt: 2 +12:45:10 - - alte_spalte1 (leer) +12:45:10 - - temp_field (leer) +12:45:11 - Übersprungene Spalten (leer): 1 +12:45:11 - Abgewählte Spalten (rot): 2 +12:45:11 - Finale Verarbeitung: 5 Spalten, 231 Zeilen +12:45:12 - Datei gespeichert: ausgabe.csv +``` + +**Erfolgsmeldung:** +``` +┌─ Erfolg ─────────────────────────────────┐ +│ │ +│ Datei erfolgreich gespeichert: │ +│ /home/benutzer/Documents/ausgabe.csv │ +│ │ +│ [OK] │ +└───────────────────────────────────────────┘ +``` + +#### 7. Voreinstellung speichern + +**Menü** → **Voreinstellungen** → **Speichern** + +``` +┌─ Voreinstellung speichern ──────────────┐ +│ Name der Voreinstellung: │ +│ [meine_export_einstellung_________] │ +│ │ +│ [OK] [Abbrechen] │ +└──────────────────────────────────────────┘ +``` + +Name eingeben → **[OK]** + +``` +Status: +12:50:00 - Voreinstellung gespeichert: meine_export_einstellung +``` + +### Voreinstellung laden + +**Menü** → **Voreinstellungen** → **Laden** + +``` +┌─ Voreinstellung laden ──────────────────┐ +│ Voreinstellung auswählen: │ +│ ┌─────────────────────────────────────┐ │ +│ │ meine_export_einstellung │ │ +│ │ mietermerkmale │ │ +│ │ │ │ +│ └─────────────────────────────────────┘ │ +│ [OK] [Abbrechen] │ +└──────────────────────────────────────────┘ +``` + +Voreinstellung auswählen → **[OK]** + +**Alle Einstellungen werden automatisch geladen!** + +--- + +## Voreinstellungen nutzen + +### Was wird gespeichert? + +Eine Voreinstellung enthält: +- ☑ Kopfzeile ja/nein +- ☑ Leere Zeilen entfernen +- ☑ Leere Spalten entfernen +- 📄 Pfad zur Mapping-Datei +- 📊 Spaltenauswahl +- 💾 Ausgabeformat (CSV/Excel/ODS) +- 🔧 CSV-Export-Einstellungen (Delimiter, Quoting) +- 📏 Sortierung (Spalte & Datentyp) + +### Wann sind Voreinstellungen sinnvoll? + +✅ **Monatliche Berichte** - Immer gleiche Verarbeitung +✅ **Standard-Exporte** - Wiederkehrende Aufgaben +✅ **Team-Workflows** - Einheitliche Formate +✅ **Verschiedene Datenquellen** - Unterschiedliche Mappings + +### Voreinstellung bearbeiten (nur CLI) + +**Hauptmenü** → **2** (Voreinstellungen verwalten) + +``` +╔═══════════════════════════════════════════════════════╗ +║ VOREINSTELLUNGEN VERWALTEN ║ +╚═══════════════════════════════════════════════════════╝ + +Verfügbare Voreinstellungen: + 1. meine_export_einstellung + 2. mietermerkmale + +Optionen: + 1. Voreinstellung bearbeiten + 2. Voreinstellung löschen + 0. Zurück zum Hauptmenü + +Ihre Wahl: 1 +``` + +**Voreinstellung bearbeiten:** +1. Voreinstellung auswählen +2. Alle Schritte durchgehen (mit aktuellen Werten vorausgefüllt) +3. Bei Schritt 6 (Spaltenauswahl): Beispieldatei laden +4. Spalten wie gewohnt aus-/abwählen +5. Am Ende: Überschreiben oder neu speichern + +--- + +## Mapping-Dateien erstellen + +### Was ist ein Mapping? + +Ein Mapping übersetzt technische Spaltennamen in lesbare Namen: + +``` +"btr" → "Betrag" +"dat" → "Datum" +"empf" → "Empfänger" +``` + +### Format + +Mapping-Dateien sind JSON-Dateien: + +```json +{ + "btr": "Betrag", + "dat": "Datum", + "kto": "Kontonummer", + "blz": "Bankleitzahl", + "empf": "Empfänger", + "verw": "Verwendungszweck" +} +``` + +### Eigenes Mapping erstellen + +1. Texteditor öffnen (z.B. `nano`, `gedit`) +2. JSON-Format verwenden (siehe oben) +3. Als `.json` speichern (z.B. `mein_mapping.json`) + +**Beispiel:** +```bash +nano ~/Dokumente/bank_mapping.json +``` + +Inhalt: +```json +{ + "alt1": "Neuer Name 1", + "alt2": "Neuer Name 2", + "xyz": "Beschreibung XYZ" +} +``` + +Speichern: **Strg+O**, **Enter**, **Strg+X** + +### Mitgeliefertes Mapping + +Das System enthält bereits: `/usr/share/csv-processor/mietermerkmale.json` + +Dieses können Sie als Vorlage verwenden oder direkt nutzen. + +--- + +## Häufige Anwendungsfälle + +### 1. Monatlicher Export aus Buchhaltung + +**Situation:** Jeden Monat CSV-Export aus Buchhaltungssoftware → Excel-Datei für Chef + +**Lösung:** +1. **Einmalig:** Voreinstellung "Buchhaltung_Export" erstellen + - Mapping: Spaltennamen übersetzen + - Spalten: Nur relevante auswählen + - Format: Excel + - Sortierung: Nach Datum + +2. **Jeden Monat:** + - Programm starten + - Voreinstellung laden + - Neue CSV-Datei angeben + - Ausgabename: `Bericht_Oktober_2025.xlsx` + - **Fertig in 30 Sekunden!** + +### 2. Daten für verschiedene Abteilungen + +**Situation:** Gleiche Quelldaten, aber unterschiedliche Spalten für verschiedene Teams + +**Lösung:** +- Voreinstellung "Export_Vertrieb" - Nur Vertriebsspalten +- Voreinstellung "Export_Buchhaltung" - Nur Finanzspalten +- Voreinstellung "Export_Geschäftsführung" - Übersicht + +### 3. Excel zu CSV konvertieren + +**Situation:** Excel-Datei von Kunde → Eigenes System braucht CSV + +**Lösung (GUI):** +1. Excel-Datei laden +2. Alle Einstellungen auf Standard lassen +3. Ausgabeformat: CSV +4. Delimiter: Semikolon +5. Speichern + +### 4. CSV bereinigen + +**Situation:** CSV-Datei mit vielen leeren Zeilen/Spalten + +**Lösung:** +1. Datei laden +2. ☑ Leere Zeilen entfernen +3. ☑ Leere Spalten entfernen +4. Alle Spalten ausgewählt lassen +5. Speichern + +### 5. Spaltennamen vereinheitlichen + +**Situation:** Verschiedene Systeme, unterschiedliche Spaltennamen, sollen gleich sein + +**Lösung:** +1. Mapping-Datei erstellen mit allen Varianten: +```json +{ + "btr": "Betrag", + "betrag": "Betrag", + "amount": "Betrag", + "sum": "Betrag" +} +``` +2. Voreinstellung mit diesem Mapping +3. Auf alle Dateien anwenden + +--- + +## Tipps & Tricks + +### Performance + +**Große Dateien (>10.000 Zeilen):** +- Verarbeitung kann 5-30 Sekunden dauern +- Bei Excel: Erstes Tabellenblatt ist schneller +- GUI: Status-Log beobachten + +**Sehr große Dateien (>100.000 Zeilen):** +- CLI-Version ist etwas schneller +- Leere Spalten vorher entfernen spart Zeit +- Ggf. Datei vorher splitten + +### Spaltenauswahl + +**Trick: Schnell viele Spalten abwählen (CLI):** +``` +Ihre Wahl: -10-50 +``` +Wählt Spalten 10 bis 50 ab (rot) + +**Trick: Nur wenige Spalten behalten (GUI):** +1. **[Alle abwählen]** +2. **Strg+Klick** auf gewünschte Spalten + +### Mappings + +**Tipp:** Verwenden Sie sprechende Ziel-Namen: +- ✅ "Betrag in Euro" +- ✅ "Datum (Format: TT.MM.JJJJ)" +- ❌ "btr_eur" + +**Tipp:** Ein Mapping pro Datenquelle: +- `system_a_mapping.json` +- `system_b_mapping.json` +- `kunde_xy_mapping.json` + +### CSV-Export + +**Für Excel (deutsche Version):** +- Delimiter: Semikolon +- Quoting: Minimal + +**Für andere Software:** +- Delimiter: Komma oder Tab +- Quoting: Alle (sicherer) + +**Für Datenbanken:** +- Delimiter: Tab +- Quoting: Minimal +- Keine Kopf-/Fußzeile + +### Voreinstellungen + +**Namenskonvention:** +``` +[Zweck]_[Format]_[Besonderheit] +``` + +Beispiele: +- `Mietermerkmale_Excel_monatlich` +- `Buchhaltung_CSV_Vertrieb` +- `Datenbank_Import_Tab` + +### Datensicherheit + +✅ **Original-Dateien bleiben unverändert** +✅ **Keine Daten werden ins Internet gesendet** +✅ **Voreinstellungen enthalten keine Daten, nur Konfiguration** + +--- + +## Fehlerbehebung + +### "Datei nicht gefunden" + +**Problem:** Pfad falsch oder Datei existiert nicht + +**Lösung:** +- Pfad nochmal prüfen +- In GUI: **[Durchsuchen]** verwenden +- Tippfehler korrigieren +- Bei Netzlaufwerken: Verbindung prüfen + +### "Fehler beim Lesen der CSV" + +**Problem:** Encoding oder Format nicht erkannt + +**Lösung (CLI):** +- Im Import-Dialog verschiedene Encodings probieren +- Delimiter manuell wählen + +**Lösung (GUI):** +- Datei in Excel öffnen +- Als CSV mit UTF-8 speichern +- Erneut laden + +### "Keine Spalten ausgewählt" + +**Problem:** Alle Spalten abgewählt oder alle leer + +**Lösung:** +- Prüfen ob Spalten grün (☑) sind +- Ggf. "Leere Spalten entfernen" deaktivieren +- Quelldatei prüfen: Enthält sie Daten? + +### "Excel-Support nicht verfügbar" + +**Problem:** Bibliothek `openpyxl` fehlt + +**Lösung:** +```bash +pip3 install openpyxl +``` + +Oder IT kontaktieren. + +### "Programm reagiert nicht" (GUI) + +**Problem:** Große Datei wird verarbeitet + +**Lösung:** +- Warten (Status-Log beobachten) +- Bei >100.000 Zeilen: 1-2 Minuten normal +- Ggf. CLI-Version verwenden + +### Voreinstellung lädt nicht alle Spalten + +**Grund:** Gespeicherte Spalten sind in aktueller Datei leer oder nicht vorhanden + +**Das ist normal!** Das Programm überspringt automatisch: +1. Leere Spalten (wenn "Leere Spalten entfernen" aktiv) +2. Nicht vorhandene Spalten + +**Status-Meldung beachten:** +``` +Übersprungene Spalten (leer oder nicht vorhanden): 3 + - alte_spalte1 + - leere_spalte2 + - nicht_vorhanden +``` + +--- + +## Support + +Bei Problemen oder Fragen: + +1. **Diese Anleitung durchlesen** 📖 +2. **Status-/Log-Meldungen beachten** 📋 +3. **IT-Abteilung kontaktieren** 💻 + +--- + +## Schnellreferenz + +### CLI-Befehle + +| Aktion | Befehl | +|--------|--------| +| Programm starten | `csv-processor-cli` | +| Datei verarbeiten | Hauptmenü → `1` | +| Voreinstellungen | Hauptmenü → `2` | +| Beenden | Hauptmenü → `0` oder `Strg+C` | + +### GUI-Shortcuts + +| Aktion | Shortcut | +|--------|----------| +| Programm starten | `csv-processor-gui` | +| Datei öffnen | **Strg+O** | +| Speichern | **Strg+S** | +| Voreinstellung laden | **Strg+L** | + +### 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 | + +--- + +**Version:** 2.0 +**Stand:** Oktober 2025 +**Erstellt für:** Interne Nutzung diff --git a/csv_processor/csv-processor-debian.deb b/csv_processor/csv-processor-debian.deb new file mode 100644 index 0000000000000000000000000000000000000000..07b20894948cbc59bed0867973e545b2fc5b55c7 GIT binary patch literal 23418 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 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/csv_processor_gui.py b/csv_processor/csv_processor_gui.py new file mode 100644 index 0000000..ffd4d8c --- /dev/null +++ b/csv_processor/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/csv_processor/mietermerkmale.json b/csv_processor/mietermerkmale.json new file mode 100644 index 0000000..9a52a95 --- /dev/null +++ b/csv_processor/mietermerkmale.json @@ -0,0 +1,504 @@ +{ + "has_header": true, + "remove_empty_rows": true, + "remove_empty": true, + "mapping_file": "/home/sebastian.zell/py/csv_processor/mietermerkmale.json", + "output_format": "csv", + "selected_columns": [ + "Objekt-Zusammenfassung", + "Ort", + "PLZ", + "Straße und Hausnummer", + "Objektname", + "Objektnummer", + "Auftraggeber", + "Aufnahmedatum", + "Erfasser", + "Freitext Erfasser", + "Wohnungsadresse", + "Geografische Breite", + "Geografische Länge", + "Höhe", + "Genauigkeit", + "Einheitsnummer", + "Mietername", + "Gebäudeart", + "Stockwerk", + "Grundriss", + "Keller vorhanden", + "Kellernummer", + "Foto Keller", + "Wintergarten vorhanden", + "Wintergarten beheizt", + "Balkone vorhanden", + "Französische Balkone", + "Aufzug erreichbar", + "Mieter anwesend", + "Aufgemessen", + "Foto Klingel", + "Fläche in qm", + "Einzug", + "Auszug", + "Wohnwert Text", + "Eingangstür", + "Gaszähler in Wohnung", + "Lage Gaszähler", + "Nummer Gaszähler", + "Stand Gaszähler", + "Foto Gaszähler", + "Kaltwasserzähler in Wohnung", + "Lage Kaltwasserzähler", + "Nummer Kaltwasserzähler", + "Stand Kaltwasserzähler", + "Foto Kaltwasserzähler", + "Warmwasserzähler vorhanden", + "Lage Warmwasserzähler", + "Nummer Warmwasserzähler", + "Stand Warmwasserzähler", + "Foto Warmwasserzähler", + "Wärmemengenzähler vorhanden", + "Wärmemengenzähler zentral", + "Lage Wärmemengenzähler", + "Nummer Wärmemengenzähler", + "Stand Wärmemengenzähler", + "Foto Wärmemengenzähler", + "Lage Wärmemengenzähler (R)", + "Nummer Wärmemengenzähler (R)", + "Stand Wärmemengenzähler (R)", + "Foto Wärmemengenzähler (R)", + "Stromzähler vorhanden", + "Lage Stromzähler", + "Nummer Stromzähler", + "Stand Stromzähler", + "Foto Stromzähler", + "Küche", + "Details Küche", + "Foto Küche", + "Bad", + "Details Bad", + "Heizung", + "Lage Therme", + "Warmwasser", + "Lage Boiler", + "Lage Durchlauferhitzer", + "Rauminformationen", + "Raum-Check", + "Zimmer 1 - Bezeichnung", + "Zimmer 1 - Fenster vorhanden", + "Zimmer 1 - Fenstertyp", + "Zimmer 1 - Boden", + "Zimmer 1 - Boden (Text)", + "Zimmer 1 - Wand", + "Zimmer 1 - Wand (Text)", + "Zimmer 1 - Decke", + "Zimmer 1 - Decke (Text)", + "Zimmer 1 - Anschlüsse", + "Zimmer 1 - Sonnenschutz", + "Zimmer 1 - Foto", + "Zimmer 1 - Bemerkungen", + "Zimmer 2 - Bemerkungen", + "Zimmer 2 - Bezeichnung", + "Zimmer 2 - Fenster vorhanden", + "Zimmer 2 - Fenstertyp", + "Zimmer 2 - Boden", + "Zimmer 2 - Boden (Text)", + "Zimmer 2 - Wand", + "Zimmer 2 - Wand (Text)", + "Zimmer 2 - Decke", + "Zimmer 2 - Decke (Text)", + "Zimmer 2 - Anschlüsse", + "Zimmer 2 - Sonnenschutz", + "Zimmer 2 - Bemerkungen", + "Zimmer 2 - Foto", + "Zimmer 3 - Bezeichnung", + "Zimmer 3 - Fenster vorhanden", + "Zimmer 3 - Fenstertyp", + "Zimmer 3 - Boden", + "Zimmer 3 - Boden (Text)", + "Zimmer 3 - Wand", + "Zimmer 3 - Wand (Text)", + "Zimmer 3 - Decke", + "Zimmer 3 - Decke (Text)", + "Zimmer 3 - Anschlüsse", + "Zimmer 3 - Sonnenschutz", + "Zimmer 3 - Bemerkungen", + "Zimmer 3 - Foto", + "Zimmer 4 - Bezeichnung", + "Zimmer 4 - Fenster vorhanden", + "Zimmer 4 - Fenstertyp", + "Zimmer 4 - Boden", + "Zimmer 4 - Boden (Text)", + "Zimmer 4 - Wand", + "Zimmer 4 - Wand (Text)", + "Zimmer 4 - Decke", + "Zimmer 4 - Decke (Text)", + "Zimmer 4 - Anschlüsse", + "Zimmer 4 - Sonnenschutz", + "Zimmer 4 - Bemerkungen", + "Zimmer 4 - Foto", + "Zimmer 5 - Bezeichnung", + "Zimmer 5 - Fenster vorhanden", + "Zimmer 5 - Fenstertyp", + "Zimmer 5 - Boden", + "Zimmer 5 - Boden (Text)", + "Zimmer 5 - Wand", + "Zimmer 5 - Wand (Text)", + "Zimmer 5 - Decke", + "Zimmer 5 - Decke (Text)", + "Zimmer 5 - Anschlüsse", + "Zimmer 5 - Sonnenschutz", + "Zimmer 5 - Bemerkungen", + "Zimmer 5 - Foto", + "Zimmer 6 - Bezeichnung", + "Zimmer 6 - Fenster vorhanden", + "Zimmer 6 - Fenstertyp", + "Zimmer 6 - Boden", + "Zimmer 6 - Boden (Text)", + "Zimmer 6 - Wand", + "Zimmer 6 - Wand (Text)", + "Zimmer 6 - Decke", + "Zimmer 6 - Decke (Text)", + "Zimmer 6 - Anschlüsse", + "Zimmer 6 - Sonnenschutz", + "Zimmer 6 - Bemerkungen", + "Zimmer 6 - Foto", + "Zimmer 7 - Bezeichnung", + "Zimmer 7 - Fenster vorhanden", + "Zimmer 7 - Fenstertyp", + "Zimmer 7 - Boden", + "Zimmer 7 - Boden (Text)", + "Zimmer 7 - Wand", + "Zimmer 7 - Wand (Text)", + "Zimmer 7 - Decke", + "Zimmer 7 - Decke (Text)", + "Zimmer 7 - Anschlüsse", + "Zimmer 7 - Sonnenschutz", + "Zimmer 7 - Bemerkungen", + "Zimmer 7 - Foto", + "Zimmer 8 - Bezeichnung", + "Zimmer 8 - Fenster vorhanden", + "Zimmer 8 - Fenstertyp", + "Zimmer 8 - Boden", + "Zimmer 8 - Boden (Text)", + "Zimmer 8 - Wand", + "Zimmer 8 - Wand (Text)", + "Zimmer 8 - Decke", + "Zimmer 8 - Decke (Text)", + "Zimmer 8 - Anschlüsse", + "Zimmer 8 - Sonnenschutz", + "Zimmer 8 - Bemerkungen", + "Zimmer 8 - Foto", + "Zimmer 9 - Bezeichnung", + "Zimmer 9 - Fenster vorhanden", + "Zimmer 9 - Fenstertyp", + "Zimmer 9 - Boden", + "Zimmer 9 - Boden (Text)", + "Zimmer 9 - Wand", + "Zimmer 9 - Wand (Text)", + "Zimmer 9 - Decke", + "Zimmer 9 - Decke (Text)", + "Zimmer 9 - Anschlüsse", + "Zimmer 9 - Sonnenschutz", + "Zimmer 9 - Bemerkungen", + "Zimmer 9 - Foto", + "Küche - Bezeichnung", + "Küche - Fenster vorhanden", + "Küche - Fenstertyp", + "Küche - Boden", + "Küche - Boden (Text)", + "Küche - Wand", + "Küche - Wand (Text)", + "Küche - Decke", + "Küche - Decke (Text)", + "Küche - Anschlüsse", + "Küche - Sonnenschutz", + "Küche - Bemerkungen", + "Küche - Foto", + "Bad - Bezeichnung", + "Bad - Fenster vorhanden", + "Bad - Fenstertyp", + "Bad - Boden", + "Bad - Boden (Text)", + "Bad - Wand", + "Bad - Wand (Text)", + "Bad - Decke", + "Bad - Decke (Text)", + "Bad - Anschlüsse", + "Bad - Sonnenschutz", + "Bad - Bemerkungen", + "Bad - Foto", + "Flur - Bezeichnung", + "Flur - Fenster vorhanden", + "Flur - Fenstertyp", + "Flur - Boden", + "Flur - Boden (Text)", + "Flur - Wand", + "Flur - Wand (Text)", + "Flur - Decke", + "Flur - Decke (Text)", + "Flur - Anschlüsse", + "Flur - Sonnenschutz", + "Flur - Bemerkungen", + "Flur - Foto", + "Abstellraum - Bezeichnung", + "Abstellraum - Fenster vorhanden", + "Abstellraum - Fenstertyp", + "Abstellraum - Boden", + "Abstellraum - Boden (Text)", + "Abstellraum - Wand", + "Abstellraum - Wand (Text)", + "Abstellraum - Decke", + "Abstellraum - Decke (Text)", + "Abstellraum - Anschlüsse", + "Abstellraum - Sonnenschutz", + "Abstellraum - Bemerkungen", + "Abstellraum - Foto", + "WC separat - Bezeichnung", + "WC separat - Fenster vorhanden", + "WC separat - Fenstertyp", + "WC separat - Boden", + "WC separat - Boden (Text)", + "WC separat - Wand", + "WC separat - Wand (Text)", + "WC separat - Decke", + "grp_kammer-grp_wc_separat-wc_separat_decke_text", + "WC separat - Anschlüsse", + "WC separat - Sonnenschutz", + "WC separat - Bemerkungen", + "WC separat - Foto", + "Kammer - Bezeichnung", + "Kammer - Fenster vorhanden", + "Kammer - Fenstertyp", + "Kammer - Boden", + "Kammer - Boden (Text)", + "Kammer - Wand", + "Kammer - Wand (Text)", + "Kammer - Decke", + "Kammer - Decke (Text)", + "Kammer - Anschlüsse", + "Kammer - Sonnenschutz", + "Kammer - Bemerkungen", + "Kammer - Foto", + "Allgemeine Fotos", + "Besonderheiten", + "E-Mail senden", + "E-Mail-Empfänger", + "Unterschrift nötig", + "Unterschrift Kunde", + "Unterschrift Mitarbeiter", + "Fertig", + "Kellergruppe", + "Instanz-ID", + "Instanz-Name", + "Entitäts-Label", + "Wohnzimmer - Bezeichnung", + "Wohnzimmer - Fenster vorhanden", + "Wohnzimmer - Fenstertyp", + "Wohnzimmer - Boden", + "Wohnzimmer - Boden (Text)", + "Wohnzimmer - Wand", + "Wohnzimmer - Wand (Text)", + "Wohnzimmer - Decke", + "Wohnzimmer - Decke (Text)", + "Wohnzimmer - Anschlüsse", + "Wohnzimmer - Sonnenschutz", + "Wohnzimmer - Bemerkungen", + "Wohnzimmer - Foto", + "Schlafzimmer - Bezeichnung", + "Schlafzimmer - Fenster vorhanden", + "Schlafzimmer - Fenstertyp", + "Schlafzimmer - Boden", + "Schlafzimmer - Boden (Text)", + "Schlafzimmer - Wand", + "Schlafzimmer - Wand (Text)", + "Schlafzimmer - Decke", + "Schlafzimmer - Decke (Text)", + "Schlafzimmer - Anschlüsse", + "Schlafzimmer - Sonnenschutz", + "Schlafzimmer - Bemerkungen", + "Schlafzimmer - Foto", + "Kinderzimmer - Bezeichnung", + "Kinderzimmer - Fenster vorhanden", + "Kinderzimmer - Fenstertyp", + "Kinderzimmer - Boden", + "Kinderzimmer - Boden (Text)", + "Kinderzimmer - Wand", + "Kinderzimmer - Wand (Text)", + "Kinderzimmer - Decke", + "Kinderzimmer - Decke (Text)", + "Kinderzimmer - Anschlüsse", + "Kinderzimmer - Sonnenschutz", + "Kinderzimmer - Bemerkungen", + "Kinderzimmer - Foto", + "Küche - Bezeichnung", + "Küche - Fenster vorhanden", + "Küche - Fenstertyp", + "Küche - Boden", + "Küche - Boden (Text)", + "Küche - Wand", + "Küche - Wand (Text)", + "Küche - Decke", + "Küche - Decke (Text)", + "Küche - Anschlüsse", + "Küche - Sonnenschutz", + "Küche - Bemerkungen", + "Küche - Foto", + "Bad - Bezeichnung", + "Bad - Fenster vorhanden", + "Bad - Fenstertyp", + "Bad - Boden", + "Bad - Boden (Text)", + "Bad - Wand", + "Bad - Wand (Text)", + "Bad - Decke", + "Bad - Decke (Text)", + "Bad - Anschlüsse", + "Bad - Sonnenschutz", + "Bad - Bemerkungen", + "Bad - Foto", + "Flur - Bezeichnung", + "Flur - Fenster vorhanden", + "Flur - Fenstertyp", + "Flur - Boden", + "Flur - Boden (Text)", + "Flur - Wand", + "Flur - Wand (Text)", + "Flur - Decke", + "Flur - Decke (Text)", + "Flur - Anschlüsse", + "Flur - Sonnenschutz", + "Flur - Bemerkungen", + "Flur - Foto", + "Abstellraum - Bezeichnung", + "Abstellraum - Fenster vorhanden", + "Abstellraum - Fenstertyp", + "Abstellraum - Boden", + "Abstellraum - Boden (Text)", + "Abstellraum - Wand", + "Abstellraum - Wand (Text)", + "Abstellraum - Decke", + "Abstellraum - Decke (Text)", + "Abstellraum - Anschlüsse", + "Abstellraum - Sonnenschutz", + "Abstellraum - Bemerkungen", + "Abstellraum - Foto", + "WC separat - Bezeichnung", + "WC separat - Fenster vorhanden", + "WC separat - Fenstertyp", + "WC separat - Boden", + "WC separat - Boden (Text)", + "WC separat - Wand", + "WC separat - Wand (Text)", + "WC separat - Decke", + "WC separat - Decke (Text)", + "WC separat - Anschlüsse", + "WC separat - Sonnenschutz", + "WC separat - Bemerkungen", + "WC separat - Foto", + "Waschküche - Bezeichnung", + "Waschküche - Fenster vorhanden", + "Waschküche - Fenstertyp", + "Waschküche - Boden", + "Waschküche - Boden (Text)", + "Waschküche - Wand", + "Waschküche - Wand (Text)", + "Waschküche - Decke", + "Waschküche - Decke (Text)", + "Waschküche - Anschlüsse", + "Waschküche - Sonnenschutz", + "Waschküche - Bemerkungen", + "Waschküche - Foto", + "Hauswirtschaftsraum - Bezeichnung", + "Hauswirtschaftsraum - Fenster vorhanden", + "Hauswirtschaftsraum - Fenstertyp", + "Hauswirtschaftsraum - Boden", + "Hauswirtschaftsraum - Boden (Text)", + "Hauswirtschaftsraum - Wand", + "Hauswirtschaftsraum - Wand (Text)", + "Hauswirtschaftsraum - Decke", + "Hauswirtschaftsraum - Decke (Text)", + "Hauswirtschaftsraum - Anschlüsse", + "Hauswirtschaftsraum - Sonnenschutz", + "Hauswirtschaftsraum - Bemerkungen", + "Hauswirtschaftsraum - Foto", + "Technikraum - Bezeichnung", + "Technikraum - Fenster vorhanden", + "Technikraum - Fenstertyp", + "Technikraum - Boden", + "Technikraum - Boden (Text)", + "Technikraum - Wand", + "Technikraum - Wand (Text)", + "Technikraum - Decke", + "Technikraum - Decke (Text)", + "Technikraum - Anschlüsse", + "Technikraum - Sonnenschutz", + "Technikraum - Bemerkungen", + "Technikraum - Foto", + "Speisekammer - Bezeichnung", + "Speisekammer - Fenster vorhanden", + "Speisekammer - Fenstertyp", + "Speisekammer - Boden", + "Speisekammer - Boden (Text)", + "Speisekammer - Wand", + "Speisekammer - Wand (Text)", + "Speisekammer - Decke", + "Speisekammer - Decke (Text)", + "Speisekammer - Anschlüsse", + "Speisekammer - Sonnenschutz", + "Speisekammer - Bemerkungen", + "Speisekammer - Foto", + "Heizungsraum - Bezeichnung", + "Heizungsraum - Fenster vorhanden", + "Heizungsraum - Fenstertyp", + "Heizungsraum - Boden", + "Heizungsraum - Boden (Text)", + "Heizungsraum - Wand", + "Heizungsraum - Wand (Text)", + "Heizungsraum - Decke", + "Heizungsraum - Decke (Text)", + "Heizungsraum - Anschlüsse", + "Heizungsraum - Sonnenschutz", + "Heizungsraum - Bemerkungen", + "Heizungsraum - Foto", + "Durchgangszimmer - Bezeichnung", + "Durchgangszimmer - Fenster vorhanden", + "Durchgangszimmer - Fenstertyp", + "Durchgangszimmer - Boden", + "Durchgangszimmer - Boden (Text)", + "Durchgangszimmer - Wand", + "Durchgangszimmer - Wand (Text)", + "Durchgangszimmer - Decke", + "Durchgangszimmer - Decke (Text)", + "Durchgangszimmer - Anschlüsse", + "Durchgangszimmer - Sonnenschutz", + "Durchgangszimmer - Bemerkungen", + "Durchgangszimmer - Foto", + "Esszimmer - Bezeichnung", + "Esszimmer - Fenster vorhanden", + "Esszimmer - Fenstertyp", + "Esszimmer - Boden", + "Esszimmer - Boden (Text)", + "Esszimmer - Wand", + "Esszimmer - Wand (Text)", + "Esszimmer - Decke", + "Esszimmer - Decke (Text)", + "Esszimmer - Anschlüsse", + "Esszimmer - Sonnenschutz", + "Esszimmer - Bemerkungen", + "Esszimmer - Foto", + "Arbeitszimmer - Bezeichnung", + "Arbeitszimmer - Fenster vorhanden", + "Arbeitszimmer - Fenstertyp", + "Arbeitszimmer - Boden", + "Arbeitszimmer - Boden (Text)", + "Arbeitszimmer - Wand", + "Arbeitszimmer - Wand (Text)", + "Arbeitszimmer - Decke", + "Arbeitszimmer - Decke (Text)", + "Arbeitszimmer - Anschlüsse", + "Arbeitszimmer - Sonnenschutz", + "Arbeitszimmer - Bemerkungen", + "Arbeitszimmer - Foto" + ], + "source_file": "/home/sebastian.zell/Downloads/stammdaten_wohnungen(3).csv" +} \ No newline at end of file