#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Hausverwaltung CSV zu DBF Konverter GUI Version für Linux (Debian, Ubuntu, Kubuntu) Version 3.0 """ import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext import json import csv import os import struct from datetime import datetime, date import threading import re import sys # ===== BACKEND FUNKTIONEN (aus Original-Code) ===== def normalize_text(text): """Normalisiert Text für Vergleiche""" text = text.lower() replacements = { 'ü': 'ue', 'ö': 'oe', 'ä': 'ae', 'ß': 'ss', 'é': 'e', 'è': 'e', 'ê': 'e', 'à': 'a', 'â': 'a' } for old, new in replacements.items(): text = text.replace(old, new) return text def load_mandanten(): """Lädt Mandanten aus Config""" if not os.path.exists('mandanten_config.json'): return [] with open('mandanten_config.json', 'r', encoding='utf-8') as f: return json.load(f)['mandanten'] def save_mandanten(mandanten): """Speichert Mandanten in Config""" with open('mandanten_config.json', 'w', encoding='utf-8') as f: json.dump({"mandanten": mandanten}, f, indent=4, ensure_ascii=False) def load_mieter(mandantennummer): """Lädt Mieter für einen bestimmten Mandanten""" mieter_datei = f"identitaeten_mieter_{mandantennummer}.json" if not os.path.exists(mieter_datei): return [] with open(mieter_datei, 'r', encoding='utf-8') as f: return json.load(f)['mieter'] def load_kostenkonten(mandantennummer): """Lädt Kostenkonten für einen bestimmten Mandanten""" kosten_datei = f"identitaeten_kosten_{mandantennummer}.json" if not os.path.exists(kosten_datei): return [] with open(kosten_datei, 'r', encoding='utf-8') as f: return json.load(f)['kostenkonten'] def load_projekte(mandantennummer): """Lädt Projekte/Kostenstellen für einen bestimmten Mandanten""" projekte_datei = f"projekte_{mandantennummer}.json" if not os.path.exists(projekte_datei): return [], '' with open(projekte_datei, 'r', encoding='utf-8') as f: config = json.load(f) return config['projekte'], config.get('default_projekt', '') # ===== GUI KLASSE ===== class HausverwaltungGUI: def __init__(self, root): self.root = root self.root.title("Hausverwaltung CSV→DBF Konverter v3.0") self.root.geometry("900x700") # Moderne Farben self.bg_color = "#f0f0f0" self.accent_color = "#2196F3" self.success_color = "#4CAF50" self.error_color = "#f44336" self.root.configure(bg=self.bg_color) # Variablen self.csv_file = None self.mandanten = load_mandanten() self.selected_mandant = None self.processing = False self.setup_ui() self.load_initial_data() def setup_ui(self): """Erstellt die GUI-Komponenten""" # Style konfigurieren style = ttk.Style() style.theme_use('clam') style.configure('Title.TLabel', font=('Ubuntu', 16, 'bold')) style.configure('Header.TLabel', font=('Ubuntu', 11, 'bold')) style.configure('Success.TLabel', foreground=self.success_color) style.configure('Error.TLabel', foreground=self.error_color) # Hauptcontainer main_frame = ttk.Frame(self.root, padding="20") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # Titel title_label = ttk.Label(main_frame, text="📊 CSV zu DBF Konverter für Hausverwaltung", style='Title.TLabel') title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20)) # 1. Mandanten-Auswahl ttk.Label(main_frame, text="1. Mandant auswählen:", style='Header.TLabel').grid( row=1, column=0, sticky=tk.W, pady=(10, 5)) self.mandant_frame = ttk.LabelFrame(main_frame, text="Mandanten", padding="10") self.mandant_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15)) self.mandant_combo = ttk.Combobox(self.mandant_frame, width=50, state="readonly") self.mandant_combo.grid(row=0, column=0, padx=(0, 10)) self.mandant_combo.bind('<>', self.on_mandant_select) self.mandant_info = ttk.Label(self.mandant_frame, text="") self.mandant_info.grid(row=0, column=1) # 2. CSV-Datei auswählen ttk.Label(main_frame, text="2. CSV-Datei auswählen:", style='Header.TLabel').grid( row=3, column=0, sticky=tk.W, pady=(10, 5)) file_frame = ttk.Frame(main_frame) file_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15)) self.file_label = ttk.Label(file_frame, text="Keine Datei ausgewählt", relief=tk.SUNKEN, padding="5") self.file_label.grid(row=0, column=0, sticky=(tk.W, tk.E)) ttk.Button(file_frame, text="📁 Datei wählen", command=self.select_csv_file).grid(row=0, column=1, padx=(10, 0)) file_frame.columnconfigure(0, weight=1) # 3. Zeitraum auswählen ttk.Label(main_frame, text="3. Zeitraum auswählen:", style='Header.TLabel').grid( row=5, column=0, sticky=tk.W, pady=(10, 5)) time_frame = ttk.LabelFrame(main_frame, text="Zeitraum", padding="10") time_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15)) # Monate ttk.Label(time_frame, text="Monate:").grid(row=0, column=0, sticky=tk.W) self.months_var = tk.StringVar(value="1-12") ttk.Entry(time_frame, textvariable=self.months_var, width=20).grid( row=0, column=1, padx=(5, 20)) ttk.Label(time_frame, text="(z.B. 1-12 oder 1,2,3)", font=('Ubuntu', 9)).grid(row=0, column=2) # Jahre ttk.Label(time_frame, text="Jahre:").grid(row=1, column=0, sticky=tk.W, pady=(5, 0)) current_year = datetime.now().year self.years_var = tk.StringVar(value=str(current_year)) ttk.Entry(time_frame, textvariable=self.years_var, width=20).grid( row=1, column=1, padx=(5, 20), pady=(5, 0)) ttk.Label(time_frame, text="(z.B. 2024 oder 2023,2024)", font=('Ubuntu', 9)).grid(row=1, column=2, pady=(5, 0)) # 4. Ausgabe-Log ttk.Label(main_frame, text="Verarbeitungsprotokoll:", style='Header.TLabel').grid( row=7, column=0, sticky=tk.W, pady=(10, 5)) # Log-Textfeld mit Scrollbar log_frame = ttk.Frame(main_frame) log_frame.grid(row=8, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 15)) self.log_text = scrolledtext.ScrolledText(log_frame, height=12, width=80, wrap=tk.WORD, font=('Courier', 9)) self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) log_frame.columnconfigure(0, weight=1) log_frame.rowconfigure(0, weight=1) # Fortschrittsbalken self.progress = ttk.Progressbar(main_frame, mode='indeterminate') self.progress.grid(row=9, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10)) # Buttons button_frame = ttk.Frame(main_frame) button_frame.grid(row=10, column=0, columnspan=3) self.process_btn = ttk.Button(button_frame, text="🔄 Konvertierung starten", command=self.start_processing) self.process_btn.grid(row=0, column=0, padx=5) ttk.Button(button_frame, text="⚙️ Konfiguration", command=self.open_config).grid(row=0, column=1, padx=5) ttk.Button(button_frame, text="❌ Beenden", command=self.root.quit).grid(row=0, column=2, padx=5) # Grid-Konfiguration main_frame.columnconfigure(1, weight=1) main_frame.rowconfigure(8, weight=1) self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) def load_initial_data(self): """Lädt initiale Daten""" if self.mandanten: mandant_list = [f"{m['name']} (Nr. {m['nummer']})" for m in self.mandanten] self.mandant_combo['values'] = mandant_list if mandant_list: self.mandant_combo.current(0) self.on_mandant_select(None) else: self.log("⚠️ Keine Mandanten gefunden. Bitte Konfiguration prüfen.") def on_mandant_select(self, event): """Wird aufgerufen wenn ein Mandant ausgewählt wird""" if self.mandant_combo.current() >= 0: self.selected_mandant = self.mandanten[self.mandant_combo.current()] info_text = (f"Konto: {self.selected_mandant['konto']}, " f"Gegenkonto: {self.selected_mandant['standard_gegenkonto']}, " f"Nächster Beleg: {self.selected_mandant.get('beleg_index', 1)}") self.mandant_info.config(text=info_text) self.log(f"✓ Mandant gewählt: {self.selected_mandant['name']}") def select_csv_file(self): """Öffnet Dialog zur CSV-Auswahl""" filename = filedialog.askopenfilename( title="CSV-Datei auswählen", filetypes=[("CSV Dateien", "*.csv"), ("Alle Dateien", "*.*")] ) if filename: self.csv_file = filename self.file_label.config(text=os.path.basename(filename)) self.log(f"✓ CSV-Datei gewählt: {filename}") def log(self, message): """Fügt eine Nachricht zum Log hinzu""" timestamp = datetime.now().strftime("%H:%M:%S") self.log_text.insert(tk.END, f"[{timestamp}] {message}\n") self.log_text.see(tk.END) self.root.update_idletasks() def start_processing(self): """Startet die Konvertierung in einem separaten Thread""" if self.processing: messagebox.showwarning("Warnung", "Konvertierung läuft bereits!") return if not self.selected_mandant: messagebox.showerror("Fehler", "Bitte wählen Sie einen Mandanten!") return if not self.csv_file: messagebox.showerror("Fehler", "Bitte wählen Sie eine CSV-Datei!") return # Starte Verarbeitung in Thread self.processing = True self.process_btn.config(state='disabled') self.progress.start() thread = threading.Thread(target=self.process_conversion) thread.daemon = True thread.start() def process_conversion(self): """Führt die eigentliche Konvertierung durch""" try: self.log("\n" + "="*60) self.log("🚀 Starte Konvertierung...") # Lade Konfigurationen mieter = load_mieter(self.selected_mandant['nummer']) self.log(f"✓ {len(mieter)} Mieter geladen") kostenkonten = load_kostenkonten(self.selected_mandant['nummer']) self.log(f"✓ {len(kostenkonten)} Kostenkonten geladen") projekte, default_projekt = load_projekte(self.selected_mandant['nummer']) self.log(f"✓ {len(projekte)} Projekte geladen") # Lese CSV buchungen = self.lese_csv_mit_log(self.csv_file) self.log(f"✓ {len(buchungen)} Buchungen aus CSV geladen") # Parse Zeitraum monate = self.parse_monatseingabe(self.months_var.get()) jahre = self.parse_jahreseingabe(self.years_var.get()) # Filtere Buchungen gefilterte = self.filtere_buchungen(buchungen, monate, jahre) self.log(f"✓ {len(gefilterte)} Buchungen nach Filterung") if not gefilterte: self.log("⚠️ Keine Buchungen für den gewählten Zeitraum!") return # Erstelle DBF ausgabe_prefix = f"buchungen_{self.selected_mandant['nummer']}" start_beleg = self.selected_mandant.get('beleg_index', 1) letzte_beleg = self.erstelle_dbf_mit_log( self.selected_mandant, gefilterte, mieter, kostenkonten, projekte, default_projekt, ausgabe_prefix, start_beleg ) # Update Beleg-Index self.selected_mandant['beleg_index'] = letzte_beleg + 1 save_mandanten(self.mandanten) self.log("\n" + "="*60) self.log("✅ KONVERTIERUNG ERFOLGREICH ABGESCHLOSSEN!") self.log(f"📊 Buchungen verarbeitet: {len(gefilterte)}") self.log(f"📁 DBF-Dateien erstellt im Verzeichnis: {os.getcwd()}") self.log("="*60) messagebox.showinfo("Erfolg", f"Konvertierung erfolgreich!\n\n" f"Verarbeitete Buchungen: {len(gefilterte)}\n" f"Belegnummern: {start_beleg} bis {letzte_beleg}") except Exception as e: self.log(f"❌ FEHLER: {str(e)}") messagebox.showerror("Fehler", f"Konvertierung fehlgeschlagen:\n{str(e)}") finally: self.processing = False self.process_btn.config(state='normal') self.progress.stop() def lese_csv_mit_log(self, csv_datei): """Liest CSV-Datei mit Logging""" buchungen = [] with open(csv_datei, 'r', encoding='utf-8') as f: reader = csv.DictReader(f, delimiter=';') reader.fieldnames = [name.strip().lstrip('\ufeff') if name else name for name in reader.fieldnames] # Spaltenerkennung datum_spalte = None text_spalte = None betrag_spalte = None for name in reader.fieldnames: name_lower = name.lower() if 'buchungstag' in name_lower or 'datum' in name_lower: datum_spalte = name if 'buchungstext' in name_lower or 'verwendungszweck' in name_lower: text_spalte = name if 'betrag' in name_lower: betrag_spalte = name for row in reader: try: datum_str = row[datum_spalte].strip() datum = datetime.strptime(datum_str, '%d.%m.%Y') betrag_str = row[betrag_spalte].strip().replace('.', '').replace(',', '.') betrag = float(betrag_str) buchung = { 'datum': datum, 'buchungstext': row[text_spalte].strip() if row[text_spalte] else '', 'betrag': betrag } buchungen.append(buchung) except: continue return buchungen def parse_monatseingabe(self, eingabe): """Parst Monatseingabe""" monate = set() teile = eingabe.split(',') for teil in teile: teil = teil.strip() if '-' in teil: start, ende = map(int, teil.split('-')) monate.update(range(start, ende + 1)) else: monate.add(int(teil)) return sorted(list(monate)) def parse_jahreseingabe(self, eingabe): """Parst Jahreseingabe""" jahre = set() teile = eingabe.split(',') for teil in teile: teil = teil.strip() if '-' in teil: start, ende = map(int, teil.split('-')) jahre.update(range(start, ende + 1)) else: jahre.add(int(teil)) return sorted(list(jahre)) def filtere_buchungen(self, buchungen, monate, jahre): """Filtert Buchungen nach Zeitraum""" return [b for b in buchungen if b['datum'].month in monate and b['datum'].year in jahre] def erstelle_dbf_mit_log(self, mandant, buchungen, mieter, kostenkonten, projekte, default_projekt, ausgabe_datei_praefix, start_beleg_nr): """Erstellt DBF-Dateien mit Logging (vereinfachte Version)""" # Gruppiere nach Monat buchungen_pro_monat = {} for buchung in buchungen: monats_key = buchung['datum'].strftime('%Y-%m') if monats_key not in buchungen_pro_monat: buchungen_pro_monat[monats_key] = [] buchungen_pro_monat[monats_key].append(buchung) self.log(f"📅 Erstelle DBF-Dateien für {len(buchungen_pro_monat)} Monate...") beleg_nr = start_beleg_nr for monat_key, monats_buchungen in buchungen_pro_monat.items(): jahr, monat = map(int, monat_key.split('-')) jahr_kurz = str(jahr)[-2:] ausgabe_datei = f"{ausgabe_datei_praefix}_{monat:02d}{jahr_kurz}.dbf" self.log(f" → Erstelle {ausgabe_datei} ({len(monats_buchungen)} Buchungen)") # DBF erstellen (vereinfacht - nutzt Ihre Original-Logik) self.create_dbf_file(ausgabe_datei, monats_buchungen, beleg_nr, mandant, mieter, kostenkonten, projekte, default_projekt) beleg_nr += len(monats_buchungen) return beleg_nr - 1 def create_dbf_file(self, filename, buchungen, start_beleg, mandant, mieter, kostenkonten, projekte, default_projekt): """Erstellt eine DBF-Datei (vereinfacht aus Original)""" # Hier würde Ihre Original DBF-Erstellungslogik stehen # Aus Platzgründen nur Grundstruktur heute = datetime.now() num_records = len(buchungen) header_len = 32 + 43 * 32 + 1 record_len = 368 header = bytearray(32) header[0] = 0x03 # dBase III header[1] = heute.year - 1900 header[2] = heute.month header[3] = heute.day struct.pack_into('