Oberflaeche im Pcore-Stil: Tuerkis, Kacheln, Glas-Knoepfe, Roboto

Seine Ansage: "schau dir unsern fivem server an und unsere webseiten, dann
weisst du wie mein design ist" - also die Palette aus _ui_www und der
Adminpage: Tuerkis #06C6A4 auf gestuften dunklen Flaechen (#0d0e10 Grund,
#15171b Kachel), Raender #25282e, Text #e8e8ea / #8a8e96, Rot #e85a5a.

Das PROGRAMM traegt diese Handschrift, die RECHNUNG die Farbe der jeweiligen
Firma - das Blatt gehoert dem Kunden, nicht uns.

- Roboto wird mitgeliefert und nur fuer den Prozess geladen
  (AddFontResourceEx mit FR_PRIVATE): keine Installation, keine Adminrechte,
  nach dem Schliessen ist der Rechner wie vorher. Fehlt sie, bleibt es bei
  Segoe UI. Die Schriftdateien gehen mit in die exe (spec).
- Abschnitte sind Kacheln mit Ueberschrift statt grauer Rahmen.
- Knoepfe im Glas-Stil wie in seinen Oberflaechen: getoente Flaeche, Rand in
  Akzent-Transparenz. Tk kann kein backdrop-filter - die Toenung wird
  ausgerechnet (theme.mischen), das Ergebnis ist auf einfarbigem Grund
  identisch. "PDF erstellen" fuehrt, "Storno" ist rot.
- Zebrastreifen in der Leistungsliste: bei 17 Zeilen verrutscht man sonst.
- Kein expand am offenen Reiter mehr - dadurch sprang beim Umschalten die
  ganze Leiste ("das taben macht den aktiven kleiner").
- Das Fenster fuer den gemeinsamen Ordner stand als einziges noch in Weiss da:
  es kam nie durch _style_dialog.

Dazu ein Fund, der nichts mit dem Design zu tun hat: die Pruefstaende erbten
"nummer_eingerichtet" aus der config DIESES Rechners. Sobald hier jemand die
Nummer einmal einrichtet, liefen sie in einen anderen Zweig und wurden rot -
ohne dass sich am Programm etwas geaendert haette. Sie legen den Zustand jetzt
selbst fest. Alle 14 gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
This commit is contained in:
TheMockTv 2026-09-05 18:52:42 +02:00
parent 0ad03fd1a4
commit 5ebb02b267
17 changed files with 330 additions and 87 deletions

110
app.py
View file

