# -*- coding: utf-8 -*- """ Konfiguration fuer das Rechnungstool Campinghof Bartl. Haelt die Standardwerte (Firmendaten, Steuersaetze, Leistungskatalog, Rechnungsnummer-Zaehler) und kuemmert sich um Laden/Speichern der config.json. Alle Werte sind ueber das Einstellungs-Menue in der App aenderbar. """ import json import os import sys # Pfade je nach Modus: # - normal (python): alles neben den .py-Dateien # - eingefroren (.exe): config.json/rechnungen NEBEN der .exe (beschreibbar), # gebuendelte Ressourcen (Logo) liegen in sys._MEIPASS if getattr(sys, "frozen", False): BASE_DIR = os.path.dirname(sys.executable) RESOURCE_DIR = getattr(sys, "_MEIPASS", BASE_DIR) else: BASE_DIR = os.path.dirname(os.path.abspath(__file__)) RESOURCE_DIR = BASE_DIR CONFIG_PATH = os.path.join(BASE_DIR, "config.json") # Leistungskatalog 1:1 aus der Excel-Vorlage (Rechnung Campinghof Bartl). # satz = Umsatzsteuersatz in Prozent (7 = Beherbergung, 19 = Zusatzleistung) # preis = Brutto-Einzelpreis in Euro (Endpreis inkl. USt) # pro_person = True -> Gesamt = anzahl * preis * personen (Pos. 5 + 6) # False -> Gesamt = anzahl * preis DEFAULT_KATALOG = [ {"nr": 1, "leistung": "SP Wohnwagen/ Wohnmobil pro Nacht inkl. Pkw, Strom, Entsorgung, Markise oder Vorzelt", "satz": 7, "preis": 13.0, "pro_person": False}, {"nr": 2, "leistung": "SP Zelt bis 2 Personen inkl. Strom", "satz": 7, "preis": 9.0, "pro_person": False}, {"nr": 3, "leistung": "SP Zelt bis 4 Personen inkl. Strom", "satz": 7, "preis": 11.0, "pro_person": False}, {"nr": 4, "leistung": "SP Zelt bis 8 Personen inkl. Strom", "satz": 7, "preis": 14.0, "pro_person": False}, {"nr": 5, "leistung": "Personengebühr Erwachsene ab 15 Jahre", "satz": 7, "preis": 8.0, "pro_person": True}, {"nr": 6, "leistung": "Personengebühr Kinder ab 2 Jahre bis 14 Jahre", "satz": 7, "preis": 5.0, "pro_person": True}, {"nr": 7, "leistung": "Gästezimmer belegt mit 1-2 Personen pro Nacht", "satz": 7, "preis": 60.0, "pro_person": True}, {"nr": 8, "leistung": "Gästezimmer jede weitere Person pro Nacht", "satz": 7, "preis": 15.0, "pro_person": True}, {"nr": 9, "leistung": "Waschmaschinengebühr pro Wäsche", "satz": 19, "preis": 4.0, "pro_person": False}, {"nr": 10, "leistung": "Frischwasser tanken Wohnmobil", "satz": 19, "preis": 4.0, "pro_person": False}, {"nr": 11, "leistung": "Fahrradverleih", "satz": 19, "preis": 15.0, "pro_person": False}, {"nr": 12, "leistung": "Strompauschale für Heizung/Klimaanlage", "satz": 19, "preis": 7.0, "pro_person": False}, {"nr": 13, "leistung": "Bettwäsche-Paket pro Person", "satz": 19, "preis": 5.0, "pro_person": False}, {"nr": 14, "leistung": "Handtücher-Paket pro Person", "satz": 19, "preis": 5.0, "pro_person": False}, {"nr": 15, "leistung": "Endreinigung Ferienwohnung", "satz": 19, "preis": 40.0, "pro_person": False}, {"nr": 16, "leistung": "Gasfüllung 5 Kg (zuzüglich Pfand 30 €)", "satz": 19, "preis": 18.5, "pro_person": False}, {"nr": 17, "leistung": "Gasfüllung 11 Kg (zuzüglich Pfand 35 €)", "satz": 19, "preis": 31.5, "pro_person": False}, ] # Firmen- und Fusszeilen-Daten – Platzhalter. # Die echten Daten stehen in der config.json neben dem Programm (nicht im Repo) # und werden im Menü "Einstellungen -> Firmendaten & Texte" gepflegt. DEFAULT_FIRMA = { "name": "Mein Campingplatz", "untertitel": "Camping & Ferienwohnung", "absender": "Mein Campingplatz - Musterstraße 1 - 00000 Musterstadt", "inhaber": "Inhaber Vorname Nachname", "bank": "Bank Musterbank", "iban": "IBAN DE00 0000 0000 0000 0000 00", "bic": "SWIFT/BIC XXXXDE00XXX", "steuernummer":"Steuernummer 000/000/00000", "homepage": "Homepage www.example.de", "email": "E-Mail kontakt@example.de", "telefon": "Telefon +49 (0) 000 0000000", "logo": "assets/logo.png", # Bilddatei -> oben links auf der Rechnung (relativ zum Tool-Ordner) } # Festtexte unten auf der Rechnung (aus der Vorlage). DEFAULT_TEXTE = { "dank1": "Vielen Dank für Ihren Besuch auf unserem Campinghof. Wir hoffen, Sie hatten einen " "angenehmen Aufenthalt und würden uns freuen, Sie bald wieder bei uns begrüßen zu dürfen.", "dank2": "Unser Campinghof hat 365 Tage für Sie geöffnet. Wir bieten auch über den Winter " "Abstellplätze für einen monatlichen Betrag von 50,00 € an.", "hinweis": "* Bei Zahlung per Überweisung bitte die Rechnungsnummer angeben *", } DEFAULT_CONFIG = { "firma": DEFAULT_FIRMA, "texte": DEFAULT_TEXTE, "katalog": DEFAULT_KATALOG, # Alle Steuern als EINE benannte Liste. # - extra_blatt = False: USt-Satz, wird auf das Netto der zugeordneten Leistungen gerechnet. # - extra_blatt = True : Aufschlag auf die Endsumme (z.B. Beherbergungssteuer), eigenes Blatt. "steuersaetze": [ {"satz": 7, "bezeichnung": "Mehrwertsteuer", "extra_blatt": False}, {"satz": 19, "bezeichnung": "Mehrwertsteuer", "extra_blatt": False}, {"satz": 5, "bezeichnung": "Beherbergungssteuer", "extra_blatt": True}, ], # Rechnungsnummer im Format JJJJ-NNN (z.B. 2026-013). Zaehler des zuletzt # vergebenen Werts -> beim Anlegen wird +1 vorgeschlagen. "rechnung_jahr": 2026, "rechnung_zaehler": 13, # Ordner, in dem die fertigen PDF-Rechnungen gespeichert werden. # Leer -> beim ersten Start wird der Nutzer gefragt (Default siehe default_output_dir()). "output_dir": "", } def _merge(default, loaded): """Tief mergen: fehlende Keys aus default ergaenzen, vorhandene aus loaded behalten.""" if isinstance(default, dict) and isinstance(loaded, dict): out = {} for k, v in default.items(): out[k] = _merge(v, loaded[k]) if k in loaded else v # zusaetzliche Keys aus loaded mitnehmen (z.B. erweiterter Katalog) for k, v in loaded.items(): if k not in out: out[k] = v return out return loaded def load_config(): """config.json laden; fehlende Werte aus DEFAULT_CONFIG ergaenzen. Legt Datei an, falls fehlend.""" if not os.path.exists(CONFIG_PATH): save_config(DEFAULT_CONFIG) return json.loads(json.dumps(DEFAULT_CONFIG)) try: with open(CONFIG_PATH, "r", encoding="utf-8") as f: loaded = json.load(f) cfg = _merge(DEFAULT_CONFIG, loaded) return cfg except (json.JSONDecodeError, OSError) as e: print(f"[config] Fehler beim Laden ({e}) - nutze Defaults") return json.loads(json.dumps(DEFAULT_CONFIG)) def save_config(cfg): """config.json schreiben (UTF-8, eingerueckt).""" try: with open(CONFIG_PATH, "w", encoding="utf-8") as f: json.dump(cfg, f, ensure_ascii=False, indent=2) print(f"[config] gespeichert -> {CONFIG_PATH}") except OSError as e: print(f"[config] FEHLER beim Speichern: {e}") raise def format_rechnungsnummer(jahr, zaehler): """2026, 13 -> '2026-013'""" return f"{jahr}-{zaehler:03d}" def default_output_dir(): """Sinnvoller Standardordner, der auf jedem Windows-Profil existiert.""" return os.path.join(os.path.expanduser("~"), "Documents", "Campinghof Rechnungen") def ensure_output_dir(cfg=None): """ Gibt den konfigurierten Ausgabeordner zurueck und legt ihn an. Faellt bei leerem/unbrauchbarem Pfad auf default_output_dir() zurueck. """ d = "" if cfg: d = (cfg.get("output_dir") or "").strip() if not d: d = default_output_dir() try: os.makedirs(d, exist_ok=True) return d except OSError as e: print(f"[config] Ausgabeordner '{d}' nicht nutzbar ({e}) -> Standard") d = default_output_dir() os.makedirs(d, exist_ok=True) return d