rechnungstool/helfer.py
TheMockTv 2277a12e0f Zwei getrennte Knoepfe und app.py in Module aufgeteilt
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
2026-09-02 19:41:00 +02:00

87 lines
3.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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 "")