Knoepfe: "Storno" und "Berichtigen" setzen den Weg schon beim Oeffnen fest, das Popup fragt dann nur noch die Nummer. Vorher entschied ein zweiter Knopf im Popup - der ist raus. app.py war 1802 Zeilen, jetzt 672. Neu daneben: helfer.py Zahlen, Datum, Namen, Pruefungen (87) bestand.py was im Rechnungsordner liegt, nur lesen (171) storno.py den spiegelbildlichen Storno rechnen (57) einzelinstanz.py nur ein Fenster zulassen (90) theme.py Farben nach Windows hell/dunkel (120) korrektur.py Storno- und Berichtigungsweg (306) einstellungen.py die Dialoge aus dem Menue (358) Die Oberflaeche bleibt eine Klasse - die Teile sind Mixins, damit kein einziger self-Zugriff umgeschrieben werden musste. Kein Modul importiert app.py zurueck, keine ungenutzten Importe mehr. Die Pruefstaende holen die Funktionen jetzt aus dem Modul, dem sie gehoeren. pruef_storno.py 100 gruen, pruef_nummern.py 7 gruen, Fenster nach der Aufteilung gestartet und angesehen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKvGkdNW1vKdnMPAM9Bp3X
87 lines
3.1 KiB
Python
87 lines
3.1 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Kleine Helfer, die überall gebraucht werden: Zahlen, Datum, Namen, Prüfungen."""
|
||
|
||
import datetime
|
||
|
||
import config
|
||
|
||
|
||
def parse_num(s):
|
||
"""'12', '3,5' -> float; leer/ungueltig -> 0.0"""
|
||
s = (s or "").strip().replace(",", ".")
|
||
if not s:
|
||
return 0.0
|
||
try:
|
||
return float(s)
|
||
except ValueError:
|
||
return 0.0
|
||
def parse_date(s):
|
||
"""'28.01.2026' -> date; sonst None"""
|
||
s = (s or "").strip()
|
||
for fmt in ("%d.%m.%Y", "%d.%m.%y", "%Y-%m-%d"):
|
||
try:
|
||
return datetime.datetime.strptime(s, fmt).date()
|
||
except ValueError:
|
||
continue
|
||
return None
|
||
def safe_filename(s):
|
||
keep = "-_.() "
|
||
return "".join(c for c in s if c.isalnum() or c in keep).strip() or "Rechnung"
|
||
def saeubern(s):
|
||
"""Rand-Leerzeichen weg, Mehrfachleerzeichen zu einem.
|
||
|
||
Wird vor der Pruefung auf die Felder angewendet - geprueft werden muss
|
||
genau das, was nachher auf dem Blatt steht.
|
||
"""
|
||
return " ".join(str(s or "").split())
|
||
def buchstaben(s):
|
||
return sum(1 for c in str(s) if c.isalpha())
|
||
def ziffern(s):
|
||
return sum(1 for c in str(s) if c.isdigit())
|
||
# Auswahl fuer das Anrede-Feld. Die Liste ist nicht bindend - das Feld bleibt
|
||
# tippbar, damit Sonderfaelle nicht am Programm scheitern.
|
||
ANREDEN = ["Herr", "Frau", "Herr und Frau", "Familie", "Firma", "Eheleute"]
|
||
def nummer_normalisieren(s):
|
||
"""'2026-5', ' 2026-005 ' -> '2026-005'; alles andere kommt getrimmt zurueck.
|
||
|
||
Damit findet die Suche die Rechnung auch, wenn die fuehrenden Nullen fehlen.
|
||
"""
|
||
s = str(s or "").strip()
|
||
teile = s.split("-")
|
||
if len(teile) == 2:
|
||
try:
|
||
return config.format_rechnungsnummer(int(teile[0]), int(teile[1]))
|
||
except ValueError:
|
||
pass
|
||
return s
|
||
def kunde_maengel(kunde):
|
||
"""Beanstandungen an Name und Anschrift; leere Liste = in Ordnung.
|
||
|
||
Prueft nicht nur "irgendwas eingetippt": ein Feld aus Leerzeichen,
|
||
Bindestrichen oder einem einzelnen Zeichen ist kein Name und keine
|
||
Anschrift. Vier Ziffern reichen, damit auch oesterreichische und
|
||
niederlaendische Postleitzahlen durchgehen.
|
||
"""
|
||
maengel = []
|
||
for feld, schluessel in (("Vorname", "vorname"),
|
||
("Nachname / Firma", "nachname"),
|
||
("Straße / Nr.", "strasse")):
|
||
wert = saeubern(kunde.get(schluessel))
|
||
if not wert:
|
||
maengel.append(f"{feld}: ist leer")
|
||
elif buchstaben(wert) < 2:
|
||
maengel.append(f"{feld}: „{wert}“ ergibt keinen Namen")
|
||
|
||
ort = saeubern(kunde.get("plzort"))
|
||
if not ort:
|
||
maengel.append("PLZ / Ort: ist leer")
|
||
elif ziffern(ort) < 4 or buchstaben(ort) < 2:
|
||
maengel.append(f"PLZ / Ort: „{ort}“ – bitte Postleitzahl UND Ort, "
|
||
"z. B. 04416 Markkleeberg")
|
||
return maengel
|
||
def name_aufteilen(ganz):
|
||
"""'Andres Schäfer' -> ('Andres', 'Schäfer'); ein einzelnes Wort wird Nachname."""
|
||
teile = str(ganz or "").split()
|
||
if len(teile) > 1:
|
||
return " ".join(teile[:-1]), teile[-1]
|
||
return "", (teile[0] if teile else "")
|