import json import csv import os import struct from datetime import datetime, date # ===== CONFIG DATEIEN ERSTELLEN/LADEN ===== def create_default_configs(): """Erstellt Standard-Config-Dateien falls nicht vorhanden""" # Mandanten Config if not os.path.exists('mandanten_config.json'): mandanten_config = { "mandanten": [ { "name": "Mustermann GmbH", "nummer": "001", "konto": "1200", "standard_gegenkonto": "8400", "beleg_index": 1 }, { "name": "Beispiel AG", "nummer": "002", "konto": "1800", "standard_gegenkonto": "8500", "beleg_index": 1 } ] } with open('mandanten_config.json', 'w', encoding='utf-8') as f: json.dump(mandanten_config, f, indent=4, ensure_ascii=False) print("✓ mandanten_config.json erstellt") # Prüfe ob Identitäten-Dateien für jeden Mandanten existieren mandanten = load_mandanten() for mandant in mandanten: # Mieter-Identitäten mieter_datei = f"identitaeten_mieter_{mandant['nummer']}.json" if not os.path.exists(mieter_datei): mieter_config = { "mandant": mandant['name'], "mandantennummer": mandant['nummer'], "mieter": [ { "suchbegriffe": ["Thomas Müller", "Müller", "T. Müller"], "konto": "1210", "beschreibung": "Miete Thomas Müller" }, { "suchbegriffe": ["Schmidt", "Anna Schmidt", "A. Schmidt"], "konto": "1220", "beschreibung": "Miete Anna Schmidt" }, { "suchbegriffe": ["Weber GmbH", "Weber"], "konto": "1230", "beschreibung": "Miete Weber GmbH" } ] } with open(mieter_datei, 'w', encoding='utf-8') as f: json.dump(mieter_config, f, indent=4, ensure_ascii=False) print(f"✓ {mieter_datei} erstellt") # Kostenkonten-Identitäten kosten_datei = f"identitaeten_kosten_{mandant['nummer']}.json" if not os.path.exists(kosten_datei): kosten_config = { "mandant": mandant['name'], "mandantennummer": mandant['nummer'], "kostenkonten": [ { "suchbegriffe": ["Wasser", "Wasserbetriebe", "Berliner Wasserbetriebe"], "gegenkonto": "6300", "beschreibung": "Wasserkosten" }, { "suchbegriffe": ["Strom", "Vattenfall", "Stromversorgung"], "gegenkonto": "6310", "beschreibung": "Stromkosten" }, { "suchbegriffe": ["Gas", "Gasag", "Gasversorgung"], "gegenkonto": "6320", "beschreibung": "Gaskosten" }, { "suchbegriffe": ["Versicherung", "Allianz", "Gebäudeversicherung"], "gegenkonto": "6500", "beschreibung": "Versicherung" }, { "suchbegriffe": ["Hausverwaltung", "Verwaltung", "Verwaltungsgebühr"], "gegenkonto": "6700", "beschreibung": "Hausverwaltungskosten" } ] } with open(kosten_datei, 'w', encoding='utf-8') as f: json.dump(kosten_config, f, indent=4, ensure_ascii=False) print(f"✓ {kosten_datei} erstellt") # Projekte-Datei projekte_datei = f"projekte_{mandant['nummer']}.json" if not os.path.exists(projekte_datei): projekte_config = { "mandant": mandant['name'], "mandantennummer": mandant['nummer'], "default_projekt": "ALLG001", "projekte": [ { "suchbegriffe": ["Projekt Alpha", "Alpha"], "kst": "PRJ001", "beschreibung": "Projekt Alpha" }, { "suchbegriffe": ["Hausverwaltung", "HV2024"], "kst": "HV2024", "beschreibung": "Hausverwaltung 2024" }, { "suchbegriffe": ["Musterstraße 10", "Musterstr. 10"], "kst": "HAUS01", "beschreibung": "Musterstraße 10" } ] } with open(projekte_datei, 'w', encoding='utf-8') as f: json.dump(projekte_config, f, indent=4, ensure_ascii=False) print(f"✓ {projekte_datei} erstellt") def load_mandanten(): """Lädt Mandanten aus Config""" 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" 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" 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" with open(projekte_datei, 'r', encoding='utf-8') as f: config = json.load(f) return config['projekte'], config.get('default_projekt', '') def normalize_text(text): """ Normalisiert Text für Vergleiche: - Kleinbuchstaben - ü → ue, ö → oe, ä → ae, ß → ss """ 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 # ===== PARSING LOGIK ===== def parse_mieter(buchungstext, mieter): """ Sucht nach Mietern im Buchungstext (OHNE Projektnummer) Returns: (konto, beschreibung) oder (None, None) """ buchungstext_norm = normalize_text(buchungstext) for mieter_entry in mieter: for suchbegriff in mieter_entry['suchbegriffe']: suchbegriff_norm = normalize_text(suchbegriff) if suchbegriff_norm in buchungstext_norm: print(f" → Mieter gefunden: '{suchbegriff}' → Konto {mieter_entry['konto']}") return mieter_entry['konto'], mieter_entry.get('beschreibung', '') return None, None def parse_kostenkonten(buchungstext, kostenkonten): """ Sucht nach Kostenkonten im Buchungstext Returns: (gegenkonto, beschreibung) oder (None, None) """ buchungstext_norm = normalize_text(buchungstext) for kosten_entry in kostenkonten: for suchbegriff in kosten_entry['suchbegriffe']: suchbegriff_norm = normalize_text(suchbegriff) if suchbegriff_norm in buchungstext_norm: print(f" → Kostenkonto gefunden: '{suchbegriff}' → Gegenkonto {kosten_entry['gegenkonto']}") return kosten_entry['gegenkonto'], kosten_entry.get('beschreibung', '') return None, None def parse_projekt(buchungstext, projekte, default_projekt): """ Sucht nach Projekten im Buchungstext Wird NUR aufgerufen wenn Kostenkonto gefunden wurde! Returns: kostenstelle """ buchungstext_norm = normalize_text(buchungstext) for projekt in projekte: for suchbegriff in projekt['suchbegriffe']: suchbegriff_norm = normalize_text(suchbegriff) if suchbegriff_norm in buchungstext_norm: print(f" → Projekt gefunden: '{suchbegriff}' → KST {projekt['kst']}") return projekt['kst'] # Kein Projekt gefunden → Default verwenden if default_projekt: print(f" → Kein Projekt gefunden → Default-KST {default_projekt}") return default_projekt return '' def bestimme_konten_und_kst(buchungstext, mandant, mieter, kostenkonten, projekte, default_projekt): """ Bestimmt Konten, Kostenstelle und Beschreibung basierend auf dem Buchungstext. Mit Mieternummer-Parsing für Konten beginnend mit "1". """ norm_buchungstext = normalize_text(buchungstext) # --- TEIL 0: ZUERST NACH MIETERNUMMER SUCHEN (nur für Konten mit "1") --- import re buchungstyp = 'unbekannt' gegenkonto = None beschreibung = None # Suche nach 5-stelligen Zahlen im Buchungstext (potenzielle Mieternummern) mieternummer_pattern = r'\b(1\d{4})\b' # 5-stellige Zahl beginnend mit 1 gefundene_nummern = re.findall(mieternummer_pattern, buchungstext) if gefundene_nummern: print(f" 🔍 Gefundene potenzielle Mieternummern: {gefundene_nummern}") # Prüfe ob eine der gefundenen Nummern ein Mieterkonto ist for nummer in gefundene_nummern: for mieter_entry in mieter: if mieter_entry.get('konto') == nummer: gegenkonto = mieter_entry.get('konto') beschreibung = mieter_entry.get('beschreibung') buchungstyp = 'mieter' print(f" ✓ Mieter über Kontonummer gefunden: {nummer} → {beschreibung}") break if buchungstyp == 'mieter': break # --- TEIL 1: NUR WENN KEINE MIETERNUMMER GEFUNDEN, NORMALES PARSING --- if buchungstyp == 'unbekannt': # Standard-Gegenkonto aus Mandanten-Config verwenden gegenkonto = mandant.get('standard_gegenkonto', '1300') # KORRIGIERT! beschreibung = "Unbekannte Buchung" # Suche nach Mietern über Suchbegriffe for mieter_entry in mieter: for suchbegriff in mieter_entry.get('suchbegriffe', []): if normalize_text(suchbegriff) in norm_buchungstext: gegenkonto = mieter_entry.get('konto') beschreibung = mieter_entry.get('beschreibung') buchungstyp = 'mieter' print(f" ✓ Buchungstyp: Mieter (gefunden über '{suchbegriff}')") break if buchungstyp == 'mieter': break # --- TEIL 1b: KOSTENKONTEN PRÜFEN --- if buchungstyp == 'unbekannt': for kosten_entry in kostenkonten: for suchbegriff in kosten_entry.get('suchbegriffe', []): if normalize_text(suchbegriff) in norm_buchungstext: gegenkonto = kosten_entry.get('gegenkonto') beschreibung = kosten_entry.get('bezeichnung', '') buchungstyp = 'kosten' print(f" ✓ Buchungstyp: Kosten (gefunden über '{suchbegriff}')") break if buchungstyp == 'kosten': break # --- TEIL 2: KOSTENSTELLE BESTIMMEN (nur bei Kostenkonten) --- gefundene_kst = '' if buchungstyp == 'kosten': for projekt in projekte: for suchbegriff in projekt.get('suchbegriffe', []): if normalize_text(suchbegriff) in norm_buchungstext: gefundene_kst = projekt.get('kst', '') print(f" ✓ Projekt-Kostenstelle gefunden: '{gefundene_kst}' (über '{suchbegriff}')") break if gefundene_kst: break # Default-Kostenstelle nur bei Kostenkonten if not gefundene_kst and default_projekt: gefundene_kst = default_projekt print(f" → Keine Kostenstelle gefunden → Default-KST {default_projekt}") # --- TEIL 3: ERGEBNISSE ZUSAMMENFÜHREN --- konto = mandant.get('standard_bank_kto', mandant.get('konto', '1200')) kostenstelle = gefundene_kst # DIAGNOSE-AUSGABE print("\n" + "="*25 + " FINALES ZUORDNUNGSERGEBNIS " + "="*25) print(f" - KONTO: {konto}") print(f" - GEGENKONTO: {gegenkonto}") print(f" - KOSTENSTELLE: '{kostenstelle}'") print(f" - BESCHREIBUNG: {beschreibung}") print(f" - BUCHUNGSTYP: {buchungstyp}") print("="*70 + "\n") return str(konto), str(gegenkonto), str(kostenstelle), beschreibung # ===== CSV EINLESEN ===== def lese_csv(csv_datei): """Liest CSV-Datei ein und gibt Liste von Buchungen zurück""" buchungen = [] with open(csv_datei, 'r', encoding='utf-8') as f: reader = csv.DictReader(f, delimiter=';') # Spaltennamen bereinigen (Leerzeichen UND BOM entfernen) reader.fieldnames = [name.strip().lstrip('\ufeff') if name else name for name in reader.fieldnames] # Debug: Spaltennamen anzeigen print(f"\n📋 Gefundene CSV-Spalten: {reader.fieldnames}") # Flexible 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 or 'text' in name_lower: text_spalte = name if 'betrag' in name_lower or 'amount' in name_lower: betrag_spalte = name if not datum_spalte: raise ValueError(f"Keine Datums-Spalte gefunden! Verfügbare Spalten: {reader.fieldnames}") if not text_spalte: raise ValueError(f"Keine Text-Spalte gefunden! Verfügbare Spalten: {reader.fieldnames}") if not betrag_spalte: raise ValueError(f"Keine Betrags-Spalte gefunden! Verfügbare Spalten: {reader.fieldnames}") print(f"✓ Verwende Spalten: Datum='{datum_spalte}', Text='{text_spalte}', Betrag='{betrag_spalte}'") for row in reader: try: # Datum parsen (Format: DD.MM.YYYY) datum_str = row[datum_spalte].strip() datum = datetime.strptime(datum_str, '%d.%m.%Y') # Betrag konvertieren (Format: -123,45 → -123.45) betrag_str = row[betrag_spalte].strip().replace('.', '').replace(',', '.') betrag = float(betrag_str) # Buchungstext buchungstext = row[text_spalte].strip() if row[text_spalte] else '' buchung = { 'datum': datum, 'buchungstext': buchungstext, 'betrag': betrag, 'umsatzart': row.get('Umsatzart', '').strip() if row.get('Umsatzart') else '' } buchungen.append(buchung) except Exception as e: print(f"⚠️ Zeile übersprungen (Fehler: {e}): {row}") continue return buchungen def filtere_buchungen(buchungen, monate, jahre): """Filtert Buchungen nach ausgewählten Monaten und Jahren""" gefiltert = [] for buchung in buchungen: if buchung['datum'].month in monate and buchung['datum'].year in jahre: gefiltert.append(buchung) return gefiltert # ===== DBF MANUELL ERSTELLEN ===== # --- START: ERSETZEN SIE IHRE ALTE FUNKTION DURCH DIESE --- def erstelle_dbf_manuell(mandant, buchungen, mieter, kostenkonten, projekte, default_projekt, ausgabe_datei_praefix, start_beleg_nr): """ Gruppiert Buchungen nach Monat und erstellt für jeden Monat eine eigene, Byte-genaue DBF-Datei. Returns: letzte_beleg_nr """ # NEU: Schritt 1 - Buchungen nach Monat gruppieren buchungen_pro_monat = {} for buchung in buchungen: monats_schluessel = buchung['datum'].strftime('%Y-%m') # z.B. '2023-10' if monats_schluessel not in buchungen_pro_monat: buchungen_pro_monat[monats_schluessel] = [] buchungen_pro_monat[monats_schluessel].append(buchung) print(f"\nFunde {len(buchungen_pro_monat)} verschiedene Monate in den Buchungsdaten.") beleg_nr = start_beleg_nr # NEU: Schritt 2 - Für jeden Monat eine eigene DBF-Datei erstellen for monat_key, monats_buchungen in buchungen_pro_monat.items(): jahr, monat = map(int, monat_key.split('-')) jahr_kurz = str(jahr)[-2:] # NEU: Dateinamen für diesen Monat generieren ausgabe_datei = f"{ausgabe_datei_praefix}_{monat:02d}{jahr_kurz}.dbf" print(f"\n{'='*70}") print(f"Erstelle DBF-Datei für Monat {monat_key}: {ausgabe_datei}") print(f"{'='*70}") # --- AB HIER BEGINNT IHRE BEWÄHRTE LOGIK, ANGEPASST FÜR EINE DATEI --- # DBF Header (32 bytes) - angepasst für die Anzahl der Monatsbuchungen heute = datetime.now() num_records = len(monats_buchungen) header_len = 32 + 43 * 32 + 1 # 32 Header + 43 Felder * 32 + 1 Terminator record_len = 368 # 1 (deletion) + 367 (fields) header = bytearray(32) header[0] = 0x03 # dBase III header[1] = heute.year - 1900 header[2] = heute.month header[3] = heute.day struct.pack_into(' 0 else f"{int(value):{length}d}" record[pos:pos+length] = formatted.rjust(length)[:length].encode('ascii') else: encoded = value.encode('cp850')[:length] record[pos:pos+length] = encoded.ljust(length) pos += length # Felder schreiben (Ihre exakte Reihenfolge) write_field(datum.day, 2, is_numeric=True); write_field(datum.month, 2, is_numeric=True) write_field(beleg_str, 10); write_field(konto_num, 8, is_numeric=True) write_field(gegenkonto_num, 8, is_numeric=True); write_field(kostenstelle, 8) write_field('', 10); write_field(betrag, 14, decimals=2, is_numeric=True) write_field(0.00, 5, decimals=2, is_numeric=True); write_field(0.00, 14, decimals=2, is_numeric=True) write_field(buchungstext, 30); write_field(0.00, 14, decimals=2, is_numeric=True) write_field('', 3); write_field(0, 8, is_numeric=True); write_field(True, 1, is_logical=True) write_field(betrag, 14, decimals=2, is_numeric=True); write_field(None, 14, empty=True) write_field(None, 1, empty=True); write_field(text2, 30); write_field(None, 1, empty=True) write_field(standard_datum, 8, is_date=True); write_field(False, 1, is_logical=True) write_field(datum.year, 4, is_numeric=True); write_field('', 20); write_field(None, 8, empty=True) write_field(None, 15, empty=True); write_field(0.00, 14, decimals=2, is_numeric=True) write_field(standard_datum, 8, is_date=True); write_field(None, 1, empty=True) write_field(False, 1, is_logical=True); write_field(0, 8, is_numeric=True) write_field('', 20); write_field(0.00, 14, decimals=2, is_numeric=True) write_field(None, 6, empty=True); write_field('', 3); write_field(0, 1, is_numeric=True) write_field(standard_datum, 8, is_date=True); write_field(None, 1, empty=True) write_field(None, 1, empty=True); write_field('', 1); write_field(0, 4, is_numeric=True) write_field(None, 8, empty=True); write_field('', 15) f.write(record) beleg_nr += 1 f.write(b'\x1A') # EOF print(f"✓ DBF-Datei '{ausgabe_datei}' erfolgreich mit {num_records} Einträgen erstellt.") letzte_beleg_nr = beleg_nr - 1 return letzte_beleg_nr # --- ENDE: BIS HIER ALLES ERSETZEN --- # ===== HILFSFUNKTIONEN ===== def parse_monatseingabe(eingabe): """Parst Monatseingabe wie '1,2,3' oder '1-3' oder '12' """ 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(eingabe): """Parst Jahreseingabe wie '2024' oder '2023,2024' """ 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)) # ===== HAUPTPROGRAMM ===== def main(): print("=" * 70) print(" CSV zu DBF Konverter für Hausverwaltung (v2)") print("=" * 70) create_default_configs() mandanten = load_mandanten() print("\n📋 Verfügbare Mandanten:") for idx, m in enumerate(mandanten, 1): print(f" {idx}. {m['name']} (Nr. {m['nummer']}, Konto {m['konto']}, nächster Beleg: {m.get('beleg_index', 1)})") auswahl = int(input("\n➤ Mandant auswählen (Nummer): ")) - 1 mandant = mandanten[auswahl] print(f"\n✓ Mandant gewählt: {mandant['name']}") print(f" Mandantennummer: {mandant['nummer']}") print(f" Konto: {mandant['konto']}") print(f" Standard-Gegenkonto: {mandant['standard_gegenkonto']}") print(f" Start-Belegnummer: {mandant.get('beleg_index', 1)}") mieter = load_mieter(mandant['nummer']) print(f" Mieter geladen: {len(mieter)} Einträge") kostenkonten = load_kostenkonten(mandant['nummer']) print(f" Kostenkonten geladen: {len(kostenkonten)} Einträge") projekte, default_projekt = load_projekte(mandant['nummer']) print(f" Projekte geladen: {len(projekte)} Einträge") print(f" Default-Projekt: {default_projekt}") csv_datei = input("\n➤ CSV-Datei Pfad: ") alle_buchungen = lese_csv(csv_datei) print(f"\n✓ {len(alle_buchungen)} Buchungen aus CSV geladen") print("\n📅 Für welche Monate sollen DBF-Dateien erstellt werden?") print(" Beispiele: '12' oder '1,2,3' oder '1-12'") monate_eingabe = input("➤ Monate: ") monate = parse_monatseingabe(monate_eingabe) print(f"✓ Ausgewählte Monate: {', '.join(map(str, monate))}") print("\n📅 Für welche Jahre sollen DBF-Dateien erstellt werden?") print(" Beispiele: '2024' oder '2023,2024' oder '2023-2024'") jahre_eingabe = input("➤ Jahre: ") jahre = parse_jahreseingabe(jahre_eingabe) print(f"✓ Ausgewählte Jahre: {', '.join(map(str, jahre))}") gefilterte_buchungen = filtere_buchungen(alle_buchungen, monate, jahre) print(f"\n✓ {len(gefilterte_buchungen)} Buchungen gefiltert (von {len(alle_buchungen)} gesamt)") if len(gefilterte_buchungen) == 0: print("\n⚠️ Keine Buchungen für die ausgewählten Monate/Jahre gefunden!") return monate_str = '_'.join(map(str, monate)) if len(monate) <= 3 else f"{min(monate)}-{max(monate)}" jahre_str = '_'.join(map(str, jahre)) ausgabe_datei = f"buchungen_{mandant['nummer']}_M{monate_str}_J{jahre_str}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.dbf" print(f"\n🔨 Erstelle DBF-Datei: {ausgabe_datei}") print("-" * 70) start_beleg_nr = mandant.get('beleg_index', 1) letzte_beleg_nr = erstelle_dbf_manuell( mandant, gefilterte_buchungen, mieter, kostenkonten, projekte, default_projekt, ausgabe_datei, start_beleg_nr ) # Beleg-Index in Config aktualisieren mandant['beleg_index'] = letzte_beleg_nr + 1 save_mandanten(mandanten) print(f"\n✓ Mandanten-Config aktualisiert: Nächster Beleg startet bei {mandant['beleg_index']}") print("\n" + "=" * 70) print("✅ Fertig! DBF-Datei erfolgreich erstellt.") print("=" * 70) print(f"\n💾 Datei: {ausgabe_datei}") print(f"📊 Buchungen: {len(gefilterte_buchungen)}") print(f"🔢 Belegnummern: {start_beleg_nr} bis {letzte_beleg_nr}") print(f"📄 Mieter-Config: identitaeten_mieter_{mandant['nummer']}.json") print(f"📄 Kosten-Config: identitaeten_kosten_{mandant['nummer']}.json") print(f"📄 Projekte-Config: projekte_{mandant['nummer']}.json") if __name__ == "__main__": main()