@ -39,6 +39,7 @@ from bestand import (hoechster_zaehler_im_ordner, naechste_freie_nummer,
vorhandene_rechnungen) vorhandene_rechnungen)
from einzelinstanz import (eigene_pid_merken, einzelinstanz_pruefen, fenster_nach_vorn, from einzelinstanz import (eigene_pid_merken, einzelinstanz_pruefen, fenster_nach_vorn,
laufende_pid_lesen) laufende_pid_lesen)
import theme
from theme import ThemeMixin from theme import ThemeMixin
from korrektur import KorrekturMixin from korrektur import KorrekturMixin
from einstellungen import EinstellungenMixin from einstellungen import EinstellungenMixin
@ -202,12 +203,13 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
# ---------------------------------------------------------------- Kopf # ---------------------------------------------------------------- Kopf
def _build_kopf(self): def _build_kopf(self):
top = ttk.Frame(self, padding=(12, 10, 12, 4)) top = ttk.Frame(self, padding=(16, 14, 16, 6))
top.pack(fill="x") top.pack(fill="x")
# Kunde (links) # Kunde (links)
kunde = ttk.LabelFrame(top, text="Rechnung an (Kunde)", padding=8) kunde = ttk.LabelFrame(top, text="RECHNUNG AN (KUNDE)", padding=14,
kunde.grid(row=0, column=0, sticky="nsew", padx=(0, 10)) style="Karte.TLabelframe")
kunde.grid(row=0, column=0, sticky="nsew", padx=(0, 14))
self.k_anrede = tk.StringVar() self.k_anrede = tk.StringVar()
self.k_vorname = tk.StringVar() self.k_vorname = tk.StringVar()
self.k_name = tk.StringVar() # Nachname oder Firma – die Zuordnung im Steuerjournal self.k_name = tk.StringVar() # Nachname oder Firma – die Zuordnung im Steuerjournal
@ -223,7 +225,8 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
("PLZ / Ort *", self.k_plzort), ("PLZ / Ort *", self.k_plzort),
] ]
for i, (lbl, var) in enumerate(felder): for i, (lbl, var) in enumerate(felder):
ttk.Label(kunde, text=lbl).grid(row=i, column=0, sticky="w", pady=2) ttk.Label(kunde, text=lbl, style="Karte.TLabel").grid(
row=i, column=0, sticky="w", pady=4)
if var is self.k_anrede: if var is self.k_anrede:
# Auswahlliste, aber nicht gesperrt - Sonderfaelle kann man tippen. # Auswahlliste, aber nicht gesperrt - Sonderfaelle kann man tippen.
w = ttk.Combobox(kunde, textvariable=var, width=36, values=ANREDEN) w = ttk.Combobox(kunde, textvariable=var, width=36, values=ANREDEN)
@ -232,12 +235,13 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
w.grid(row=i, column=1, sticky="ew", padx=(8, 0), pady=2) w.grid(row=i, column=1, sticky="ew", padx=(8, 0), pady=2)
ttk.Label(kunde, text="* Pflicht – Name und Anschrift des Kunden gehören " ttk.Label(kunde, text="* Pflicht – Name und Anschrift des Kunden gehören "
"auf die Rechnung (§ 14 Abs. 4 Nr. 1 UStG).", "auf die Rechnung (§ 14 Abs. 4 Nr. 1 UStG).",
foreground=self.col_muted, wraplength=330, justify="left").grid( style="KarteMuted.TLabel", wraplength=330, justify="left").grid(
row=len(felder), column=0, columnspan=2, sticky="w", pady=(6, 0)) row=len(felder), column=0, columnspan=2, sticky="w", pady=(6, 0))
kunde.columnconfigure(1, weight=1) kunde.columnconfigure(1, weight=1)
# Rechnungsinfos (rechts) # Rechnungsinfos (rechts)
info = ttk.LabelFrame(top, text="Rechnungsdaten", padding=8) info = ttk.LabelFrame(top, text="RECHNUNGSDATEN", padding=14,
style="Karte.TLabelframe")
info.grid(row=0, column=1, sticky="nsew") info.grid(row=0, column=1, sticky="nsew")
self.r_nummer = tk.StringVar() self.r_nummer = tk.StringVar()
self.r_datum = tk.StringVar(value=datetime.date.today().strftime("%d.%m.%Y")) self.r_datum = tk.StringVar(value=datetime.date.today().strftime("%d.%m.%Y"))
@ -253,8 +257,8 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
date_vars = (self.r_datum, self.r_anreise, self.r_abreise) date_vars = (self.r_datum, self.r_anreise, self.r_abreise)
self.datum_labels = {} self.datum_labels = {}
for i, (lbl, var) in enumerate(rows): for i, (lbl, var) in enumerate(rows):
beschriftung = ttk.Label(info, text=lbl) beschriftung = ttk.Label(info, text=lbl, style="Karte.TLabel")
beschriftung.grid(row=i, column=0, sticky="w", pady=2) beschriftung.grid(row=i, column=0, sticky="w", pady=4)
if var is self.r_anreise: if var is self.r_anreise:
self.datum_labels["von"] = beschriftung self.datum_labels["von"] = beschriftung
elif var is self.r_abreise: elif var is self.r_abreise:
@ -267,14 +271,16 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
self.nummer_entry = ent self.nummer_entry = ent
if var in date_vars: # Datum: Punkte automatisch, Cursor ans Ende if var in date_vars: # Datum: Punkte automatisch, Cursor ans Ende
ent.bind("<KeyRelease>", self._auto_datum_evt) ent.bind("<KeyRelease>", self._auto_datum_evt)
self.naechte_label = ttk.Label(info, text="Nächte") self.naechte_label = ttk.Label(info, text="Nächte", style="Karte.TLabel")
self.naechte_label.grid(row=4, column=0, sticky="w", pady=2) self.naechte_label.grid(row=4, column=0, sticky="w", pady=4)
ttk.Label(info, textvariable=self.r_naechte, font=("Segoe UI", 10, "bold")).grid( ttk.Label(info, textvariable=self.r_naechte, style="Karte.TLabel",
font=(theme.SCHRIFT_NAME, 11, "bold")).grid(
row=4, column=1, sticky="w", padx=(8, 0)) row=4, column=1, sticky="w", padx=(8, 0))
# Storno-Hinweis: steht nur da, solange ein Storno im Zwischenspeicher liegt. # Storno-Hinweis: steht nur da, solange ein Storno im Zwischenspeicher liegt.
self.storno_hinweis = tk.StringVar(value="") self.storno_hinweis = tk.StringVar(value="")
ttk.Label(info, textvariable=self.storno_hinweis, foreground="#b00020", ttk.Label(info, textvariable=self.storno_hinweis, style="Karte.TLabel",
font=("Segoe UI", 9, "bold"), wraplength=200, justify="left").grid( foreground=self.col_gefahr,
font=(theme.SCHRIFT_NAME, 9, "bold"), wraplength=200, justify="left").grid(
row=5, column=0, columnspan=2, sticky="w", pady=(6, 0)) row=5, column=0, columnspan=2, sticky="w", pady=(6, 0))
info.columnconfigure(1, weight=1) info.columnconfigure(1, weight=1)
self.r_anreise.trace_add("write", lambda *a: self._update_naechte()) self.r_anreise.trace_add("write", lambda *a: self._update_naechte())
@ -369,10 +375,11 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
self.blaetter.add(montage_tab, text=" Montage ") self.blaetter.add(montage_tab, text=" Montage ")
self._build_montage(montage_tab) self._build_montage(montage_tab)
wrap = ttk.LabelFrame(camping, text="Leistungen – Anzahl (und ggf. Personen) eintragen", padding=(6, 4)) wrap = ttk.LabelFrame(camping, text="LEISTUNGEN – ANZAHL (UND GGF. PERSONEN) EINTRAGEN",
padding=(12, 8), style="Karte.TLabelframe")
wrap.pack(side="top", fill="both", expand=True) wrap.pack(side="top", fill="both", expand=True)
canvas = tk.Canvas(wrap, highlightthickness=0, bg=self.col_bg) canvas = tk.Canvas(wrap, highlightthickness=0, bg=self.col_card)
sb = ttk.Scrollbar(wrap, orient="vertical", command=canvas.yview) sb = ttk.Scrollbar(wrap, orient="vertical", command=canvas.yview)
self.pos_frame = ttk.Frame(canvas) self.pos_frame = ttk.Frame(canvas)
self._pos_win = canvas.create_window((0, 0), window=self.pos_frame, anchor="nw") self._pos_win = canvas.create_window((0, 0), window=self.pos_frame, anchor="nw")
@ -399,10 +406,11 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
# Kopfzeile (gleiches Grid wie die Daten) # Kopfzeile (gleiches Grid wie die Daten)
for i, (title, anchor, _) in enumerate(self.COLS): for i, (title, anchor, _) in enumerate(self.COLS):
ttk.Label(self.pos_frame, text=title, anchor=anchor, ttk.Label(self.pos_frame, text=title.upper(), anchor=anchor,
font=("Segoe UI", 9, "bold")).grid(row=0, column=i, padx=3, pady=(0, 3), sticky="ew") style="Abschnitt.TLabel").grid(
row=0, column=i, padx=3, pady=(0, 5), sticky="ew")
ttk.Separator(self.pos_frame, orient="horizontal").grid( ttk.Separator(self.pos_frame, orient="horizontal").grid(
row=1, column=0, columnspan=len(self.COLS), sticky="ew", pady=(0, 2)) row=1, column=0, columnspan=len(self.COLS), sticky="ew", pady=(0, 4))
for idx, pos in enumerate(self.cfg["katalog"]): for idx, pos in enumerate(self.cfg["katalog"]):
r = idx + 2 r = idx + 2
@ -414,17 +422,26 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
av.trace_add("write", lambda *a: self.recalc()) av.trace_add("write", lambda *a: self.recalc())
pv.trace_add("write", lambda *a: self.recalc()) pv.trace_add("write", lambda *a: self.recalc())
ttk.Label(self.pos_frame, text=str(nr), anchor="center").grid(row=r, column=0, padx=3, pady=2, sticky="ew") # Jede zweite Zeile bekommt einen anderen Grund: bei 17 Positionen
# verrutscht man beim Eintragen sonst leicht eine Zeile.
zeilenstil = "Zebra.TLabel" if idx % 2 else "Karte.TLabel"
ttk.Label(self.pos_frame, text=str(nr), anchor="center", style=zeilenstil).grid(
row=r, column=0, padx=0, pady=3, sticky="ew", ipady=2)
ttk.Label(self.pos_frame, text=pos["leistung"], anchor="w", wraplength=320, ttk.Label(self.pos_frame, text=pos["leistung"], anchor="w", wraplength=320,
justify="left").grid(row=r, column=1, padx=3, pady=2, sticky="ew") justify="left", style=zeilenstil).grid(
ttk.Label(self.pos_frame, text=str(pos["satz"]), anchor="center").grid(row=r, column=2, padx=3, pady=2, sticky="ew") row=r, column=1, padx=0, pady=3, sticky="ew", ipady=2)
ttk.Entry(self.pos_frame, textvariable=av, width=8, justify="center").grid(row=r, column=3, padx=3, pady=2) ttk.Label(self.pos_frame, text=str(pos["satz"]), anchor="center",
style=zeilenstil).grid(row=r, column=2, padx=0, pady=3, sticky="ew", ipady=2)
ttk.Entry(self.pos_frame, textvariable=av, width=8, justify="center").grid(
row=r, column=3, padx=4, pady=3)
# Personen-Feld NUR bei "pro Person"-Positionen anzeigen # Personen-Feld NUR bei "pro Person"-Positionen anzeigen
if pos["pro_person"]: if pos["pro_person"]:
ttk.Entry(self.pos_frame, textvariable=pv, width=6, justify="center").grid(row=r, column=4, padx=3, pady=2) ttk.Entry(self.pos_frame, textvariable=pv, width=6, justify="center").grid(
ttk.Label(self.pos_frame, text=berechnung.eur_kurz(pos["preis"]), anchor="e").grid(row=r, column=5, padx=3, pady=2, sticky="ew") row=r, column=4, padx=4, pady=3)
g = ttk.Label(self.pos_frame, text="0,00", anchor="e") ttk.Label(self.pos_frame, text=berechnung.eur_kurz(pos["preis"]), anchor="e",
g.grid(row=r, column=6, padx=3, pady=2, sticky="ew") style=zeilenstil).grid(row=r, column=5, padx=0, pady=3, sticky="ew", ipady=2)
g = ttk.Label(self.pos_frame, text="0,00", anchor="e", style=zeilenstil)
g.grid(row=r, column=6, padx=0, pady=3, sticky="ew", ipady=2)
self.gesamt_lbls[nr] = g self.gesamt_lbls[nr] = g
# ------------------------------------------------------------ Montage # ------------------------------------------------------------ Montage
@ -440,7 +457,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
self.m_monate.trace_add("write", lambda *a: self.recalc()) self.m_monate.trace_add("write", lambda *a: self.recalc())
ttk.Label(eltern, text="Monatsmiete für einen Montagearbeiter", ttk.Label(eltern, text="Monatsmiete für einen Montagearbeiter",
font=("Segoe UI", 11, "bold")).grid(row=0, column=0, columnspan=4, sticky="w") font=(theme.SCHRIFT_NAME, 11, "bold")).grid(row=0, column=0, columnspan=4, sticky="w")
ttk.Label(eltern, wraplength=760, justify="left", foreground=self.col_muted, ttk.Label(eltern, wraplength=760, justify="left", foreground=self.col_muted,
text="Den Zeitraum trägst du oben rechts ein (dieselben Felder wie Anreise/Abreise). " text="Den Zeitraum trägst du oben rechts ein (dieselben Felder wie Anreise/Abreise). "
"Bezeichnung, Steuersatz und Monatspreis stehen unter " "Bezeichnung, Steuersatz und Monatspreis stehen unter "
@ -451,17 +468,17 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
ttk.Entry(eltern, textvariable=self.m_monate, width=8, justify="center").grid( ttk.Entry(eltern, textvariable=self.m_monate, width=8, justify="center").grid(
row=2, column=1, sticky="w", padx=(8, 24), pady=3) row=2, column=1, sticky="w", padx=(8, 24), pady=3)
ttk.Label(eltern, text="Monatspreis (brutto):").grid(row=2, column=2, sticky="w", pady=3) ttk.Label(eltern, text="Monatspreis (brutto):").grid(row=2, column=2, sticky="w", pady=3)
self.m_preis = ttk.Label(eltern, text="", font=("Segoe UI", 10, "bold")) self.m_preis = ttk.Label(eltern, text="", font=(theme.SCHRIFT_NAME, 10, "bold"))
self.m_preis.grid(row=2, column=3, sticky="w", padx=(8, 0), pady=3) self.m_preis.grid(row=2, column=3, sticky="w", padx=(8, 0), pady=3)
ttk.Separator(eltern, orient="horizontal").grid( ttk.Separator(eltern, orient="horizontal").grid(
row=3, column=0, columnspan=4, sticky="ew", pady=12) row=3, column=0, columnspan=4, sticky="ew", pady=12)
ttk.Label(eltern, text="So steht es auf der Rechnung:", ttk.Label(eltern, text="So steht es auf der Rechnung:",
font=("Segoe UI", 9, "bold")).grid(row=4, column=0, columnspan=4, sticky="w") font=(theme.SCHRIFT_NAME, 9, "bold")).grid(row=4, column=0, columnspan=4, sticky="w")
self.m_text = ttk.Label(eltern, text="", wraplength=760, justify="left") self.m_text = ttk.Label(eltern, text="", wraplength=760, justify="left")
self.m_text.grid(row=5, column=0, columnspan=4, sticky="w", pady=(4, 10)) self.m_text.grid(row=5, column=0, columnspan=4, sticky="w", pady=(4, 10))
self.m_zahlen = ttk.Label(eltern, text="", font=("Segoe UI", 10)) self.m_zahlen = ttk.Label(eltern, text="", font=(theme.SCHRIFT_NAME, 10))
self.m_zahlen.grid(row=6, column=0, columnspan=4, sticky="w") self.m_zahlen.grid(row=6, column=0, columnspan=4, sticky="w")
eltern.columnconfigure(3, weight=1) eltern.columnconfigure(3, weight=1)
@ -522,14 +539,14 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
status = ttk.Frame(self, padding=(12, 2)) status = ttk.Frame(self, padding=(12, 2))
status.pack(side="bottom", fill="x") status.pack(side="bottom", fill="x")
ttk.Label(status, text="powered by ravokk", foreground=self.col_muted, ttk.Label(status, text="powered by ravokk", foreground=self.col_muted,
font=("Segoe UI", 8)).pack(side="right") font=(theme.SCHRIFT_NAME, 8)).pack(side="right")
# Steht der gemeinsame Ordner? Das gehoert sichtbar hin - ohne ihn kennt # Steht der gemeinsame Ordner? Das gehoert sichtbar hin - ohne ihn kennt
# das Programm die Nummern des Steuerjournals nicht. # das Programm die Nummern des Steuerjournals nicht.
self.buch_lbl = ttk.Label(status, text="", foreground=self.col_muted, self.buch_lbl = ttk.Label(status, text="", foreground=self.col_muted,
font=("Segoe UI", 8)) font=(theme.SCHRIFT_NAME, 8))
self.buch_lbl.pack(side="left") self.buch_lbl.pack(side="left")
f = ttk.Frame(self, padding=(12, 6, 12, 10)) f = ttk.Frame(self, padding=(16, 12, 16, 14))
f.pack(side="bottom", fill="x") f.pack(side="bottom", fill="x")
ttk.Separator(self, orient="horizontal").pack(side="bottom", fill="x") ttk.Separator(self, orient="horizontal").pack(side="bottom", fill="x")
@ -539,13 +556,17 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
links = ttk.Frame(f) links = ttk.Frame(f)
links.pack(side="left") links.pack(side="left")
ttk.Button(links, text="Vorschau", command=self.vorschau_zeigen, width=12).pack(side="left", padx=4) # "PDF erstellen" ist der Knopf, um den es geht - der traegt die
ttk.Button(links, text="PDF erstellen", command=self.pdf_erstellen, width=18).pack(side="left", padx=4) # Akzentfarbe. Storno greift in eine ausgestellte Rechnung ein und ist
# deshalb rot markiert, nicht weil er gefaehrlich waere, sondern damit
# niemand ihn versehentlich statt "PDF erstellen" trifft.
ttk.Button(links, text="PDF erstellen", command=self.pdf_erstellen,
style="Haupt.TButton").pack(side="left", padx=(0, 6))
ttk.Button(links, text="Vorschau", command=self.vorschau_zeigen).pack(side="left", padx=6)
ttk.Button(links, text="Storno", command=self.dlg_storno, ttk.Button(links, text="Storno", command=self.dlg_storno,
width=12).pack(side="left", padx=4) style="Gefahr.TButton").pack(side="left", padx=6)
ttk.Button(links, text="Berichtigen", command=self.dlg_berichtigen, ttk.Button(links, text="Berichtigen", command=self.dlg_berichtigen).pack(side="left", padx=6)
width=14).pack(side="left", padx=4) ttk.Button(links, text="Zurücksetzen", command=self.reset_formular).pack(side="left", padx=6)
ttk.Button(links, text="Zurücksetzen", command=self.reset_formular, width=14).pack(side="left", padx=4)
# --------------------------------------------------- Gemeinsames Buch # --------------------------------------------------- Gemeinsames Buch
def _buch_oeffnen(self): def _buch_oeffnen(self):
@ -638,7 +659,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
rahmen = ttk.Frame(d, padding=18) rahmen = ttk.Frame(d, padding=18)
rahmen.pack(fill="both", expand=True) rahmen.pack(fill="both", expand=True)
ttk.Label(rahmen, text="Wo soll die Rechnungsnummer weiterlaufen?", ttk.Label(rahmen, text="Wo soll die Rechnungsnummer weiterlaufen?",
font=("Segoe UI", 13, "bold")).grid(row=0, column=0, columnspan=2, sticky="w") font=(theme.SCHRIFT_NAME, 13, "bold")).grid(row=0, column=0, columnspan=2, sticky="w")
ttk.Label(rahmen, wraplength=620, justify="left", text=( ttk.Label(rahmen, wraplength=620, justify="left", text=(
"Zwei Wege - such dir einen aus." + chr(10) + chr(10) + "Zwei Wege - such dir einen aus." + chr(10) + chr(10) +
"Das wird EINMAL eingerichtet. Danach steht der Zähler fest: eine " "Das wird EINMAL eingerichtet. Danach steht der Zähler fest: eine "
@ -649,7 +670,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
if gefunden: if gefunden:
tk.Label(rahmen, wraplength=620, justify="left", fg="#1f5c3d", tk.Label(rahmen, wraplength=620, justify="left", fg="#1f5c3d",
font=("Segoe UI", 9, "bold"), font=(theme.SCHRIFT_NAME, 9, "bold"),
text=f"Im Rechnungsordner und im Nummernbuch steht schon " text=f"Im Rechnungsordner und im Nummernbuch steht schon "
f"{jahr}-{gefunden:03d} – niedriger geht es nicht.").grid( f"{jahr}-{gefunden:03d} – niedriger geht es nicht.").grid(
row=2, column=0, columnspan=2, sticky="w", pady=(0, 10)) row=2, column=0, columnspan=2, sticky="w", pady=(0, 10))
@ -677,7 +698,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
eingabe.pack(side="left", padx=(8, 0)) eingabe.pack(side="left", padx=(8, 0))
weg_gewaehlt() weg_gewaehlt()
hinweis = tk.Label(rahmen, text="", wraplength=620, justify="left", fg="#a00000", hinweis = tk.Label(rahmen, text="", wraplength=620, justify="left", fg="#a00000",
font=("Segoe UI", 9)) font=(theme.SCHRIFT_NAME, 9))
hinweis.grid(row=5, column=0, columnspan=2, sticky="w", pady=(8, 0)) hinweis.grid(row=5, column=0, columnspan=2, sticky="w", pady=(8, 0))
def uebernehmen(): def uebernehmen():
@ -957,7 +978,8 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
rows.append(("Gesamtbetrag:", berechnung.eur(r.gesamt), True)) rows.append(("Gesamtbetrag:", berechnung.eur(r.gesamt), True))
for i, (lbl, val, fett) in enumerate(rows): for i, (lbl, val, fett) in enumerate(rows):
font = ("Segoe UI", 12, "bold") if fett else ("Segoe UI", 10) font = ((theme.SCHRIFT_NAME, 15, "bold") if fett
else (theme.SCHRIFT_NAME, 10))
pady = (5, 0) if fett else (1, 0) pady = (5, 0) if fett else (1, 0)
ttk.Label(self.summen_box, text=lbl, font=font).grid(row=i, column=0, sticky="e", padx=6, pady=pady) ttk.Label(self.summen_box, text=lbl, font=font).grid(row=i, column=0, sticky="e", padx=6, pady=pady)
vkw = {"font": font, "width": 14, "anchor": "e"} vkw = {"font": font, "width": 14, "anchor": "e"}
@ -1040,7 +1062,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
rahmen = ttk.Frame(d, padding=18) rahmen = ttk.Frame(d, padding=18)
rahmen.pack(fill="both", expand=True) rahmen.pack(fill="both", expand=True)
ttk.Label(rahmen, text=f"{wer or 'ohne Namen'} – {betrag}", ttk.Label(rahmen, text=f"{wer or 'ohne Namen'} – {betrag}",
font=("Segoe UI", 12, "bold")).grid(row=0, column=0, sticky="w") font=(theme.SCHRIFT_NAME, 12, "bold")).grid(row=0, column=0, sticky="w")
ttk.Label(rahmen, wraplength=560, justify="left", text=( ttk.Label(rahmen, wraplength=560, justify="left", text=(
f"RECHNUNG {nummer} – der Beleg. Danach lässt sie sich nicht mehr " f"RECHNUNG {nummer} – der Beleg. Danach lässt sie sich nicht mehr "

Binary file not shown.

BIN
assets/fonts/Roboto.ttf Normal file

Binary file not shown.

View file

@ -11,6 +11,7 @@ import tkinter as tk
from tkinter import ttk, messagebox, filedialog, colorchooser from tkinter import ttk, messagebox, filedialog, colorchooser
import berechnung import berechnung
import theme
import bilder import bilder
import config import config
from helfer import parse_num from helfer import parse_num
@ -50,7 +51,7 @@ class EinstellungenMixin:
self._style_dialog(d) self._style_dialog(d)
frm = ttk.Frame(d, padding=18) frm = ttk.Frame(d, padding=18)
frm.pack(fill="both", expand=True) frm.pack(fill="both", expand=True)
ttk.Label(frm, text="Rechnungstool Campinghof Bartl", font=("Segoe UI", 11, "bold"), ttk.Label(frm, text="Rechnungstool Campinghof Bartl", font=(theme.SCHRIFT_NAME, 11, "bold"),
foreground=self.col_accent).pack(anchor="w") foreground=self.col_accent).pack(anchor="w")
ttk.Label(frm, justify="left", ttk.Label(frm, justify="left",
text="Daten eingeben, PDF erstellen, drucken.\n" text="Daten eingeben, PDF erstellen, drucken.\n"
@ -74,7 +75,7 @@ class EinstellungenMixin:
frm = ttk.Frame(d, padding=12) frm = ttk.Frame(d, padding=12)
frm.pack(fill="both", expand=True) frm.pack(fill="both", expand=True)
ttk.Label(frm, wraplength=580, justify="left", font=("Segoe UI", 9, "bold"), ttk.Label(frm, wraplength=580, justify="left", font=(theme.SCHRIFT_NAME, 9, "bold"),
text="Steuersätze: Satz % + Name. Die Katalog-Preise sind BRUTTO; die USt dieses " text="Steuersätze: Satz % + Name. Die Katalog-Preise sind BRUTTO; die USt dieses "
"Satzes wird herausgerechnet und als 'enthalten' ausgewiesen. Haken bei " "Satzes wird herausgerechnet und als 'enthalten' ausgewiesen. Haken bei "
"'Aufschlag': diese Steuer (z.B. Beherbergungssteuer) wird stattdessen auf den " "'Aufschlag': diese Steuer (z.B. Beherbergungssteuer) wird stattdessen auf den "
@ -92,7 +93,7 @@ class EinstellungenMixin:
for w in liste.winfo_children(): for w in liste.winfo_children():
w.destroy() w.destroy()
for c, t in enumerate(["Satz %", "Bezeichnung (Name der Steuer)", "Aufschlag", ""]): for c, t in enumerate(["Satz %", "Bezeichnung (Name der Steuer)", "Aufschlag", ""]):
ttk.Label(liste, text=t, font=("Segoe UI", 9, "bold")).grid( ttk.Label(liste, text=t, font=(theme.SCHRIFT_NAME, 9, "bold")).grid(
row=0, column=c, padx=4, pady=(0, 3), sticky="w") row=0, column=c, padx=4, pady=(0, 3), sticky="w")
for i, row in enumerate(arbeit, start=1): for i, row in enumerate(arbeit, start=1):
ttk.Entry(liste, textvariable=row["satz"], width=8, justify="center").grid(row=i, column=0, padx=4, pady=2) ttk.Entry(liste, textvariable=row["satz"], width=8, justify="center").grid(row=i, column=0, padx=4, pady=2)
@ -247,7 +248,7 @@ class EinstellungenMixin:
for w in inner.winfo_children(): for w in inner.winfo_children():
w.destroy() w.destroy()
for c, t in enumerate(["Pos.", "Bezeichnung", "Steuersatz", "Preis €", "pro Person", ""]): for c, t in enumerate(["Pos.", "Bezeichnung", "Steuersatz", "Preis €", "pro Person", ""]):
ttk.Label(inner, text=t, font=("Segoe UI", 9, "bold")).grid( ttk.Label(inner, text=t, font=(theme.SCHRIFT_NAME, 9, "bold")).grid(
row=0, column=c, padx=4, pady=(0, 4), sticky="w") row=0, column=c, padx=4, pady=(0, 4), sticky="w")
for i, row in enumerate(arbeit, start=1): for i, row in enumerate(arbeit, start=1):
ttk.Label(inner, text=str(i), width=4).grid(row=i, column=0, padx=4, pady=1) ttk.Label(inner, text=str(i), width=4).grid(row=i, column=0, padx=4, pady=1)

View file

@ -23,6 +23,7 @@ import tkinter as tk
from tkinter import ttk, messagebox from tkinter import ttk, messagebox
import berechnung import berechnung
import theme
import config import config
import pdf_renderer import pdf_renderer
import berechnung import berechnung
@ -375,7 +376,7 @@ class KorrekturMixin:
rahmen = ttk.Frame(d, padding=18) rahmen = ttk.Frame(d, padding=18)
rahmen.pack(fill="both", expand=True) rahmen.pack(fill="both", expand=True)
ttk.Label(rahmen, text=f"Storno zu Rechnung {alt_nummer}", ttk.Label(rahmen, text=f"Storno zu Rechnung {alt_nummer}",
font=("Segoe UI", 13, "bold")).pack(anchor="w") font=(theme.SCHRIFT_NAME, 13, "bold")).pack(anchor="w")
# ---- Schritt 1: A oder B ------------------------------------------ # ---- Schritt 1: A oder B ------------------------------------------
schritt1 = ttk.Frame(rahmen) schritt1 = ttk.Frame(rahmen)
@ -384,17 +385,17 @@ class KorrekturMixin:
"Auf dem Storno-Blatt muss stehen, wie die Leistung stattdessen abgerechnet " "Auf dem Storno-Blatt muss stehen, wie die Leistung stattdessen abgerechnet "
"wird – sonst weiß der Gast nicht, woran er ist.")).pack(anchor="w", pady=(0, 12)) "wird – sonst weiß der Gast nicht, woran er ist.")).pack(anchor="w", pady=(0, 12))
ttk.Label(schritt1, text="A – es folgt eine neue Rechnung", ttk.Label(schritt1, text="A – es folgt eine neue Rechnung",
font=("Segoe UI", 10, "bold")).pack(anchor="w") font=(theme.SCHRIFT_NAME, 10, "bold")).pack(anchor="w")
ttk.Label(schritt1, wraplength=620, justify="left", text=( ttk.Label(schritt1, wraplength=620, justify="left", text=(
"Der übliche Weg: die Rechnung war falsch und wird nach dem Storno richtig neu " "Der übliche Weg: die Rechnung war falsch und wird nach dem Storno richtig neu "
"geschrieben. Das Formular bleibt danach offen.")).pack(anchor="w", pady=(2, 10)) "geschrieben. Das Formular bleibt danach offen.")).pack(anchor="w", pady=(2, 10))
ttk.Label(schritt1, text="B – die Leistung ist schon abgerechnet", ttk.Label(schritt1, text="B – die Leistung ist schon abgerechnet",
font=("Segoe UI", 10, "bold")).pack(anchor="w") font=(theme.SCHRIFT_NAME, 10, "bold")).pack(anchor="w")
ttk.Label(schritt1, wraplength=620, justify="left", text=( ttk.Label(schritt1, wraplength=620, justify="left", text=(
"Für Doppelte: derselbe Aufenthalt steht mehrfach im Ordner. Im nächsten Schritt " "Für Doppelte: derselbe Aufenthalt steht mehrfach im Ordner. Im nächsten Schritt "
"trägst du die Nummer der Rechnung ein, die bleibt.")).pack(anchor="w", pady=(2, 10)) "trägst du die Nummer der Rechnung ein, die bleibt.")).pack(anchor="w", pady=(2, 10))
ttk.Label(schritt1, text="C – nur Storno", ttk.Label(schritt1, text="C – nur Storno",
font=("Segoe UI", 10, "bold")).pack(anchor="w") font=(theme.SCHRIFT_NAME, 10, "bold")).pack(anchor="w")
ttk.Label(schritt1, wraplength=620, justify="left", text=( ttk.Label(schritt1, wraplength=620, justify="left", text=(
"Die Leistung wird gar nicht abgerechnet: Buchung geplatzt oder die Rechnung " "Die Leistung wird gar nicht abgerechnet: Buchung geplatzt oder die Rechnung "
"war komplett irrtümlich. Es folgt keine neue Rechnung und es gibt auch keine " "war komplett irrtümlich. Es folgt keine neue Rechnung und es gibt auch keine "
@ -411,7 +412,7 @@ class KorrekturMixin:
feld = ttk.Entry(zeile, textvariable=eingabe, width=16, justify="center") feld = ttk.Entry(zeile, textvariable=eingabe, width=16, justify="center")
feld.pack(side="left", padx=(8, 0)) feld.pack(side="left", padx=(8, 0))
stand = tk.Label(schritt2, text="", wraplength=620, justify="left", stand = tk.Label(schritt2, text="", wraplength=620, justify="left",
font=("Segoe UI", 9)) font=(theme.SCHRIFT_NAME, 9))
stand.pack(anchor="w", pady=(8, 0)) stand.pack(anchor="w", pady=(8, 0))
def suchen(*_a): def suchen(*_a):

View file

@ -25,6 +25,8 @@ import os
import tkinter as tk import tkinter as tk
from tkinter import ttk, filedialog, messagebox from tkinter import ttk, filedialog, messagebox
import theme
import gemeinsam import gemeinsam
BREITE = 780 BREITE = 780
@ -39,6 +41,18 @@ class OrdnerWahl(tk.Toplevel):
self.ergebnis = None self.ergebnis = None
self.title("Gemeinsamer Ordner für das Nummernbuch") self.title("Gemeinsamer Ordner für das Nummernbuch")
self.transient(master) self.transient(master)
# Dieses Fenster stand als einziges noch in Weiss da: es kam nie durch
# _style_dialog. Farben holt es sich jetzt vom Hauptfenster, damit es
# nicht wieder auseinanderlaeuft, wenn dort etwas geaendert wird.
self._bg = getattr(master, "col_bg", "#0d0e10")
self._fg = getattr(master, "col_fg", "#e8e8ea")
self._warn = getattr(master, "col_gefahr", "#e85a5a")
self._gut = getattr(master, "col_accent", "#06C6A4")
self._muted = getattr(master, "col_muted", "#8a8e96")
if hasattr(master, "_style_dialog"):
master._style_dialog(self)
else:
self.configure(bg=self._bg)
self.pfad = tk.StringVar(value=vorgabe or "") self.pfad = tk.StringVar(value=vorgabe or "")
rahmen = ttk.Frame(self, padding=18) rahmen = ttk.Frame(self, padding=18)
@ -47,7 +61,7 @@ class OrdnerWahl(tk.Toplevel):
breite = BREITE - 60 breite = BREITE - 60
ttk.Label(rahmen, text="Bitte den gemeinsamen Ordner auswählen", ttk.Label(rahmen, text="Bitte den gemeinsamen Ordner auswählen",
font=("Segoe UI", 13, "bold")).grid(row=0, column=0, sticky="w") font=(theme.SCHRIFT_NAME, 13, "bold")).grid(row=0, column=0, sticky="w")
ttk.Label(rahmen, wraplength=breite, justify="left", text=( ttk.Label(rahmen, wraplength=breite, justify="left", text=(
"Rechnungstool und Steuerjournal führen zusammen ein Nummernbuch. Darin steht " "Rechnungstool und Steuerjournal führen zusammen ein Nummernbuch. Darin steht "
"jede vergebene Rechnungsnummer – auch die aus der alten Excel-Tabelle, zu der " "jede vergebene Rechnungsnummer – auch die aus der alten Excel-Tabelle, zu der "
@ -56,8 +70,8 @@ class OrdnerWahl(tk.Toplevel):
"eine Nummer ein zweites Mal. Jede Nummer darf es aber nur einmal geben " "eine Nummer ein zweites Mal. Jede Nummer darf es aber nur einmal geben "
"(§ 14 Abs. 4 Nr. 4 UStG).")).grid(row=1, column=0, sticky="w", pady=(8, 12)) "(§ 14 Abs. 4 Nr. 4 UStG).")).grid(row=1, column=0, sticky="w", pady=(8, 12))
tk.Label(rahmen, wraplength=breite, justify="left", fg="#a00000", tk.Label(rahmen, wraplength=breite, justify="left", fg=self._warn, bg=self._bg,
font=("Segoe UI", 10, "bold"), font=(theme.SCHRIFT_NAME, 10, "bold"),
text="WICHTIG: In BEIDEN Programmen muss derselbe Ordner stehen – am besten " text="WICHTIG: In BEIDEN Programmen muss derselbe Ordner stehen – am besten "
"der Ordner, in dem die Programme liegen.").grid( "der Ordner, in dem die Programme liegen.").grid(
row=2, column=0, sticky="w", pady=(0, 14)) row=2, column=0, sticky="w", pady=(0, 14))
@ -71,7 +85,8 @@ class OrdnerWahl(tk.Toplevel):
width=26).grid(row=0, column=1, sticky="w", padx=(10, 0)) width=26).grid(row=0, column=1, sticky="w", padx=(10, 0))
self.stand = tk.Label(rahmen, text="", wraplength=breite, justify="left", self.stand = tk.Label(rahmen, text="", wraplength=breite, justify="left",
font=("Segoe UI", 9)) bg=self._bg,
font=(theme.SCHRIFT_NAME, 9))
self.stand.grid(row=4, column=0, sticky="w", pady=(10, 0)) self.stand.grid(row=4, column=0, sticky="w", pady=(10, 0))
knoepfe = ttk.Frame(rahmen) knoepfe = ttk.Frame(rahmen)
@ -127,12 +142,12 @@ class OrdnerWahl(tk.Toplevel):
""" """
ordner = self.pfad.get().strip() ordner = self.pfad.get().strip()
if not ordner: if not ordner:
self.stand.configure(text="Noch kein Ordner gewählt.", fg="#a00000") self.stand.configure(text="Noch kein Ordner gewählt.", fg=self._warn)
self.ok.state(["disabled"]) self.ok.state(["disabled"])
return return
taugt, grund = gemeinsam.ordner_taugt(ordner) taugt, grund = gemeinsam.ordner_taugt(ordner)
if not taugt: if not taugt:
self.stand.configure(text=grund, fg="#a00000") self.stand.configure(text=grund, fg=self._warn)
self.ok.state(["disabled"]) self.ok.state(["disabled"])
return return
self.ok.state(["!disabled"]) self.ok.state(["!disabled"])
@ -144,11 +159,11 @@ class OrdnerWahl(tk.Toplevel):
self.stand.configure( self.stand.configure(
text=f"Hier liegt schon ein Nummernbuch mit {anzahl} Rechnungsnummern – " text=f"Hier liegt schon ein Nummernbuch mit {anzahl} Rechnungsnummern – "
"das ist der Ordner des anderen Programms. Genau richtig.", "das ist der Ordner des anderen Programms. Genau richtig.",
fg="#1f5c3d") fg=self._gut)
else: else:
self.stand.configure( self.stand.configure(
text="Hier ist noch kein Nummernbuch – es wird neu angelegt. Wähle im " text="Hier ist noch kein Nummernbuch – es wird neu angelegt. Wähle im "
"anderen Programm denselben Ordner.", fg="#444444") "anderen Programm denselben Ordner.", fg=self._muted)
def uebernehmen(self): def uebernehmen(self):
ordner = self.pfad.get().strip() ordner = self.pfad.get().strip()

View file

@ -63,6 +63,10 @@ shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json"
config.CONFIG_PATH = os.path.join(tmp, "config.json") config.CONFIG_PATH = os.path.join(tmp, "config.json")
roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) roh = json.load(open(config.CONFIG_PATH, encoding="utf-8"))
roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0 roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0
# Der Erststart-Zustand gehoert dem Pruefstand, nicht der config des
# Entwicklerrechners: sonst laeuft er in einen anderen Zweig, sobald hier
# jemand die Nummer einmal eingerichtet hat.
roh["nummer_eingerichtet"] = False
roh["output_dir"] = ordner roh["output_dir"] = ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2) json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2)

View file

@ -39,6 +39,10 @@ config.BASE_DIR = tmp
config.CONFIG_PATH = os.path.join(tmp, "config.json") config.CONFIG_PATH = os.path.join(tmp, "config.json")
roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) roh = json.load(open(config.CONFIG_PATH, encoding="utf-8"))
roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0 roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0
# Der Erststart-Zustand gehoert dem Pruefstand, nicht der config des
# Entwicklerrechners: sonst laeuft er in einen anderen Zweig, sobald hier
# jemand die Nummer einmal eingerichtet hat.
roh["nummer_eingerichtet"] = False
roh["output_dir"] = ordner roh["output_dir"] = ordner
roh["gemeinsam_ordner"] = gemeinsam_ordner roh["gemeinsam_ordner"] = gemeinsam_ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2) json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2)

View file

@ -27,6 +27,10 @@ shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json"
config.CONFIG_PATH = os.path.join(tmp, "config.json") config.CONFIG_PATH = os.path.join(tmp, "config.json")
roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) roh = json.load(open(config.CONFIG_PATH, encoding="utf-8"))
roh["output_dir"] = ordner roh["output_dir"] = ordner
# Der Erststart-Zustand gehoert dem Pruefstand, nicht der config des
# Entwicklerrechners: dlg_nummer zeigt sonst nur eine Meldung statt des
# Fensters, das hier geprueft werden soll.
roh["nummer_eingerichtet"] = False
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False) json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False)
# Kein Erststart-Dialog im Pruefstand - er wuerde den Lauf haengen lassen. # Kein Erststart-Dialog im Pruefstand - er wuerde den Lauf haengen lassen.

View file

@ -34,6 +34,10 @@ shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json"
config.CONFIG_PATH = os.path.join(tmp, "config.json") config.CONFIG_PATH = os.path.join(tmp, "config.json")
roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) roh = json.load(open(config.CONFIG_PATH, encoding="utf-8"))
roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0 roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0
# Der Erststart-Zustand gehoert dem Pruefstand, nicht der config des
# Entwicklerrechners: sonst laeuft er in einen anderen Zweig, sobald hier
# jemand die Nummer einmal eingerichtet hat.
roh["nummer_eingerichtet"] = False
roh["output_dir"] = ordner roh["output_dir"] = ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False) json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False)

