Tankstellenmarke als eigenes Feld, Belegfelder in der App richtig zugeordnet

Die Marke steht auf jedem Beleg, war aber nirgends gespeichert: der Parser
erkennt sie seit dem 16.08.2026, faltete sie aber nur in station_name.
Fuer jeden Beleg, den eine aeltere Fassung eingelesen hat, blieb dort der
Betreibername stehen und die Marke war danach nicht mehr zu holen.

- shell_beleg_parser: station_brand als eigenes Feld aus beiden Parserwegen,
  dazu marke_aus_text()/marke_aus_pdf() - sie lesen nur den Belegkopf und
  haengen nicht am vollstaendigen Einlesen.
- belege.marken_nachtragen(): traegt die Marke einmal beim Start aus der
  abgelegten Belegdatei nach. Nur wo das Feld gar nicht vorkommt und die
  Datei liegt; ein einmal eingetragenes Feld wird nie ueberschrieben.
- Panel und App: die gespeicherte Marke geht vor, geraten wird nur, wo keine
  da ist. Ein Teil, der fuer sich genommen eine Kopfmarke ist, taugt zudem
  nie als Ort.

Dazu Punkt 5 der Meldung vom 04.09.2026: ein eingelesener Beleg fuellte nur
Datum und Uhrzeit. Der Parser las ihn immer vollstaendig - die App fragte
liter/kosten/station/ersparnis/kraftstoff ab, veroeffentlicht werden aber die
Namen des Belegs (liters/fuel_total_eur/station_name/discount/fuel_type).
Uebereingestimmt hat allein ts. Die Zuordnung steht jetzt als
belegFormularwerte() in daten/belegentwurf.ts und wird gegen die echte
Nutzlast dieses Belegs geprueft.

