Doppelte Rechnungen anzeigen (Datei > Doppelte Rechnungen)
Der Altbestand wurde nie geprueft - die Sperre verhindert nur NEUE Dubletten.
Neues Modul duplikate.py findet zwei Sorten, und sie bedeuten nicht dasselbe:
Nummer doppelt - zwei Geldbelege mit derselben Rechnungsnummer. Der harte
Fall (§ 14 Abs. 4 Nr. 4 UStG).
Inhalt doppelt - gleicher Gast, gleiches Datum, gleicher Betrag unter zwei
Nummern. Nur ein Hinweis, kann echt sein.
Der schwierige Teil ist das NICHT-Finden: Storno und berichtigte Rechnung
haben absichtlich eigene Nummern, und ein Berichtigungsblatt traegt sogar die
Nummer der Rechnung, die es berichtigt. Nichts davon ist eine Dublette.
PDFs ohne Kenndaten fallen nicht durch: ihre Nummer wird aus dem Dateinamen
gelesen und sie werden getrennt aufgelistet - das ist genau der ungepruefte
Altbestand.
Beim Start laeuft die Pruefung still mit; liegt etwas vor, steht es rot in der
Statuszeile. Kein Popup beim Hochfahren.
pruef_duplikate.py (12 Pruefungen): Ordner mit dem echten Schlamassel gebaut -
zweimal 2026-013 fuer zwei verschiedene Gaeste, dazu eine saubere Storno-Kette
und ein Berichtigungsblatt, die NICHT gemeldet werden duerfen, ein
inhaltsgleiches Paar und eine fremde PDF. Fenster angesehen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKvGkdNW1vKdnMPAM9Bp3X
This commit is contained in:
parent
1060a6421f
commit
67a09e4085
4 changed files with 323 additions and 2 deletions
120
pruef_duplikate.py
Normal file
120
pruef_duplikate.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Pruefstand: findet die Duplikatpruefung das Richtige - und NUR das?
|
||||
|
||||
Der schwierige Teil ist nicht das Finden, sondern das Nicht-Finden: eine Kette
|
||||
aus Rechnung, Storno und berichtigter Rechnung sieht auf den ersten Blick nach
|
||||
lauter Dubletten aus, ist aber genau richtig so. Ein Berichtigungsblatt traegt
|
||||
sogar absichtlich die Nummer der Rechnung, die es berichtigt.
|
||||
|
||||
Der Ordner wird darum von Hand gebaut - mit dem echten Renderer, aber ohne die
|
||||
Sperren des Programms. Genau so sieht der Altbestand aus, den niemand geprueft
|
||||
hat.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
QUELLE = r"C:\claude\rechnungstool"
|
||||
sys.path.insert(0, QUELLE)
|
||||
|
||||
tmp = tempfile.mkdtemp(prefix="dubl_pruef_")
|
||||
ordner = os.path.join(tmp, "rechnungen")
|
||||
os.makedirs(ordner)
|
||||
|
||||
import config # noqa: E402
|
||||
shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json"))
|
||||
config.CONFIG_PATH = os.path.join(tmp, "config.json")
|
||||
|
||||
import berechnung # noqa: E402
|
||||
import pdf_renderer # noqa: E402
|
||||
from duplikate import duplikate_finden # noqa: E402
|
||||
|
||||
cfg = config.load_config()
|
||||
fehler = []
|
||||
|
||||
|
||||
def pruefe(bedingung, text):
|
||||
print((" OK | " if bedingung else "FEHLER | ") + text)
|
||||
if not bedingung:
|
||||
fehler.append(text)
|
||||
|
||||
|
||||
def schreibe(datei, nummer, name, datum, tage=2, **extra):
|
||||
"""Schreibt eine PDF direkt - ohne die Sperren des Programms."""
|
||||
r = berechnung.berechne(cfg["katalog"], {1: {"anzahl": tage, "personen": 0}}, cfg)
|
||||
kopf = {"rechnungsnummer": nummer, "datum": datum, "anreise": "", "abreise": "",
|
||||
"naechte": "", "nachname": name, "vorname": "Anna",
|
||||
"kunde_zeilen": ["", f"Anna {name}", "", "Musterweg 1", "12345 Musterstadt"]}
|
||||
kopf.update(extra)
|
||||
pdf_renderer.render_rechnung(os.path.join(ordner, datei), cfg, r, kopf)
|
||||
|
||||
|
||||
# --- der Schlamassel: zweimal dieselbe Nummer, verschiedene Gaeste -----------
|
||||
schreibe("Rechnung_2026-013_Meier.pdf", "2026-013", "Meier", "12.08.2026", 2)
|
||||
schreibe("Rechnung_2026-013_Schmidt.pdf", "2026-013", "Schmidt", "14.08.2026", 3)
|
||||
|
||||
# --- eine saubere Kette: darf NICHT als Dublette gelten ---------------------
|
||||
schreibe("Rechnung_2026-020_Lang.pdf", "2026-020", "Lang", "20.08.2026", 4)
|
||||
schreibe("Storno_2026-021_Lang.pdf", "2026-021", "Lang", "21.08.2026", 4,
|
||||
art="storno", storno_zu="2026-020", storno_datum="20.08.2026", vorgang="2026-020")
|
||||
schreibe("Rechnung_2026-022_Lang.pdf", "2026-022", "Lang", "21.08.2026", 3,
|
||||
art="korrektur", ersetzt="2026-020", storno_nummer="2026-021", vorgang="2026-020")
|
||||
# Berichtigungsblatt traegt absichtlich die Nummer der Rechnung 2026-022
|
||||
schreibe("Berichtigte Rechnung_2026-08-22_2026-022.pdf", "2026-022", "Lang", "21.08.2026", 3,
|
||||
art="berichtigung", berichtigt_zu="2026-022", berichtigt_am="22.08.2026",
|
||||
vorgang="2026-020")
|
||||
|
||||
# --- inhaltsgleich unter zwei Nummern: nur ein Hinweis ----------------------
|
||||
schreibe("Rechnung_2026-030_Weber.pdf", "2026-030", "Weber", "25.08.2026", 2)
|
||||
schreibe("Rechnung_2026-031_Weber.pdf", "2026-031", "Weber", "25.08.2026", 2)
|
||||
|
||||
# --- eine fremde PDF ohne Kenndaten ----------------------------------------
|
||||
with open(os.path.join(ordner, "Irgendwas.pdf"), "wb") as f:
|
||||
f.write(b"%PDF-1.4 keine Kenndaten")
|
||||
|
||||
nummern, inhalte, unlesbar = duplikate_finden(ordner)
|
||||
|
||||
print("--- gefunden ---")
|
||||
for nr, gruppe in nummern:
|
||||
print(f" Nummer {nr}: {[b['_datei'] for b in gruppe]}")
|
||||
for (name, datum, betrag), gruppe in inhalte:
|
||||
print(f" Inhalt {name} {datum} {betrag}: {[b.get('rechnungsnummer') for b in gruppe]}")
|
||||
print(f" ohne Kenndaten: {unlesbar}")
|
||||
|
||||
pruefe(len(nummern) == 1, f"genau EINE doppelte Nummer gemeldet ({len(nummern)})")
|
||||
pruefe(nummern and nummern[0][0] == "2026-013", "und zwar 2026-013")
|
||||
pruefe(nummern and {b["nachname"] for b in nummern[0][1]} == {"Meier", "Schmidt"},
|
||||
"mit beiden Gästen")
|
||||
|
||||
gemeldete = {nr for nr, _ in nummern}
|
||||
pruefe("2026-021" not in gemeldete and "2026-020" not in gemeldete,
|
||||
"die Storno-Kette gilt NICHT als Dublette")
|
||||
pruefe("2026-022" not in gemeldete,
|
||||
"das Berichtigungsblatt zählt die Nummer der Rechnung NICHT doppelt")
|
||||
|
||||
pruefe(len(inhalte) == 1, f"genau ein inhaltsgleicher Fall ({len(inhalte)})")
|
||||
pruefe(inhalte and {b["rechnungsnummer"] for b in inhalte[0][1]} == {"2026-030", "2026-031"},
|
||||
"Weber taucht unter zwei Nummern auf")
|
||||
pruefe(inhalte and inhalte[0][0][0] == "weber", f"und wird beim Namen genannt ({inhalte[0][0]})")
|
||||
|
||||
pruefe(unlesbar == ["Irgendwas.pdf"], f"die fremde PDF wird gemeldet ({unlesbar})")
|
||||
|
||||
# --- sauberer Ordner meldet nichts ------------------------------------------
|
||||
leer = os.path.join(tmp, "leer")
|
||||
os.makedirs(leer)
|
||||
schreibe_ordner = ordner
|
||||
ordner = leer
|
||||
schreibe("Rechnung_2026-001_Neu.pdf", "2026-001", "Neu", "01.09.2026", 1)
|
||||
schreibe("Rechnung_2026-002_Alt.pdf", "2026-002", "Alt", "02.09.2026", 2)
|
||||
n2, i2, u2 = duplikate_finden(leer)
|
||||
pruefe(not n2 and not i2 and not u2,
|
||||
f"ein sauberer Ordner meldet nichts ({n2}, {i2}, {u2})")
|
||||
pruefe(duplikate_finden(os.path.join(tmp, "gibtsnicht")) == ([], [], []),
|
||||
"fehlender Ordner knallt nicht")
|
||||
|
||||
print("---")
|
||||
print(f"Ordner: {schreibe_ordner}")
|
||||
print("alles gruen" if not fehler else f"{len(fehler)} Fehler: {fehler}")
|
||||
sys.exit(1 if fehler else 0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue