Markenerkennung: Grenzen gemessen, Summenzeile im Kopf ist keine Marke

Auf die Frage, ob Belege einheitlich gestaltet sind: nein. Die Erkennung
haengt an drei Annahmen - Marke in den ersten acht Zeilen, Marke in der
festen Liste, Marke ueberhaupt vorhanden. Alle drei sind jetzt gemessen und
in Tests festgehalten.

Dabei ein echter Defekt gefunden: "Total" ist Kopfmarke UND Summenwort. Ein
Beleg mit Betrag in den ersten acht Zeilen lieferte faelschlich die Marke
"Total" (an einem echten Beleg reproduziert). _betragszeile() filtert
Summenzeilen jetzt aus dem Kopf - verlangt werden beide Kennzeichen zugleich,
Summenwort und Geldangabe, damit "SHELL TANKSTELLE, 6450 SOELDEN" nicht
mitfaellt. Gegenprobe: ein echter "TOTAL TANKSTELLE"-Kopf ergibt weiter Total.

Der Ausfall beschaedigt nichts: mit unbekannter Marke im Kopf bleiben Liter,
Betrag, Rabatt und Anschrift unveraendert richtig, nur die Marke fehlt.

Parser-Suite 17 Tests (von 11), alle zwoelf abgelegten Belege liefern weiter
Shell, audi_ha_test auf 2026.9.4.22 ohne Traceback.
This commit is contained in:
2026-09-04 17:09:12 +02:00
parent bdbdb4a010
commit 8557bfe230
7 changed files with 204 additions and 5 deletions
@@ -165,5 +165,68 @@ class BelegOhneRabatt(unittest.TestCase):
self.assertAlmostEqual(d["liters"], 42.15, delta=0.001)
class MarkenerkennungGrenzen(unittest.TestCase):
"""Was die Markenerkennung leistet - und was ausdrücklich nicht.
Belege sind nicht genormt. Diese Suite hält fest, wo die Erkennung greift,
wo sie aufgibt, und dass ein Aufgeben den Beleg nicht beschädigt.
"""
@classmethod
def setUpClass(cls):
cls.zeilen = p._text_aus_pdf(os.path.join(_BELEGE, "e-receipt.pdf")).splitlines()
def test_marke_steht_ganz_oben(self):
# Gemessen am 04.09.2026: in allen elf Belegen in Zeile 0. Das Fenster
# von acht Zeilen ist der Puffer, nicht die Erwartung - der
# österreichische Beleg (nur in der Instanz) hat sie in Zeile 1.
for datei in sorted(f for f in os.listdir(_BELEGE) if f.endswith(".pdf")):
with self.subTest(datei=datei):
zeilen = p._text_aus_pdf(os.path.join(_BELEGE, datei)).splitlines()
self.assertEqual(p._marke(zeilen[:1]), "Shell")
def test_ausserhalb_des_fensters_wird_nicht_gefunden(self):
# Ehrlich dokumentiert statt stillschweigend angenommen: steht die
# Marke erst ab Zeile 8, findet sie niemand.
verschoben = [""] * 8 + self.zeilen
self.assertIsNone(p._marke(verschoben))
def test_unbekannte_marke_beschaedigt_den_beleg_nicht(self):
# Freie Tankstellen und Marken, die nicht in _MARKEN stehen: der Beleg
# wird vollständig eingelesen, nur eben ohne Marke. Ein falscher Name
# wäre schlimmer als keiner.
for ersatz in ("OIL! Tankstelle", "Hoyer Tankstelle", "Freie Tankstelle Mueller"):
with self.subTest(kopf=ersatz):
d = p._parsen("\n".join([ersatz] + self.zeilen[1:]))
self.assertIsNone(d["station_brand"])
# Ohne Marke tritt der Betreibername an ihre Stelle.
self.assertEqual(d["station_name"], "A. Zrenner GmbH, Pascalstr. 8, Ingolstadt")
self.assertAlmostEqual(d["liters"], 42.15, delta=0.001)
self.assertAlmostEqual(d["fuel_total_eur"], 78.78, delta=0.001)
def test_summenzeile_im_kopf_ist_keine_marke(self):
# "Total" ist beides - Kopfmarke und Summenwort. Ein Beleg mit knappem
# Kopf, der den Betrag schon oben druckt, lieferte vorher fälschlich
# die Marke "Total" (nachgestellt am 04.09.2026).
for summe in ("TOTAL: 78,78 EUR", "GESAMT 78,78 EUR", "Summe 78,78 EUR"):
with self.subTest(zeile=summe):
self.assertEqual(p._marke([summe] + self.zeilen), "Shell")
def test_echte_marke_total_bleibt_erkennbar(self):
# Die Gegenprobe zum Test darüber: verworfen wird nur, wo Summenwort
# UND Geldangabe zusammenkommen - eine Kopfzeile ohne Betrag nicht.
self.assertEqual(p._marke(["TOTAL TANKSTELLE", "Musterweg 1", "80331 München"]), "Total")
# Und eine Postleitzahl in der Kopfzeile faellt nicht mit heraus.
self.assertEqual(
p._marke(["AUTO B. FRISCHMANN GMBH", "SHELL TANKSTELLE, 6450 SÖLDEN"]), "Shell"
)
def test_wortgrenze_schuetzt_vor_teiltreffern(self):
# "Star" darf nicht in "Starnberg" treffen, "Eni" nicht in "Enisstr.".
self.assertIsNone(p._marke(["Tankstelle Mueller", "Seestr. 3", "82319 Starnberg"]))
self.assertIsNone(p._marke(["Freie Tankstelle", "Enisweg 4", "12345 Musterort"]))
if __name__ == "__main__":
unittest.main(verbosity=2)