Exportbedingungen: Vergleichsart je Feldart statt nacktem Textfeld

Bisher tippte man den Wert immer von Hand ein - auch bei einem Haken, wo man
wissen musste, dass intern "true" steht, und bei einer Auswahl, wo ein
Vertipper stillschweigend nichts fand. Das ist der unangenehmste Fehlschlag:
Die CSV kommt einfach leer heraus, ohne Hinweis warum.

Jetzt bringt jede Bedingung die passende Eingabe mit:

  Ja/Nein    ein Haken
  Auswahl    "ist gleich" / "ist nicht gleich" plus das Dropdown der Optionen
  Zahl       "ist groesser =" / "ist =" / "ist kleiner =" plus Zahlenfeld
  Text       wie bisher freie Eingabe, leer = ohne Wert

Damit die Oberflaeche das kann, traegt /etiketten/spalten jetzt Art und
Auswahlwerte je Feld mit - vorher gab es nur Schluessel und Titel.

Der Filter bekommt dafuer ein op (=, !=, >=, <=), Vorgabe "=", damit
bestehende Vorlagen unveraendert weiterlaufen. Groesser/kleiner vergleicht
als Zahl und nimmt Komma wie Punkt; Zeilen ohne Wert treffen dabei NIE -
"leer" ist nicht kleiner als drei, es ist unvergleichbar. Eine unbekannte
Vergleichsart faellt auf "=" zurueck statt den Export zu zerreissen.

Die Sicherung traegt das op mit, wie schon das Feld selbst.

Sieben neue Tests, 547 gruen. Diesmal zusaetzlich in der laufenden Oberflaeche
durchgespielt: Alle vier Feldarten zeigen die richtige Eingabe, der Filter
wird gespeichert, und die CSV enthaelt bei "Laenge >= 5" genau die Artikel
mit 5 und 12 - nicht den mit 2 und nicht den ohne Wert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-16 16:18:48 +02:00
co-authored by Claude Opus 5
parent dd57d3356a
commit 8de222072c
5 changed files with 250 additions and 31 deletions
+43 -7
View File
@@ -20,6 +20,7 @@ from typing import Any, Callable
from sqlalchemy.orm import Session
from .fields import options_list
from ..models import (
Category,
EtikettenVorlage,
@@ -192,7 +193,15 @@ def feld_spalten(db: Session, art: str, kategorie_ids: list[int]) -> list[dict]:
continue
gesehen.add(fd.id)
titel = f"{fd.label} ({fd.unit})" if fd.unit else fd.label
spalten.append({"schluessel": f"{FELD_PRAEFIX}{fd.id}", "titel": titel})
# Art und Auswahlwerte kommen mit: Erst damit kann die Oberflaeche beim
# Wertefilter eine Checkbox, ein Dropdown oder ein Zahlenfeld anbieten
# statt eines nackten Textfelds, in das man sich vertippt.
spalten.append({
"schluessel": f"{FELD_PRAEFIX}{fd.id}",
"titel": titel,
"typ": fd.field_type,
"optionen": options_list(fd),
})
return spalten
@@ -242,7 +251,14 @@ def konfiguration_lesen(vorlage: EtikettenVorlage) -> dict:
if not str(fid).lstrip("-").isdigit():
continue
wert = eintrag.get("wert", "")
feldfilter.append({"feld_id": int(fid), "wert": str(wert) if wert is not None else ""})
op = eintrag.get("op", "=")
if op not in ("=", "!=", ">=", "<="):
op = "="
feldfilter.append({
"feld_id": int(fid),
"op": op,
"wert": str(wert) if wert is not None else "",
})
return {
"spalten": spalten,
"kategorie_ids": kategorie_ids,
@@ -274,7 +290,15 @@ def konfiguration_schreiben(
)
def _feld_passt(fd: FieldDefinition, bedingung: str, ist: str | None) -> bool:
def _als_zahl(text: str | None) -> float | None:
"""Zahl aus dem gespeicherten Text – Komma gilt wie Punkt."""
try:
return float(str(text).replace(",", "."))
except (TypeError, ValueError):
return None
def _feld_passt(fd: FieldDefinition, op: str, bedingung: str, ist: str | None) -> bool:
"""Trifft der gespeicherte Wert die Bedingung?
Ja/Nein ist der heikle Fall: Nimmt man den Haken weg, loescht
@@ -289,8 +313,20 @@ def _feld_passt(fd: FieldDefinition, bedingung: str, ist: str | None) -> bool:
if fd.field_type == FieldType.boolean.value:
from .fields import coerce_value
return (coerce_value(fd, ist) or "false") == (coerce_value(fd, bedingung) or "false")
return (ist or "").casefold() == (bedingung or "").strip().casefold()
gleich = (coerce_value(fd, ist) or "false") == (coerce_value(fd, bedingung) or "false")
return not gleich if op == "!=" else gleich
if op in (">=", "<="):
# Groesser/kleiner gibt es nur auf Zahlen. Ohne Wert oder mit
# unlesbarem Text trifft die Bedingung nie - „leer" ist nicht kleiner
# als drei, es ist unvergleichbar.
links, rechts = _als_zahl(ist), _als_zahl(bedingung)
if links is None or rechts is None:
return False
return links >= rechts if op == ">=" else links <= rechts
gleich = (ist or "").casefold() == (bedingung or "").strip().casefold()
return not gleich if op == "!=" else gleich
def _nach_feldwerten(db: Session, art: str, zeilen: list, feldfilter: list[dict]) -> list:
@@ -307,7 +343,7 @@ def _nach_feldwerten(db: Session, art: str, zeilen: list, feldfilter: list[dict]
for eintrag in feldfilter:
fd = db.get(FieldDefinition, eintrag["feld_id"])
if fd is not None:
bedingungen.append((fd, eintrag["wert"]))
bedingungen.append((fd, eintrag.get("op", "="), eintrag["wert"]))
if not bedingungen:
return zeilen
@@ -316,7 +352,7 @@ def _nach_feldwerten(db: Session, art: str, zeilen: list, feldfilter: list[dict]
for zeile in zeilen:
produkt_id = zeile.product_id if art == "items" else getattr(zeile, "id", None)
eigene = werte.get(produkt_id, {})
if all(_feld_passt(fd, wert, eigene.get(fd.id)) for fd, wert in bedingungen):
if all(_feld_passt(fd, op, wert, eigene.get(fd.id)) for fd, op, wert in bedingungen):
passend.append(zeile)
return passend