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
@@ -1331,6 +1331,13 @@ function markeErkennen(name) {
return null;
}
/* NUR DER ZWEITE WEG: seit dem 04.09.2026 speichert die Integration die Marke
als eigenes Feld `station_brand` (shell_beleg_parser._marke, aus dem
Belegkopf), und fuer Altbelege traegt sie belege.marken_nachtragen() aus der
abgelegten Belegdatei nach. Wo dieses Feld etwas hergibt, gilt es - hier
geraten wird nur, wo es fehlt: bei Vorgaengen ohne Beleg, bei von Hand
eingetragenen Tankstellen und solange das Nachtragen nicht gelaufen ist. */
/* Kennzeichen einer Firmierung. Ein Teil, der so etwas enthaelt, ist der
BETREIBER und gehoert nach Vorgabe des Eigentuemers nicht in die Anzeige. */
const FIRMA_MUSTER = /\b(gmbh|mbh|ohg|kg|ag|e\.\s?k\.|inh\.|& co)\b/i;
@@ -1357,13 +1364,23 @@ function strasseKuerzen(strasse) {
Die Markenerkennung lief bis hierher nur ueber den Namen - bei den aelteren
Belegen steht die Marke dort gar nicht, deshalb blieb in der Uebersicht die
Firmierung stehen ("...GmbH", Nutzerbefund 04.09.2026), waehrend die
Anschrift daneben ungenutzt blieb. Gelesen werden jetzt beide Felder. */
function tankstelleTeile(name, adresse) {
const teile = { marke: markeErkennen(name) || "", strasse: "", hausnummer: "", plz: "", ort: "", roh: (name || "").trim() };
Anschrift daneben ungenutzt blieb. Gelesen werden jetzt beide Felder.
`marke` ist die gespeicherte Angabe des Belegs (station_brand). Sie geht
vor, weil sie vom Belegkopf stammt und nicht aus dem Anzeigenamen geraten
ist - genau die Luecke, durch die der Beleg vom 10.11.2025 ohne Marke
dastand, obwohl "Shell Station" auf ihm steht. */
function tankstelleTeile(name, adresse, marke) {
const teile = { marke: (marke || "").trim() || markeErkennen(name) || "", strasse: "", hausnummer: "", plz: "", ort: "", roh: (name || "").trim() };
const quellen = (adresse || "").split(",");
for (const stueck of (name || "").split(",")) {
const t = stueck.trim();
if (teile.marke && t.toLowerCase() === teile.marke.toLowerCase()) continue;
// Ein Teil, der fuer sich genommen eine Kopfmarke IST, taugt nie als Ort.
// Frueher stand hier nur der Vergleich mit der gewaehlten Marke; seit die
// gespeicherte Angabe vorgeht, koennen beide auseinanderfallen, und dann
// rutschte die Marke aus dem Namen in den Ort.
const eigen = t ? markeErkennen(t) : null;
if (eigen && eigen.toLowerCase() === t.toLowerCase()) continue;
quellen.push(t);
}
for (const stueck of quellen) {
@@ -1395,16 +1412,16 @@ function tankstelleTeile(name, adresse) {
(Vorgabe des Eigentuemers, 04.09.2026). Ergibt die Zerlegung nichts, bleibt
der rohe Name stehen - ihn wegzulassen hiesse, eine bekannte Tankstelle
"unbekannt" zu nennen. */
function tankstelleKurz(name, adresse) {
const t = tankstelleTeile(name, adresse);
function tankstelleKurz(name, adresse, marke) {
const t = tankstelleTeile(name, adresse, marke);
return [t.marke, t.strasse, t.ort].filter(Boolean).join(", ") || t.roh || "unbekannt";
}
/* Die beiden Zeilen im Einzelbeleg: oben Marke und Strasse mit Hausnummer,
unten Postleitzahl und Ort - geschrieben wie eine Anschrift, das Komma
trennt nur die Marke ab. `oben` ist nie leer. */
function tankstelleZeilen(name, adresse) {
const t = tankstelleTeile(name, adresse);
function tankstelleZeilen(name, adresse, marke) {
const t = tankstelleTeile(name, adresse, marke);
const strasse = [t.strasse, t.hausnummer].filter(Boolean).join(" ");
let oben = [t.marke, strasse].filter(Boolean).join(", ");
let unten = [t.plz, t.ort].filter(Boolean).join(" ");
@@ -1442,10 +1459,10 @@ function tankstelleSuchtext(name, adresse) {
/* Ziel fuer den Kartendienst. Hier gehoeren Name UND Anschrift zusammen:
Apple Karten und Google Maps kennen Betriebsnamen und treffen damit die
Zapfsaeule statt des Nachbargebaeudes. */
function tankstelleZiel(name, adresse) {
function tankstelleZiel(name, adresse, marke) {
const a = (adresse || "").trim();
if (!a) return (name || "").trim();
return [markeErkennen(name) || "", a].filter(Boolean).join(", ");
return [(marke || "").trim() || markeErkennen(name) || "", a].filter(Boolean).join(", ");
}
/* Die Kandidaten der Reihe nach nachschlagen, erster Treffer gewinnt. */
@@ -1467,9 +1484,9 @@ function apfelgeraet() {
Tankstellen-Verweis im Einzelbeleg will das Zweite (Vorgabe des
Eigentuemers, 04.09.2026): erst sehen, wo sie liegt - eine Route erwartet
dort niemand, und sie ist im Kartendienst einen Tipp entfernt. */
function kartendienstUrl(name, adresse, pos, modus = "route") {
function kartendienstUrl(name, adresse, pos, modus = "route", marke) {
const beschriftung = (name || "").trim();
const ziel = pos ? `${pos.lat},${pos.lon}` : tankstelleZiel(name, adresse);
const ziel = pos ? `${pos.lat},${pos.lon}` : tankstelleZiel(name, adresse, marke);
if (!ziel) return null;
if (apfelgeraet()) {
if (modus === "zeigen") {
@@ -1499,7 +1516,7 @@ function tankstelleAufKarteZeigen(karte, pos, f) {
// Dieselben zwei Zeilen wie in der Wertezeile darunter (tankstelleZeilen) -
// die Nadel soll nicht die Firmierung des Betreibers nennen, wenn die Zeile
// die Kopfmarke zeigt.
const zeilen = tankstelleZeilen(f.station_name, f.station_address);
const zeilen = tankstelleZeilen(f.station_name, f.station_address, f.station_brand);
window.L.marker([pos.lat, pos.lon], { icon }).addTo(karte)
.bindPopup(esc(zeilen.oben) + (zeilen.unten ? "<br>" + esc(zeilen.unten) : ""));
// Position merken und den Knopf scharf schalten - er kann erst jetzt etwas
@@ -4752,7 +4769,7 @@ function vFuel() {
<span>Ø ${de(schnittpreis(fm), 2)} /l</span></span></button>
<div class="acc-b ${mi === 0 ? "on" : ""}" id="fm${j}${m}">`;
fm.forEach((f) => { const preis = literpreis(f); h += swipeZeile(`fill:${f.tank_id}`, `<button class="leaf" data-go="fill:${f.tank_id}">
<span class="k">${dedat(new Date(f.ts))}<small>${esc(tankstelleKurz(f.station_name, f.station_address))}</small></span>
<span class="k">${dedat(new Date(f.ts))}<small>${esc(tankstelleKurz(f.station_name, f.station_address, f.station_brand))}</small></span>
<span class="v">${f.liters != null ? de(f.liters, 2) + " l" : ""}<small>${preis != null ? de(preis, 2) + " €/l" : ""}</small></span>
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); });
h += `</div></div>`;
@@ -4793,8 +4810,8 @@ function belegKarte(f) {
Geraets; ohne Name und ohne Anschrift bleibt es eine stumme Zeile, denn
ein Verweis, der nirgendwohin fuehrt, waere schlechter als keiner. */
function tankstellenZeile(f) {
const { oben, unten } = tankstelleZeilen(f.station_name, f.station_address);
const url = kartendienstUrl(f.station_name, f.station_address, null, "zeigen");
const { oben, unten } = tankstelleZeilen(f.station_name, f.station_address, f.station_brand);
const url = kartendienstUrl(f.station_name, f.station_address, null, "zeigen", f.station_brand);
const inhalt =
`<span class="tankstellenzeile-ort">${esc(oben)}${unten ? `<small>${esc(unten)}</small>` : ""}</span>` +
(url ? `<svg class="chev" viewBox="0 0 6 10" aria-hidden="true"><polyline points="1,1 5,5 1,9"/></svg>` : "");