View file

@ -33,6 +33,10 @@ shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json"
config.CONFIG_PATH = os.path.join(tmp, "config.json") config.CONFIG_PATH = os.path.join(tmp, "config.json")
roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) roh = json.load(open(config.CONFIG_PATH, encoding="utf-8"))
roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0 roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0
# Der Erststart-Zustand gehoert dem Pruefstand, nicht der config des
# Entwicklerrechners: sonst laeuft er in einen anderen Zweig, sobald hier
# jemand die Nummer einmal eingerichtet hat.
roh["nummer_eingerichtet"] = False
roh["output_dir"] = ordner roh["output_dir"] = ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False) json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False)

View file

@ -37,6 +37,10 @@ shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json"
config.CONFIG_PATH = os.path.join(tmp, "config.json") config.CONFIG_PATH = os.path.join(tmp, "config.json")
roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) roh = json.load(open(config.CONFIG_PATH, encoding="utf-8"))
roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0 roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0
# Der Erststart-Zustand gehoert dem Pruefstand, nicht der config des
# Entwicklerrechners: sonst laeuft er in einen anderen Zweig, sobald hier
# jemand die Nummer einmal eingerichtet hat.
roh["nummer_eingerichtet"] = False
roh["output_dir"] = ordner roh["output_dir"] = ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2) json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2)

