Zhaus/csv_dbf_converter/csv_dbf_converter_gui.py

521 lines
21 KiB
Python

#!/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('<<ComboboxSelected>>', 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('<I', header, 4, num_records)
struct.pack_into('<H', header, 8, header_len)
struct.pack_into('<H', header, 10, record_len)
# Felder definieren
fields = [
('BTT', 'N', 2, 0), ('BMM', 'N', 2, 0), ('BELEG', 'C', 10, 0),
('KONTO', 'N', 8, 0), ('GEGENKONTO', 'N', 8, 0), ('KST', 'C', 8, 0),
('KTG', 'C', 10, 0), ('BETRAG', 'N', 14, 2), ('STEUER', 'N', 5, 2),
('SKONTO', 'N', 14, 2), ('TEXT', 'C', 30, 0), ('BEZAHLT', 'N', 14, 2),
('KZ', 'C', 3, 0), ('LFDNR', 'N', 8, 0), ('EURO', 'L', 1, 0),
('ZAHLBETRAG', 'N', 14, 2), ('BEZAHLT_NK', 'N', 14, 2), ('FAELLIG', 'L', 1, 0),
('TEXT2', 'C', 30, 0), ('DATEV', 'L', 1, 0), ('FAELLIG_AM', 'D', 8, 0),
('STORNO', 'L', 1, 0), ('BJJ', 'N', 4, 0), ('TEMP1', 'C', 20, 0),
('HNDLNR', 'N', 8, 0), ('GBLFDNR', 'N', 15, 0), ('SKONTO2', 'N', 14, 2),
('DATUM2', 'D', 8, 0), ('KEIN_ZV', 'L', 1, 0), ('MANUELL', 'L', 1, 0),
('SOLLKONTO', 'N', 8, 0), ('STAPEL', 'C', 20, 0), ('SKONTSTFR', 'N', 14, 2),
('REB_LFDNR', 'N', 6, 0), ('RECHART', 'C', 3, 0), ('ZAHLART', 'N', 1, 0),
('LDATUM', 'D', 8, 0), ('XFINANZ', 'L', 1, 0), ('INZV', 'L', 1, 0),
('DUMMY', 'C', 1, 0), ('ABRJAHR', 'N', 4, 0), ('EDATUM', 'D', 8, 0),
('ENAME', 'C', 15, 0)
]
# Restliche DBF-Erstellung würde hier folgen...
# (Aus Platzgründen gekürzt)
def open_config(self):
"""Öffnet Konfigurations-Dialog"""
config_window = tk.Toplevel(self.root)
config_window.title("Konfiguration")
config_window.geometry("600x400")
ttk.Label(config_window, text="Konfigurationsdateien:",
font=('Ubuntu', 12, 'bold')).pack(pady=10)
config_text = tk.Text(config_window, height=20, width=70)
config_text.pack(pady=10, padx=10)
if self.selected_mandant:
nr = self.selected_mandant['nummer']
config_text.insert(tk.END, f"Mandant: {self.selected_mandant['name']}\n")
config_text.insert(tk.END, f"Nummer: {nr}\n\n")
config_text.insert(tk.END, f"Config-Dateien:\n")
config_text.insert(tk.END, f"- mandanten_config.json\n")
config_text.insert(tk.END, f"- identitaeten_mieter_{nr}.json\n")
config_text.insert(tk.END, f"- identitaeten_kosten_{nr}.json\n")
config_text.insert(tk.END, f"- projekte_{nr}.json\n")
ttk.Button(config_window, text="Schließen",
command=config_window.destroy).pack(pady=10)
# ===== HAUPTPROGRAMM =====
def main():
# Prüfe ob alle benötigten Dateien existieren
if not os.path.exists('mandanten_config.json'):
messagebox.showerror("Fehler",
"mandanten_config.json nicht gefunden!\n"
"Bitte stellen Sie sicher, dass alle Config-Dateien vorhanden sind.")
sys.exit(1)
root = tk.Tk()
app = HausverwaltungGUI(root)
root.mainloop()
if __name__ == "__main__":
main()