rechnungstool/theme.py
TheMockTv 5ef4e0a769 Montage-Rechnung als eigener Reiter, und das Datum laesst sich nicht mehr verbiegen
Bartl vermietet auch monatsweise an Montagearbeiter. Dieses Blatt schrieb er
bisher von Hand in Word ("Miete fuer Montagearbeiter inkl. 7 % MwSt. fuer den
Monat August 2026", Netto/USt/Brutto nebeneinander). Jetzt ist es ein zweiter
Reiter neben der Campingrechnung: Kunde, Nummer, Datum und Zeitraum gelten fuer
beide - nur die Mitte wechselt, damit niemand den Kunden zweimal tippt.

Die Zeitraum-Felder sind dieselben wie Anreise/Abreise, nur anders beschriftet.
Daraus kommen die Naechte, und die meldet das Steuerjournal ans Amt.

Gerechnet wird im selben Rechenkern: der Reiter baut einen Ein-Zeilen-Katalog
mit dem Monatspreis (brutto, Vorgabe 500,00 unter Einstellungen ->
Montage-Rechnung). Die Beherbergungssteuer kommt obendrauf wie ueberall sonst -
500 + 5 % = 525. Fuer das Steuerjournal ist das eine ganz normale Buchung, art
bleibt "rechnung"; die Kenndaten tragen zusaetzlich vorlage="montage", damit der
Storno-Weg die Rechnung spaeter in den richtigen Reiter laedt (sonst waeren aus
Monaten Wohnwagen-Naechte geworden).

Datum: Tag und Monat lassen sich nicht mehr unmoeglich tippen (eine 4 wird zu
04, 35 und 13 fallen weg), und vor dem Erstellen wird gegen den Kalender
geprueft - den 31.02. faengt erst das. Vorher landete so etwas ungeprueft auf
der Rechnung.

Dazu: die Reiter bekommen Theme-Farben, sonst sieht man nicht, welcher offen ist.

Pruefstand pruef_montage.py: 45 Pruefungen am echten Fenster und an echten
PDFs - Reiterwechsel, Beschriftung, Betraege, das Blatt selbst, danach liest das
Steuerjournal die Datei ein (500,00 Basis, 25,00 Steuer, 30 Uebernachtungen),
Storno, die unveraenderte Campingrechnung und die Datumspruefung.
pruef_kernregeln, pruef_storno, pruef_kette, pruef_nummern, pruef_dialoge gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYeAfLtccFrbU3MTj1MMTx
2026-09-03 20:12:04 +02:00

137 lines
6.9 KiB
Python

# -*- coding: utf-8 -*-
"""Farben nach dem Windows-Theme (hell/dunkel) - inklusive Titelleiste.
Native tk-Menues und -Popups lassen sich unter Windows nicht einfaerben, darum
baut das Programm Menueleiste und Dropdowns selbst; hier stehen nur die Farben.
"""
import sys
import tkinter as tk
from tkinter import ttk
def windows_dark_mode():
"""True, wenn Windows auf das dunkle App-Theme eingestellt ist."""
if not sys.platform.startswith("win"):
return False
try:
import winreg
with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize") as k:
return winreg.QueryValueEx(k, "AppsUseLightTheme")[0] == 0
except OSError:
return False
class ThemeMixin:
"""Farben setzen. Erwartet ein tk.Tk als Basis."""
# ----------------------------------------------------------------- Theme
def _apply_theme(self):
"""Farben passend zum Windows-Theme (hell/dunkel) setzen."""
dark = windows_dark_mode()
self._dark_mode = dark
if dark:
self.col_bg = "#23272e" # Fenster-Hintergrund
self.col_fg = "#e6e6e6" # Schrift
self.col_field = "#2f343c" # Eingabefelder
self.col_sel = "#3a4a44" # aktiv/hover
self.col_accent = "#5fb98f" # Akzent (Gruen, hell)
self.col_line = "#3a3f47"
self.col_scroll = "#4a505a" # Scroll-Griff
self.col_muted = "#8a929c" # gedaempfte Schrift (z.B. "powered by")
else:
self.col_bg = "#f3f3f3"
self.col_fg = "#1a1a1a"
self.col_field = "#ffffff"
self.col_sel = "#cfe3d8"
self.col_accent = "#1f5c3d"
self.col_line = "#cccccc"
self.col_scroll = "#b8b8b8"
self.col_muted = "#777777" # gedaempfte Schrift (z.B. "powered by")
self.configure(bg=self.col_bg)
style = ttk.Style(self)
try:
style.theme_use("clam")
except tk.TclError:
pass
bg, fg, field, sel, accent = (self.col_bg, self.col_fg, self.col_field,
self.col_sel, self.col_accent)
style.configure(".", background=bg, foreground=fg, fieldbackground=field,
bordercolor=self.col_line, lightcolor=bg, darkcolor=bg)
style.configure("TFrame", background=bg)
style.configure("TLabel", background=bg, foreground=fg)
style.configure("TLabelframe", background=bg, bordercolor=self.col_line)
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)])
style.configure("Menu.TMenubutton", background=bg, foreground=fg, relief="flat",
arrowcolor=bg, padding=(10, 3))
style.map("Menu.TMenubutton", background=[("active", sel)], foreground=[("active", fg)])
style.configure("TCheckbutton", background=bg, foreground=fg)
style.map("TCheckbutton", background=[("active", bg)], foreground=[("active", fg)])
style.configure("TEntry", fieldbackground=field, foreground=fg, insertcolor=fg)
# Bei state="readonly" zeichnet die Combobox ihren Text als MARKIERUNG,
# sobald sie den Fokus hat - ohne diese beiden Farben steht der Wert
# unsichtbar im Feld.
style.configure("TCombobox", fieldbackground=field, foreground=fg,
background=field, arrowcolor=fg,
selectbackground=field, selectforeground=fg)
style.map("TCombobox", fieldbackground=[("readonly", field)],
foreground=[("readonly", fg)], background=[("readonly", field)],
selectbackground=[("readonly", field)], selectforeground=[("readonly", fg)])
# Die aufgeklappte Liste ist ein klassisches tk-Listbox-Widget
self.option_add("*TCombobox*Listbox.background", field)
self.option_add("*TCombobox*Listbox.foreground", fg)
self.option_add("*TCombobox*Listbox.selectBackground", sel)
self.option_add("*TCombobox*Listbox.selectForeground", fg)
for sbs in ("Vertical.TScrollbar", "Horizontal.TScrollbar", "TScrollbar"):
style.configure(sbs, troughcolor=bg, background=self.col_scroll,
bordercolor=bg, lightcolor=bg, darkcolor=bg,
arrowcolor=fg, relief="flat", gripcount=0)
style.map(sbs, background=[("active", accent)],
troughcolor=[("disabled", bg)])
style.configure("TSeparator", background=self.col_line)
# Reiter (Campingrechnung / Montage): ohne eigene Farben zeichnet clam den
# AKTIVEN Reiter dunkler als die ruhenden - man sieht dann nicht, wo man
# ist. Der offene Reiter bekommt darum den Hintergrund des Blattes und die
# Akzentfarbe, die ruhenden bleiben zurueckgenommen.
style.configure("TNotebook", background=bg, bordercolor=self.col_line,
tabmargins=(2, 4, 2, 0))
style.configure("TNotebook.Tab", background=field, foreground=self.col_muted,
bordercolor=self.col_line, padding=(16, 7),
font=("Segoe UI", 10))
style.map("TNotebook.Tab",
background=[("selected", bg), ("active", sel)],
foreground=[("selected", accent), ("active", fg)],
font=[("selected", ("Segoe UI", 10, "bold"))],
expand=[("selected", (1, 1, 1, 0))])
# Klassische tk-Widgets (Canvas, Toplevel, Text) erben diese Defaults
self.option_add("*background", bg)
self.option_add("*foreground", fg)
def _style_dialog(self, d):
"""Toplevel-Dialog an das Theme angleichen."""
d.configure(bg=self.col_bg)
# erst nach dem Anzeigen setzen, sonst greift DWM noch nicht
d.after(20, lambda: self._dark_titlebar(d))
def _dark_titlebar(self, win):
"""Windows-Titelleiste des Fensters dunkel machen (nur im Dark-Mode)."""
if not (sys.platform.startswith("win") and getattr(self, "_dark_mode", False)):
return
try:
import ctypes
win.update_idletasks()
hwnd = ctypes.windll.user32.GetParent(win.winfo_id())
val = ctypes.c_int(1)
for attr in (20, 19): # DWMWA_USE_IMMERSIVE_DARK_MODE (Win11=20, frühes Win10=19)
ctypes.windll.dwmapi.DwmSetWindowAttribute(
hwnd, attr, ctypes.byref(val), ctypes.sizeof(val))
# Frame neu zeichnen lassen (ohne Flackern/Fokusverlust)
SWP = 0x0001 | 0x0002 | 0x0004 | 0x0020 # NOSIZE|NOMOVE|NOZORDER|FRAMECHANGED
ctypes.windll.user32.SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP)
except Exception as e: # noqa: BLE001
print("[theme] Titelleiste:", e)