# -*- 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): EXE_DIR = os.path.dirname(sys.executable) RESOURCE_DIR = getattr(sys, "_MEIPASS", EXE_DIR) else: EXE_DIR = os.path.dirname(os.path.abspath(__file__)) RESOURCE_DIR = EXE_DIR def _beschreibbar(ordner): """Kann in diesen Ordner geschrieben werden? Wird ausprobiert, nicht geraten.""" try: os.makedirs(ordner, exist_ok=True) probe = os.path.join(ordner, ".schreibprobe") with open(probe, "w") as f: f.write("x") os.remove(probe) return True except OSError: return False def _daten_ordner(): """Der Ordner, den das Programm sich beim ersten Start selbst anlegt. Alles, was das Programm schreibt, liegt darin beieinander - man sieht auf einen Blick, was von ihm stammt, und kann es mitnehmen oder sichern. 1. neben der EXE ein Unterordner "Daten" 2. geht das nicht (EXE liegt z.B. unter "Programme"), dann der Ordner Rechnungstool unter LOCALAPPDATA Was schon da ist, wird benutzt; was fehlt, wird angelegt. Lagen die Dateien noch direkt neben der EXE - so war es frueher -, ziehen sie mit um, statt dass das Programm ohne Firmendaten und Zaehler neu anfaengt. """ if not getattr(sys, "frozen", False): return EXE_DIR # aus der Quelle heraus bleibt alles, wo es ist kandidat = os.path.join(EXE_DIR, "Daten") if not _beschreibbar(kandidat): kandidat = os.path.join(os.environ.get("LOCALAPPDATA", EXE_DIR), "Rechnungstool") if not _beschreibbar(kandidat): return EXE_DIR # letzter Ausweg: wie frueher for name in ("config.json",): alt, neu_ = os.path.join(EXE_DIR, name), os.path.join(kandidat, name) if os.path.exists(alt) and not os.path.exists(neu_): try: os.replace(alt, neu_) print(f"[config] {name} in den Datenordner übernommen") except OSError: pass return kandidat BASE_DIR = _daten_ordner() 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}, ] # Die Montage-Rechnung ist eine eigene Sorte Beleg (eigener Reiter im Fenster): # ein Monteur mietet monatsweise, nicht naechteweise. Deshalb steht sie NICHT im # Leistungskatalog - sie hat eine einzige Zeile mit einem Monatspreis, und im # Katalog wuerde sie zwischen den Nachtpreisen nur Unheil stiften. # preis = BRUTTO je Monat (wie im Katalog), die USt wird herausgerechnet. # Die Beherbergungssteuer kommt wie ueberall obendrauf (500 + 5 % = 525). DEFAULT_MONTAGE = { "leistung": "Miete für Montagearbeiter", "satz": 7, "preis": 500.0, } # 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, "montage": DEFAULT_MONTAGE, # Das Angebot hat eine EIGENE Reihe (2026-A001): es darf keine # Rechnungsnummer verbrauchen - die gehoert zu einer echten Rechnung. "angebot_jahr": 0, "angebot_zaehler": 0, # 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