bdbdb4a010
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.
431 lines
18 KiB
Python
431 lines
18 KiB
Python
"""Belegverarbeitung für Tankbelege (SPECIFICATION.md §7.7).
|
|
|
|
Beleg-Upload direkt in der App statt Postfachanbindung - kein IMAP-Zugriff.
|
|
Das eigentliche Lesen der PDF macht shell_beleg_parser.py daneben; hier steht,
|
|
was mit dem Ergebnis passiert.
|
|
|
|
ZWEI REGELN, DIE DEN AUFBAU ERKLÄREN
|
|
------------------------------------
|
|
1. Liter, Kosten und Ersparnis kommen vom Beleg, Kilometerstand und gefahrene
|
|
Distanz kommen immer vom Fahrzeug (§7.7 Regel 2). Ein Beleg-Nachtrag rührt
|
|
odometer_km/distance_km deshalb nicht an.
|
|
2. price_per_l wird grundsätzlich aus fuel_total_eur ÷ liters berechnet, nie
|
|
vom Beleg übernommen. fuel_total_eur ist dabei immer der reale, bereits
|
|
rabattierte Betrag; die SmartDeal-Ersparnis steht separat in discount.
|
|
|
|
Mit tank_id trägt der Upload die Belegdaten in einen bestehenden Tankvorgang
|
|
nach - egal ob der ursprünglich manuell oder automatisch angelegt wurde. Passt
|
|
das Belegdatum nicht zum erfassten Tankvorgang (anderer Kalendertag), wird
|
|
nichts übernommen, sondern ein Fehler veröffentlicht: sonst landet in der
|
|
Oberfläche versehentlich ein falscher Beleg an einem bestehenden Eintrag.
|
|
|
|
Ohne tank_id legt der Upload KEINEN eigenständigen Tankvorgang an, sondern
|
|
parst nur und veröffentlicht das Ergebnis - die Oberfläche übernimmt die Werte
|
|
in den gerade offenen Entwurf und speichert erst auf einen expliziten Klick.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import datetime
|
|
import logging
|
|
import os
|
|
from typing import TYPE_CHECKING
|
|
|
|
from . import shell_beleg_parser
|
|
from . import tankerkennung
|
|
from .tankerkennung import leerer_tankvorgang
|
|
from .veroeffentlichung import zustand_oder_none
|
|
from .verlauf import als_kilometerstand, zeit_normalisiert
|
|
|
|
if TYPE_CHECKING:
|
|
from .koordinator import Koordinator
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
def preis_je_liter(fuel_total_eur: float | None, liters: float | None) -> float | None:
|
|
if not liters or fuel_total_eur is None:
|
|
return None
|
|
return round(fuel_total_eur / liters, 3)
|
|
|
|
|
|
def _speichern_und_lesen(ordner: str, dateiname: str, rohdaten: bytes) -> tuple[str, dict]:
|
|
"""Läuft komplett im Executor: Datei schreiben und PDF lesen sind beides
|
|
blockierende Arbeit."""
|
|
os.makedirs(ordner, exist_ok=True)
|
|
pfad = os.path.join(ordner, dateiname)
|
|
with open(pfad, "wb") as datei:
|
|
datei.write(rohdaten)
|
|
return pfad, shell_beleg_parser.beleg_lesen(pfad)
|
|
|
|
|
|
async def hochladen(k: Koordinator, daten: dict) -> None:
|
|
pdf_base64 = daten.get("pdf_base64")
|
|
tank_id = daten.get("tank_id")
|
|
if not pdf_base64:
|
|
_LOGGER.warning("Beleg-Upload ohne pdf_base64 aufgerufen")
|
|
return
|
|
|
|
dateiname = daten.get("dateiname") or (
|
|
f"beleg_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
|
)
|
|
# Alles bis zum geparsten Ergebnis in einem try: eine unbehandelte
|
|
# Ausnahme hier (kaputtes Base64, volle Festplatte, unlesbares PDF) wäre
|
|
# für die Oberfläche unsichtbar - ein abgelehnter Dienstaufruf landet dort
|
|
# nur in der Browser-Konsole, angezeigt wird nichts.
|
|
try:
|
|
rohdaten = base64.b64decode(pdf_base64)
|
|
pdf_pfad, geparst = await k.hass.async_add_executor_job(
|
|
_speichern_und_lesen, k.ablage.belege_ordner, dateiname, rohdaten
|
|
)
|
|
except Exception as fehler: # noqa: BLE001 - jede Ursache endet hier gleich
|
|
_LOGGER.error("Beleg konnte nicht verarbeitet werden: %s", fehler)
|
|
await k.beleg_ergebnis_veroeffentlichen(
|
|
{"fehler": "Beleg konnte nicht gelesen werden", "tank_id": tank_id}
|
|
)
|
|
return
|
|
|
|
receipt_key = geparst.get("receipt_key")
|
|
if not receipt_key:
|
|
_LOGGER.error("Parser lieferte keinen receipt_key")
|
|
await k.beleg_ergebnis_veroeffentlichen(
|
|
{"fehler": "Beleg konnte nicht gelesen werden", "tank_id": tank_id}
|
|
)
|
|
return
|
|
|
|
# §7.7 Regel 3: derselbe Beleg (receipt_key, minutengenau) erzeugt keinen
|
|
# zweiten Datensatz.
|
|
vorhandener = await k.ablage.tankvorgang_nach_receipt_key(receipt_key)
|
|
if vorhandener is not None and vorhandener.get("tank_id") != tank_id:
|
|
_LOGGER.info("Beleg %s bereits vorhanden, übersprungen", receipt_key)
|
|
# vorhandener_tank_id zeigt auf den Tankvorgang, der den Beleg schon
|
|
# trägt. Die Oberflächen springen damit dorthin, statt nur "bereits
|
|
# erfasst" zu melden - beim Teilen aus Mail ist genau das die Frage,
|
|
# die der Nutzer hat: welcher Vorgang ist das denn?
|
|
#
|
|
# Bewusst ein EIGENES Feld statt tank_id umzudeuten: tank_id ist die
|
|
# Eingabe des Aufrufs (leer beim Neuanlegen, gesetzt beim Nachtragen),
|
|
# und das Panel entscheidet daran, wo es den Fehler anzeigt.
|
|
await k.beleg_ergebnis_veroeffentlichen(
|
|
{
|
|
"fehler": "Beleg bereits erfasst",
|
|
"tank_id": tank_id,
|
|
"vorhandener_tank_id": vorhandener.get("tank_id"),
|
|
}
|
|
)
|
|
return
|
|
|
|
beleg_felder = _beleg_felder(geparst, pdf_pfad)
|
|
|
|
if not tank_id:
|
|
await k.beleg_ergebnis_veroeffentlichen({**beleg_felder, "tank_id": None})
|
|
_LOGGER.info("Beleg %s geparst, wartet auf Speichern in der Oberfläche", receipt_key)
|
|
return
|
|
|
|
await _nachtragen(k, tank_id, geparst, beleg_felder, receipt_key)
|
|
|
|
|
|
def _beleg_felder(geparst: dict, pdf_pfad: str) -> dict:
|
|
liters = geparst.get("liters")
|
|
fuel_total_eur = geparst.get("fuel_total_eur") # real, bereits rabattiert
|
|
return {
|
|
"receipt_key": geparst.get("receipt_key"),
|
|
"receipt_no": geparst.get("receipt_no"),
|
|
"tse_beleg_nr": geparst.get("tse_beleg_nr"),
|
|
"ts": geparst.get("ts"),
|
|
"ts_payment": geparst.get("ts_payment"),
|
|
"ts_tse": geparst.get("ts_tse"),
|
|
"station_id": geparst.get("station_id"),
|
|
"station_brand": geparst.get("station_brand"),
|
|
"station_name": geparst.get("station_name"),
|
|
"station_address": geparst.get("station_address"),
|
|
"article_no": geparst.get("article_no"),
|
|
"product_name": geparst.get("product_name"),
|
|
"fuel_type": geparst.get("fuel_type"),
|
|
"liters": liters,
|
|
"fuel_total_eur": fuel_total_eur,
|
|
"price_per_l": preis_je_liter(fuel_total_eur, liters),
|
|
"discount": geparst.get("discount"), # SmartDeal-Ersparnis
|
|
"discount_per_l": geparst.get("discount_per_l"),
|
|
"list_price_per_l": geparst.get("list_price_per_l"),
|
|
"receipt_total_eur": geparst.get("receipt_total_eur"),
|
|
"net_eur": geparst.get("net_eur"),
|
|
"vat_eur": geparst.get("vat_eur"),
|
|
"receipt_file": pdf_pfad,
|
|
}
|
|
|
|
|
|
async def _nachtragen(
|
|
k: Koordinator, tank_id: str, geparst: dict, beleg_felder: dict, receipt_key: str
|
|
) -> None:
|
|
vorhanden = await k.ablage.tankvorgang_nach_id(tank_id)
|
|
if not vorhanden:
|
|
_LOGGER.warning("Tankvorgang %s für Beleg-Nachtrag nicht gefunden", tank_id)
|
|
await k.beleg_ergebnis_veroeffentlichen(
|
|
{"fehler": "Tankvorgang nicht gefunden", "tank_id": tank_id}
|
|
)
|
|
return
|
|
|
|
vorhandenes_datum = (vorhanden.get("ts") or "")[:10]
|
|
belegdatum = (geparst.get("ts") or "")[:10]
|
|
if vorhandenes_datum and belegdatum and vorhandenes_datum != belegdatum:
|
|
_LOGGER.warning(
|
|
"Belegdatum %s passt nicht zu Tankvorgang %s (Datum %s)",
|
|
belegdatum, tank_id, vorhandenes_datum,
|
|
)
|
|
await k.beleg_ergebnis_veroeffentlichen(
|
|
{"fehler": "Falsches Belegdatum", "tank_id": tank_id}
|
|
)
|
|
return
|
|
|
|
aenderungen = dict(beleg_felder)
|
|
aenderungen["status"] = "vollständig"
|
|
if await k.ablage.tankvorgang_aktualisieren(tank_id, aenderungen):
|
|
await k.tankvorgang_nachbereiten()
|
|
await k.beleg_ergebnis_veroeffentlichen({"erfolg": True, "tank_id": tank_id})
|
|
_LOGGER.info("Beleg %s in Tankvorgang %s nachgetragen", receipt_key, tank_id)
|
|
else:
|
|
await k.beleg_ergebnis_veroeffentlichen(
|
|
{"fehler": "Tankvorgang nicht gefunden", "tank_id": tank_id}
|
|
)
|
|
|
|
|
|
# ------------------------------------------------ Tankvorgänge aus dem Formular
|
|
|
|
async def tankvorgang_manuell(k: Koordinator, daten: dict) -> None:
|
|
"""Erfassung aus dem Formular "Neuer Tankvorgang".
|
|
|
|
receipt_key/receipt_file sind optional: kommen sie mit (weil zuvor ein
|
|
Beleg ohne tank_id in den Entwurf übernommen wurde), zählt der Tankvorgang
|
|
als per Beleg erfasst, sonst als manuell."""
|
|
liter = daten.get("liter")
|
|
kosten = daten.get("kosten")
|
|
if liter is None or kosten is None:
|
|
_LOGGER.warning("liter und kosten sind Pflichtfelder")
|
|
return
|
|
|
|
ts_roh = daten.get("ts")
|
|
ts = zeit_normalisiert(ts_roh) if ts_roh else datetime.datetime.now(datetime.UTC)
|
|
|
|
# Eine 0 oder ein geleertes Feld heissen "kenne ich nicht" - siehe
|
|
# als_kilometerstand().
|
|
odometer_km = als_kilometerstand(daten.get("km"))
|
|
if odometer_km is None:
|
|
odometer_km = _km_aus_sensor_wenn_jetzt(k, ts)
|
|
distanz = daten.get("distanz")
|
|
if distanz is None:
|
|
# Vorschlag aus TANK_DISTANZ_SENSOR oder, ohne ihn, der Differenz zum
|
|
# letzten Tankvorgang - aber wie odometer_km selbst frei
|
|
# überschreibbar, ein übergebener Wert hat deshalb immer Vorrang.
|
|
distanz = await tankerkennung.distanz_seit_tankung(
|
|
k, odometer_km, jetzt=_ist_jetzt(ts)
|
|
)
|
|
|
|
receipt_key = daten.get("receipt_key")
|
|
tankvorgang = leerer_tankvorgang(
|
|
ts.isoformat(),
|
|
"beleg" if receipt_key else "manual",
|
|
)
|
|
tankvorgang.update({
|
|
"receipt_key": receipt_key,
|
|
"liters": liter,
|
|
"fuel_total_eur": kosten,
|
|
"price_per_l": preis_je_liter(kosten, liter),
|
|
"discount": daten.get("ersparnis"),
|
|
"station_name": daten.get("station"),
|
|
"odometer_km": odometer_km,
|
|
"distance_km": distanz,
|
|
"fuel_type": daten.get("kraftstoff"),
|
|
"status": "vollständig",
|
|
"receipt_file": daten.get("receipt_file"),
|
|
})
|
|
|
|
await k.ablage.tankvorgang_anhaengen(tankvorgang)
|
|
await k.tankvorgang_nachbereiten()
|
|
_LOGGER.info("Tankvorgang %s angelegt", tankvorgang["tank_id"])
|
|
|
|
|
|
# Wie nah der eingetragene Zeitpunkt an "jetzt" liegen muss, damit der
|
|
# aktuelle Tachostand als Ersatz taugt.
|
|
SENSOR_ERSATZ_FENSTER = datetime.timedelta(hours=2)
|
|
|
|
|
|
def _km_aus_sensor(k: Koordinator) -> float | None:
|
|
wert = zustand_oder_none(k.hass, k.zuordnung.werte.KM_SENSOR)
|
|
try:
|
|
return float(wert) # type: ignore[arg-type]
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def _ist_jetzt(ts: datetime.datetime | None) -> bool:
|
|
"""Liegt der eingetragene Zeitpunkt nah genug an jetzt, dass die
|
|
Momentanwerte der Sensoren zu ihm passen?"""
|
|
if ts is None:
|
|
return False
|
|
jetzt = datetime.datetime.now(datetime.UTC)
|
|
return abs((jetzt - ts).total_seconds()) <= SENSOR_ERSATZ_FENSTER.total_seconds()
|
|
|
|
|
|
def _km_aus_sensor_wenn_jetzt(
|
|
k: Koordinator, ts: datetime.datetime | None
|
|
) -> float | None:
|
|
"""Der aktuelle Tachostand - aber nur fuer einen Tankvorgang von jetzt.
|
|
|
|
Der Sensor kennt genau einen Wert: den von diesem Augenblick. Fuer eine
|
|
Tankung, die gerade passiert ist, ist das der richtige Ersatz und der
|
|
Normalfall - der Nutzer muss nichts eintippen.
|
|
|
|
Fuer eine nachgetragene Tankung von vorgestern ist es der falsche Wert,
|
|
und er richtet zweifachen Schaden an: die gefahrene Distanz wird gegen
|
|
einen zu hohen Stand gerechnet, und die Datumspruefung der
|
|
Verbrauchskorrektur verwirft das Intervall, weil der gespeicherte Stand
|
|
nicht zum Tacho des eingetragenen Zeitpunkts passt. Dann lieber gar kein
|
|
Kilometerstand - genau das, was eine 0 im Formular ausdruecken soll."""
|
|
if not _ist_jetzt(ts):
|
|
_LOGGER.info(
|
|
"Tankvorgang auf %s datiert - der aktuelle Tachostand passt dazu "
|
|
"nicht und wird nicht eingesetzt", ts.isoformat(),
|
|
)
|
|
return None
|
|
return _km_aus_sensor(k)
|
|
|
|
|
|
async def tankvorgang_aktualisieren(k: Koordinator, daten: dict) -> None:
|
|
"""Bearbeitet einen bestehenden Tankvorgang (Einzelbeleg-Seite, egal ob
|
|
ursprünglich per Beleg oder manuell angelegt).
|
|
|
|
Nur tatsächlich in `daten` enthaltene Schlüssel werden geändert - fehlt
|
|
ein Feld ganz (statt nur leer/None zu sein), bleibt der bisherige Wert
|
|
unangetastet. Das bestehende Bearbeiten-Formular (Einzelbeleg-Seite)
|
|
schickt ohnehin immer alle Felder mit, verhält sich also unverändert;
|
|
ein schlankerer Aufrufer wie der CSV-Import (csv_import.py) kann so
|
|
gezielt nur einzelne Spalten aktualisieren, ohne den Rest des
|
|
Datensatzes (odometer_km, discount, fuel_type, receipt_key, ...)
|
|
stillschweigend auf None zu setzen."""
|
|
tank_id = daten.get("tank_id")
|
|
if not tank_id:
|
|
_LOGGER.warning("tankvorgang_aktualisieren ohne tank_id aufgerufen")
|
|
return
|
|
|
|
# edited_fields fortschreiben statt ersetzen: was frueher von Hand gesetzt
|
|
# wurde, bleibt geschuetzt.
|
|
vorhanden = await k.ablage.tankvorgaenge_lesen()
|
|
alt = next((t for t in vorhanden if t.get("tank_id") == tank_id), None)
|
|
hand = set((alt or {}).get("edited_fields") or [])
|
|
|
|
aenderungen: dict = {}
|
|
if "ts" in daten:
|
|
ts_normalisiert = zeit_normalisiert(daten.get("ts"))
|
|
aenderungen["ts"] = ts_normalisiert.isoformat() if ts_normalisiert else daten.get("ts")
|
|
if "liter" in daten or "kosten" in daten:
|
|
liter = daten.get("liter")
|
|
kosten = daten.get("kosten")
|
|
aenderungen["liters"] = liter
|
|
aenderungen["fuel_total_eur"] = kosten
|
|
aenderungen["price_per_l"] = preis_je_liter(kosten, liter)
|
|
if liter is not None:
|
|
# Von Hand eingetippte Liter sind der Beleg, nur ohne PDF - und
|
|
# damit dieselbe Wahrheit wie ein geparster Beleg. Ohne diesen
|
|
# Vermerk haelt verbrauchskorrektur.beleg_liter() sie fuer den
|
|
# Anstieg des Gebers und verwirft sie als Zirkelschluss; der
|
|
# Verbrauchsfaktor entstuende nie.
|
|
#
|
|
# Nebenwirkung, bewusst in Kauf genommen: edited_fields schuetzt
|
|
# das Feld vor der Automatik. Wird spaeter doch noch ein PDF zu
|
|
# demselben Tankvorgang hochgeladen, bleibt die getippte Zahl
|
|
# stehen - der Mensch hatte den Beleg in der Hand.
|
|
hand.add("liters")
|
|
if "km" in daten:
|
|
aenderungen["odometer_km"] = als_kilometerstand(daten.get("km"))
|
|
if "ersparnis" in daten:
|
|
aenderungen["discount"] = daten.get("ersparnis")
|
|
if "station" in daten:
|
|
aenderungen["station_name"] = daten.get("station")
|
|
if "distanz" in daten:
|
|
aenderungen["distance_km"] = daten.get("distanz")
|
|
if "kraftstoff" in daten:
|
|
aenderungen["fuel_type"] = daten.get("kraftstoff")
|
|
if daten.get("receipt_key"):
|
|
aenderungen["receipt_key"] = daten["receipt_key"]
|
|
aenderungen["receipt_file"] = daten.get("receipt_file")
|
|
aenderungen["source"] = "beleg"
|
|
|
|
if hand:
|
|
aenderungen["edited_fields"] = sorted(hand)
|
|
|
|
if await k.ablage.tankvorgang_aktualisieren(tank_id, aenderungen):
|
|
await k.tankvorgang_nachbereiten()
|
|
_LOGGER.info("Tankvorgang %s aktualisiert", tank_id)
|
|
else:
|
|
_LOGGER.warning("Tankvorgang %s nicht gefunden", tank_id)
|
|
|
|
|
|
async def tankvorgang_loeschen(k: Koordinator, tank_id: str | None) -> None:
|
|
if not tank_id:
|
|
_LOGGER.warning("tankvorgang_loeschen ohne tank_id aufgerufen")
|
|
return
|
|
if await k.ablage.tankvorgang_loeschen(tank_id):
|
|
await k.tankvorgang_nachbereiten()
|
|
_LOGGER.info("Tankvorgang %s gelöscht", tank_id)
|
|
else:
|
|
_LOGGER.warning("Tankvorgang %s nicht gefunden", tank_id)
|
|
|
|
|
|
async def marken_nachtragen(k: Koordinator) -> None:
|
|
"""Traegt die Tankstellenmarke bei Belegen nach, die vor dem 16.08.2026
|
|
eingelesen wurden.
|
|
|
|
WARUM ES DAS BRAUCHT
|
|
--------------------
|
|
Bis zum 16.08.2026 kannte der Parser keine Marken; `station_name` trug
|
|
deshalb die Firmierung des Betreibers ("TC Sengül GmbH"), und die Marke
|
|
war fuer diese Datensaetze nirgends gespeichert. Auf dem BELEG steht sie
|
|
aber weiterhin - jeder der geprueften Altbelege beginnt mit "Shell-Station"
|
|
bzw. "SHELL STATION". Ohne dieses Nachtragen bliebe die Einzelbeleg-Zeile
|
|
dieser Vorgaenge dauerhaft ohne Marke (Befund des Eigentuemers,
|
|
04.09.2026).
|
|
|
|
VORSICHTIG GEHALTEN
|
|
-------------------
|
|
* Angefasst wird nur, wo `station_brand` noch gar nicht vorkommt und die
|
|
Belegdatei tatsaechlich liegt. Ein einmal eingetragenes Feld - auch mit
|
|
Wert None - wird nie wieder ueberschrieben; damit ueberschreibt das
|
|
Nachtragen keine Korrektur von Hand und laeuft nicht bei jedem Start
|
|
erneut ueber dieselben Belege.
|
|
* Gelesen wird nur der Belegkopf (marke_aus_pdf), nicht der ganze Beleg.
|
|
Ein Beleg, an dem beide Parser scheitern wuerden, gibt seine Marke
|
|
trotzdem her - und ein Lesefehler bleibt folgenlos.
|
|
* Geschrieben wird nur, wenn sich wirklich etwas geaendert hat.
|
|
"""
|
|
vorgaenge = await k.ablage.tankvorgaenge_lesen()
|
|
offen = [
|
|
v
|
|
for v in vorgaenge
|
|
if "station_brand" not in v and v.get("receipt_file")
|
|
]
|
|
if not offen:
|
|
return
|
|
|
|
def _lesen() -> dict[str, str | None]:
|
|
gefunden: dict[str, str | None] = {}
|
|
for vorgang in offen:
|
|
pfad = vorgang.get("receipt_file")
|
|
if not pfad or not os.path.exists(pfad):
|
|
continue
|
|
gefunden[vorgang["tank_id"]] = shell_beleg_parser.marke_aus_pdf(pfad)
|
|
return gefunden
|
|
|
|
marken = await k.hass.async_add_executor_job(_lesen)
|
|
if not marken:
|
|
return
|
|
|
|
for vorgang in vorgaenge:
|
|
if vorgang.get("tank_id") in marken:
|
|
vorgang["station_brand"] = marken[vorgang["tank_id"]]
|
|
await k.ablage.tankvorgaenge_schreiben(vorgaenge)
|
|
benannt = sorted({m for m in marken.values() if m})
|
|
zusatz = ", ".join(benannt) if benannt else "keine erkennbar"
|
|
_LOGGER.info("Tankstellenmarke bei %d Beleg(en) nachgetragen (%s)", len(marken), zusatz)
|