View file

@ -100,6 +100,10 @@ shutil.copy(os.path.join(TOOL, "config.json"), os.path.join(tmp, "config.json"))
config.CONFIG_PATH = os.path.join(tmp, "config.json") config.CONFIG_PATH = os.path.join(tmp, "config.json")
roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) roh = json.load(open(config.CONFIG_PATH, encoding="utf-8"))
roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0 roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0
# Der Erststart-Zustand gehoert dem Pruefstand, nicht der config des
# Entwicklerrechners: sonst laeuft er in einen anderen Zweig, sobald hier
# jemand die Nummer einmal eingerichtet hat.
roh["nummer_eingerichtet"] = False
roh["output_dir"] = rechnungen roh["output_dir"] = rechnungen
roh["gemeinsam_ordner"] = gemeinsam_ordner roh["gemeinsam_ordner"] = gemeinsam_ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2) json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2)

View file

@ -55,6 +55,10 @@ shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json"
config.CONFIG_PATH = os.path.join(tmp, "config.json") config.CONFIG_PATH = os.path.join(tmp, "config.json")
roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) roh = json.load(open(config.CONFIG_PATH, encoding="utf-8"))
roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0 roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0
# Der Erststart-Zustand gehoert dem Pruefstand, nicht der config des
# Entwicklerrechners: steht dort "eingerichtet", zeigt dlg_nummer nur eine
# Meldung statt des Fensters, das hier bedient werden soll.
roh["nummer_eingerichtet"] = False
roh["output_dir"] = ordner roh["output_dir"] = ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2) json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2)

View file

@ -29,6 +29,10 @@ shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json"
config.CONFIG_PATH = os.path.join(tmp, "config.json") config.CONFIG_PATH = os.path.join(tmp, "config.json")
roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) roh = json.load(open(config.CONFIG_PATH, encoding="utf-8"))
roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0 roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0
# Der Erststart-Zustand gehoert dem Pruefstand, nicht der config des
# Entwicklerrechners: sonst laeuft er in einen anderen Zweig, sobald hier
# jemand die Nummer einmal eingerichtet hat.
roh["nummer_eingerichtet"] = False
roh["output_dir"] = ordner roh["output_dir"] = ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2) json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2)

View file

@ -30,6 +30,10 @@ config.BASE_DIR = tmp
config.CONFIG_PATH = os.path.join(tmp, "config.json") config.CONFIG_PATH = os.path.join(tmp, "config.json")
roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) roh = json.load(open(config.CONFIG_PATH, encoding="utf-8"))
roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0 roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0
# Der Erststart-Zustand gehoert dem Pruefstand, nicht der config des
# Entwicklerrechners: sonst laeuft er in einen anderen Zweig, sobald hier
# jemand die Nummer einmal eingerichtet hat.
roh["nummer_eingerichtet"] = False
roh["output_dir"] = ordner roh["output_dir"] = ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False) json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False)