tsc sauber, 258 Tests (die drei neuen Marken-Tests gegen den alten Stand als
scheiternd nachgewiesen), vite build sauber, Panel als Modul geparst,
Parser-Suite 11 Tests gegen elf echte Belege. Panel und App liefern fuer elf
Faelle byteweise dasselbe. Live in audi_ha_test auf 2026.9.4.21 ohne
Traceback: Uebersicht "Shell, Nuernberger Str., Ansbach", Einzelbeleg
zweizeilig mit Marke, und der echte geteilte Beleg fuellt durchs Backend das
ganze Formular.
This commit is contained in:
2026-09-04 17:00:22 +02:00
parent a99ae432fc
commit bdbdb4a010
17 changed files with 572 additions and 60 deletions
+38 -8
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
"""Regressionstest für shell_beleg_parser.py gegen zehn echte Shell-eReceipts.
"""Regressionstest für shell_beleg_parser.py gegen elf echte Shell-eReceipts.
Die PDFs in belege/ sind reale, vom Nutzer bereitgestellte Tankbelege aus
vier verschiedenen Stationen (Ingolstadt/Zrenner, Rain am Lech/Bauch,
Ansbach/Sengül, Königsbronn/Mogler) und decken damit die tatsächlich
fünf verschiedenen Stationen (Ingolstadt/Zrenner an zwei Adressen, Rain am
Lech/Bauch, Ansbach/Sengül, Königsbronn/Mogler) und decken damit die tatsächlich
beobachtete Formatvielfalt ab, nicht nur eine angenommene Struktur:
- Markenzeile: "SHELL STATION" / "Shell Station" / "Shell-Station"
- Rabattbezeichnung: "V-Power Smart Deal" / "ClubSmartRabatt"
@@ -42,12 +42,13 @@ class AlleBelegeParsenOhneFehler(unittest.TestCase):
cls.dateien = sorted(f for f in os.listdir(_BELEGE) if f.endswith(".pdf"))
cls.ergebnisse = {f: _parsen_datei(f) for f in cls.dateien}
def test_zehn_belege_gefunden(self):
self.assertEqual(len(self.dateien), 10, self.dateien)
def test_elf_belege_gefunden(self):
self.assertEqual(len(self.dateien), 11, self.dateien)
def test_pflichtfelder_gesetzt(self):
pflichtfelder = [
"receipt_key", "receipt_no", "ts", "station_id", "station_name",
"receipt_key", "receipt_no", "ts", "station_id", "station_brand",
"station_name",
"station_address", "article_no", "product_name", "fuel_type",
"liters", "list_price_per_l", "fuel_total_eur",
"receipt_total_eur", "net_eur", "vat_eur",
@@ -72,12 +73,35 @@ class AlleBelegeParsenOhneFehler(unittest.TestCase):
# Rabatt darf den Bruttobetrag nicht übersteigen
self.assertLess(d["discount"], d["fuel_total_eur"])
def test_marke_als_eigenes_feld(self):
# Seit dem 04.09.2026 steht die Kopfmarke ZUSAETZLICH als eigenes Feld
# da, nicht nur in station_name eingefaltet. Genau daran fehlte sie
# jedem Beleg, den ein aelterer Parser eingelesen hatte: dort blieb der
# Betreibername stehen, und nachtraeglich war die Marke nirgends mehr
# zu holen (Befund des Eigentuemers, 04.09.2026).
for datei, d in self.ergebnisse.items():
with self.subTest(datei=datei):
self.assertEqual(d["station_brand"], "Shell")
def test_marke_ohne_vollstaendiges_einlesen(self):
# marke_aus_pdf() liest nur den Belegkopf. Das ist der Weg, ueber den
# belege.marken_nachtragen() die Marke bei Altbelegen nachtraegt - er
# darf nicht vom vollstaendigen Parsen abhaengen.
for datei in self.dateien:
with self.subTest(datei=datei):
self.assertEqual(p.marke_aus_pdf(os.path.join(_BELEGE, datei)), "Shell")
def test_marke_aus_pdf_ohne_datei(self):
# Eine fehlende Belegdatei ist kein Fehlerfall - marken_nachtragen()
# laeuft ueber Vorgaenge, deren PDF laengst geloescht sein kann.
self.assertIsNone(p.marke_aus_pdf(os.path.join(_BELEGE, "gibtesnicht.pdf")))
def test_receipt_key_eindeutig(self):
schluessel = [d["receipt_key"] for d in self.ergebnisse.values()]
self.assertEqual(len(schluessel), len(set(schluessel)), schluessel)
def test_bekannte_stationen(self):
# Vier verschiedene Standorte in den zehn Belegen - stellt sicher, dass
# Fünf verschiedene Standorte in den elf Belegen - stellt sicher, dass
# die Stationserkennung nicht zufällig nur für einen Beleg passt.
# Anzeigeformat seit 2026-08-16: "Marke, Straße, Ort" statt des reinen
# Betreibernamens ("A. Zrenner GmbH" o. ä.) - der Betreiber sagt beim
@@ -88,6 +112,7 @@ class AlleBelegeParsenOhneFehler(unittest.TestCase):
"Shell, Münchener Str.15a, Ingolstadt",
"Shell, Nürnberger Str.74, Ansbach",
"Shell, Heidenheimerstr.6, Königsbronn",
"Shell, Richard-Wagner-Str. 9, Ingolstadt",
}
self.assertEqual(namen, erwartet)
@@ -107,6 +132,11 @@ class AlleBelegeParsenOhneFehler(unittest.TestCase):
"e-receipt3.pdf": dict(liters=21.73, fuel_total_eur=44.53, discount=6.08, station_id="0000000456"),
"e-receipt5.pdf": dict(liters=47.76, fuel_total_eur=79.23, discount=12.90, station_id="0000002227"),
"e-receipt9.pdf": dict(liters=51.59, fuel_total_eur=85.59, discount=13.41, station_id="0000002103"),
# Der Beleg, der in der App nur mit Datum und Uhrzeit ankam
# (Eigentuemer, 04.09.2026). Der Parser hat ihn immer vollstaendig
# gelesen - der Fehler sass in Tanken.tsx, das andere Feldnamen
# abfragte, als belege.py veroeffentlicht.
"geteilt-2026-09-04.pdf": dict(liters=43.87, fuel_total_eur=95.60, discount=11.84, station_id="0000000459"),
}
for datei, erwartet in stichproben.items():
d = self.ergebnisse[datei]
@@ -120,7 +150,7 @@ class AlleBelegeParsenOhneFehler(unittest.TestCase):
class BelegOhneRabatt(unittest.TestCase):
"""SmartDeal/ClubSmart ist optional - nicht jeder Tankvorgang hat einen
Rabatt. Simuliert durch Entfernen der Rabattzeilen aus einem echten
Beleg, da keiner der zehn realen Belege rabattfrei war."""
Beleg, da keiner der elf realen Belege rabattfrei war."""
def test_ohne_rabatt_bleibt_alles_andere_korrekt(self):
text = p._text_aus_pdf(os.path.join(_BELEGE, "e-receipt.pdf"))