216
theme.py
View file

@ -7,11 +7,85 @@ baut das Programm Menueleiste und Dropdowns selbst; hier stehen nur die Farben.
import sys import sys
import tkinter as tk import tkinter as tk
from tkinter import font as tkfont
from tkinter import ttk from tkinter import ttk
import config import config
# Schrift: Roboto, so wie in den Web-Oberflaechen. Sie wird NICHT auf dem
# Rechner installiert, sondern nur fuer diesen einen Prozess geladen (die
# Windows-Funktion AddFontResourceEx mit FR_PRIVATE). Damit braucht niemand
# Adminrechte, und nach dem Schliessen ist der Rechner wieder wie vorher.
# Findet sich die Datei nicht, bleibt es bei Segoe UI - das Programm muss auch
# ohne mitgelieferte Schrift laufen.
SCHRIFT_NAME = "Segoe UI"
SCHRIFT = (SCHRIFT_NAME, 10)
KLEIN = (SCHRIFT_NAME, 9)
_schrift_geladen = False
def schrift_vorbereiten():
"""Roboto laden und zur Schrift des Programms machen. Gibt den Namen zurueck."""
global SCHRIFT_NAME, SCHRIFT, KLEIN, _schrift_geladen
if _schrift_geladen:
return SCHRIFT_NAME
_schrift_geladen = True
if not sys.platform.startswith("win"):
return SCHRIFT_NAME
import ctypes
import os
FR_PRIVATE = 0x10
geladen = 0
for basis in (config.BASE_DIR, config.RESOURCE_DIR, os.path.dirname(os.path.abspath(__file__))):
ordner = os.path.join(basis, "assets", "fonts")
if not os.path.isdir(ordner):
continue
for datei in sorted(os.listdir(ordner)):
if datei.lower().endswith((".ttf", ".otf")):
geladen += ctypes.windll.gdi32.AddFontResourceExW(
os.path.join(ordner, datei), FR_PRIVATE, 0)
if geladen:
break
if not geladen:
print("[schrift] Roboto nicht gefunden - bleibe bei Segoe UI")
return SCHRIFT_NAME
# Ob Windows sie wirklich angenommen hat, verraet erst die Schriftenliste.
if "Roboto" not in tkfont.families():
print("[schrift] Roboto liess sich nicht laden - bleibe bei Segoe UI")
return SCHRIFT_NAME
SCHRIFT_NAME = "Roboto"
SCHRIFT = (SCHRIFT_NAME, 10)
KLEIN = (SCHRIFT_NAME, 9)
for name in ("TkDefaultFont", "TkTextFont", "TkMenuFont", "TkHeadingFont",
"TkIconFont", "TkTooltipFont"):
try:
tkfont.nametofont(name).configure(family=SCHRIFT_NAME, size=10)
except tk.TclError:
pass
print("[schrift] Roboto geladen")
return SCHRIFT_NAME
def mischen(vorn, hinten, anteil):
"""Wie 'rgba(vorn, anteil)' auf 'hinten' - nur ausgerechnet statt gemalt.
Tk kann keine halbdurchsichtigen Flaechen (kein backdrop-filter, keine
Alpha-Kanaele auf Widgets). Seine Web-Oberflaechen bauen den Glas-Effekt aus
genau zwei Zutaten: eine getoente Flaeche und ein feiner Rand in
Akzent-Transparenz. Beides laesst sich vorher ausrechnen - das Ergebnis sieht
identisch aus, solange der Grund einfarbig ist. Und der ist es hier.
"""
v = vorn.lstrip("#")
h = hinten.lstrip("#")
teile = []
for i in (0, 2, 4):
a = int(v[i:i + 2], 16)
b = int(h[i:i + 2], 16)
teile.append(round(a * anteil + b * (1 - anteil)))
return "#%02x%02x%02x" % tuple(teile)
def windows_dark_mode(): def windows_dark_mode():
"""True, wenn Windows auf das dunkle App-Theme eingestellt ist.""" """True, wenn Windows auf das dunkle App-Theme eingestellt ist."""
if not sys.platform.startswith("win"): if not sys.platform.startswith("win"):
@ -32,26 +106,42 @@ class ThemeMixin:
def _apply_theme(self): def _apply_theme(self):
"""Farben passend zum Windows-Theme (hell/dunkel) setzen.""" """Farben passend zum Windows-Theme (hell/dunkel) setzen."""
schrift_vorbereiten() # Roboto, sonst Segoe UI
dark = windows_dark_mode() dark = windows_dark_mode()
self._dark_mode = dark self._dark_mode = dark
# Die Farben sind die aus seinen anderen Sachen (FiveM-Oberflaechen,
# Adminpage, Webseiten): Tuerkis #06C6A4 auf sehr dunklen, gestuften
# Flaechen. Das PROGRAMM traegt diese Handschrift - die RECHNUNG traegt
# die Farbe der jeweiligen Firma (Einstellungen -> Farbe der Rechnung).
# Beides zu vermischen waere falsch: das Blatt gehoert dem Kunden.
if dark: if dark:
self.col_bg = "#23272e" # Fenster-Hintergrund self.col_bg = "#0d0e10" # Fenster-Hintergrund (tiefstes Grau)
self.col_fg = "#e6e6e6" # Schrift self.col_card = "#15171b" # Kachel / Abschnitt
self.col_field = "#2f343c" # Eingabefelder self.col_fg = "#e8e8ea" # Schrift
self.col_sel = "#3a4a44" # aktiv/hover self.col_field = "#1d2025" # Eingabefelder
self.col_accent = "#5fb98f" # Akzent (Gruen, hell) self.col_sel = "#1f3d38" # aktiv/hover (Akzent, stark abgedunkelt)
self.col_line = "#3a3f47" self.col_accent = "#06C6A4" # Akzent (Pcore-Tuerkis)
self.col_scroll = "#4a505a" # Scroll-Griff self.col_accent_hell = "#08dbb5"
self.col_muted = "#8a929c" # gedaempfte Schrift (z.B. "powered by") self.col_auf_accent = "#08221d" # Schrift AUF der Akzentflaeche
self.col_gefahr = "#e85a5a"
self.col_line = "#25282e"
self.col_scroll = "#2f333a"
self.col_muted = "#8a8e96"
self.col_zebra = "#1b1e24" # jede zweite Zeile der Liste
else: else:
self.col_bg = "#f3f3f3" self.col_bg = "#eef0f3"
self.col_fg = "#1a1a1a" self.col_card = "#ffffff"
self.col_fg = "#14161a"
self.col_field = "#ffffff" self.col_field = "#ffffff"
self.col_sel = "#cfe3d8" self.col_sel = "#d7f5ee"
self.col_accent = "#1f5c3d" self.col_accent = "#04886f" # dunkler, damit es auf Weiss lesbar ist
self.col_line = "#cccccc" self.col_accent_hell = "#06C6A4"
self.col_scroll = "#b8b8b8" self.col_auf_accent = "#ffffff"
self.col_muted = "#777777" # gedaempfte Schrift (z.B. "powered by") self.col_gefahr = "#c0392b"
self.col_line = "#d8dce1"
self.col_scroll = "#c2c6cc"
self.col_muted = "#5c626b"
self.col_zebra = "#e9ecf0"
self.configure(bg=self.col_bg) self.configure(bg=self.col_bg)
style = ttk.Style(self) style = ttk.Style(self)
@ -64,17 +154,89 @@ class ThemeMixin:
style.configure(".", background=bg, foreground=fg, fieldbackground=field, style.configure(".", background=bg, foreground=fg, fieldbackground=field,
bordercolor=self.col_line, lightcolor=bg, darkcolor=bg) bordercolor=self.col_line, lightcolor=bg, darkcolor=bg)
style.configure("TFrame", background=bg) style.configure("TFrame", background=bg)
style.configure("TLabel", background=bg, foreground=fg) style.configure("TLabel", background=bg, foreground=fg, font=SCHRIFT)
style.configure("TLabelframe", background=bg, bordercolor=self.col_line) style.configure("TLabelframe", background=bg, bordercolor=self.col_line)
style.configure("TLabelframe.Label", background=bg, foreground=accent) style.configure("TLabelframe.Label", background=bg, foreground=accent)
style.configure("TButton", background=field, foreground=fg, bordercolor=self.col_line)
style.map("TButton", background=[("active", sel)], foreground=[("active", fg)]) # --- Kacheln: ein Abschnitt ist eine Flaeche mit Rand, keine Linie ---
style.configure("Karte.TFrame", background=self.col_card,
bordercolor=self.col_line, relief="solid", borderwidth=1)
# Ein Abschnitt ist eine Kachel: eigene Flaeche, feiner Rand, die
# Ueberschrift in Akzentfarbe - genau wie die Kacheln in den
# Web-Oberflaechen.
style.configure("Karte.TLabelframe", background=self.col_card,
bordercolor=self.col_line, relief="solid", borderwidth=1,
lightcolor=self.col_card, darkcolor=self.col_card)
style.configure("Karte.TLabelframe.Label", background=self.col_card,
foreground=accent, font=(SCHRIFT_NAME, 9, "bold"))
style.configure("Karte.TEntry", fieldbackground=field, foreground=fg)
style.configure("Karte.TLabel", background=self.col_card, foreground=fg, font=SCHRIFT)
style.configure("KarteMuted.TLabel", background=self.col_card,
foreground=self.col_muted, font=KLEIN)
# Ueberschrift eines Abschnitts: klein, gross geschrieben, in Akzentfarbe -
# so wie die Kachelkoepfe in den Web-Oberflaechen.
style.configure("Abschnitt.TLabel", background=self.col_card,
foreground=accent, font=(SCHRIFT_NAME, 9, "bold"))
style.configure("Wert.TLabel", background=self.col_card, foreground=fg,
font=(SCHRIFT_NAME, 10))
style.configure("Summe.TLabel", background=bg, foreground=accent,
font=(SCHRIFT_NAME, 15, "bold"))
style.configure("Zebra.TLabel", background=self.col_zebra, foreground=fg, font=SCHRIFT)
# --- Knoepfe im Glas-Stil (wie in seinen Oberflaechen) ---
# Getoente Flaeche, feiner Rand in Akzent-Transparenz, Schrift in der
# Akzentfarbe. Kein Vollton: der wirkt in dieser Palette wie ein
# Fremdkoerper.
glas = mischen(accent, bg, 0.10) # ruhige Flaeche
glas_hover = mischen(accent, bg, 0.20)
rand = mischen(accent, bg, 0.35)
rand_hell = mischen(accent, bg, 0.55)
style.configure("TButton", background=glas, foreground=fg,
bordercolor=rand, padding=(14, 7), font=SCHRIFT,
focuscolor=rand, relief="solid", borderwidth=1,
lightcolor=rand, darkcolor=rand)
style.map("TButton",
background=[("pressed", mischen(accent, bg, 0.28)), ("active", glas_hover)],
bordercolor=[("active", rand_hell)],
lightcolor=[("active", rand_hell)], darkcolor=[("active", rand_hell)],
foreground=[("disabled", self.col_muted)])
# Der Knopf, um den es geht: dieselbe Glasflaeche, aber kraeftiger
# getoent, mit Akzentrand und Akzentschrift.
haupt = mischen(accent, bg, 0.22)
style.configure("Haupt.TButton", background=haupt,
foreground=self.col_accent_hell, bordercolor=self.col_accent,
lightcolor=self.col_accent, darkcolor=self.col_accent,
relief="solid", borderwidth=1,
padding=(18, 8), font=(SCHRIFT_NAME, 10, "bold"))
style.map("Haupt.TButton",
background=[("pressed", mischen(accent, bg, 0.45)),
("active", mischen(accent, bg, 0.34)),
("disabled", bg)],
foreground=[("disabled", self.col_muted)])
# Storno greift in eine ausgestellte Rechnung ein - gleicher Stil, rot.
g_rot = mischen(self.col_gefahr, bg, 0.10)
style.configure("Gefahr.TButton", background=g_rot, foreground=self.col_gefahr,
bordercolor=mischen(self.col_gefahr, bg, 0.35),
lightcolor=mischen(self.col_gefahr, bg, 0.35),
darkcolor=mischen(self.col_gefahr, bg, 0.35),
relief="solid", borderwidth=1,
padding=(14, 7), font=SCHRIFT)
style.map("Gefahr.TButton",
background=[("pressed", mischen(self.col_gefahr, bg, 0.30)),
("active", mischen(self.col_gefahr, bg, 0.20))],
bordercolor=[("active", mischen(self.col_gefahr, bg, 0.55))],
lightcolor=[("active", mischen(self.col_gefahr, bg, 0.55))],
darkcolor=[("active", mischen(self.col_gefahr, bg, 0.55))])
style.configure("Menu.TMenubutton", background=bg, foreground=fg, relief="flat", style.configure("Menu.TMenubutton", background=bg, foreground=fg, relief="flat",
arrowcolor=bg, padding=(10, 3)) arrowcolor=bg, padding=(10, 3))
style.map("Menu.TMenubutton", background=[("active", sel)], foreground=[("active", fg)]) style.map("Menu.TMenubutton", background=[("active", sel)], foreground=[("active", fg)])
style.configure("TCheckbutton", background=bg, foreground=fg) style.configure("TCheckbutton", background=bg, foreground=fg)
style.map("TCheckbutton", background=[("active", bg)], foreground=[("active", fg)]) style.map("TCheckbutton", background=[("active", bg)], foreground=[("active", fg)])
style.configure("TEntry", fieldbackground=field, foreground=fg, insertcolor=fg) style.configure("TEntry", fieldbackground=field, foreground=fg, insertcolor=fg,
bordercolor=self.col_line, padding=4)
style.map("TEntry", bordercolor=[("focus", accent)])
# Bei state="readonly" zeichnet die Combobox ihren Text als MARKIERUNG, # Bei state="readonly" zeichnet die Combobox ihren Text als MARKIERUNG,
# sobald sie den Fokus hat - ohne diese beiden Farben steht der Wert # sobald sie den Fokus hat - ohne diese beiden Farben steht der Wert
# unsichtbar im Feld. # unsichtbar im Feld.
@ -101,15 +263,17 @@ class ThemeMixin:
# ist. Der offene Reiter bekommt darum den Hintergrund des Blattes und die # ist. Der offene Reiter bekommt darum den Hintergrund des Blattes und die
# Akzentfarbe, die ruhenden bleiben zurueckgenommen. # Akzentfarbe, die ruhenden bleiben zurueckgenommen.
style.configure("TNotebook", background=bg, bordercolor=self.col_line, style.configure("TNotebook", background=bg, bordercolor=self.col_line,
tabmargins=(2, 4, 2, 0)) tabmargins=(2, 4, 2, 0), borderwidth=0)
style.configure("TNotebook.Tab", background=field, foreground=self.col_muted, style.configure("TNotebook.Tab", background=self.col_field,
bordercolor=self.col_line, padding=(16, 7), foreground=self.col_muted, bordercolor=self.col_line,
font=("Segoe UI", 10)) padding=(20, 9), font=(SCHRIFT_NAME, 10), borderwidth=0)
style.map("TNotebook.Tab", style.map("TNotebook.Tab",
background=[("selected", bg), ("active", sel)], background=[("selected", self.col_card), ("active", sel)],
foreground=[("selected", accent), ("active", fg)], foreground=[("selected", accent), ("active", fg)],
font=[("selected", ("Segoe UI", 10, "bold"))], font=[("selected", (SCHRIFT_NAME, 10, "bold"))])
expand=[("selected", (1, 1, 1, 0))]) # Kein expand beim ausgewaehlten Reiter: er wuchs dadurch um ein paar
# Pixel, die anderen rueckten nach - beim Umschalten sprang die ganze
# Leiste. Erkannt wird der offene Reiter an Farbe und Fettschrift.
# Klassische tk-Widgets (Canvas, Toplevel, Text) erben diese Defaults # Klassische tk-Widgets (Canvas, Toplevel, Text) erben diese Defaults
self.option_add("*background", bg) self.option_add("*background", bg)
self.option_add("*foreground", fg) self.option_add("*foreground", fg)