Eigene Felder: Spalten in der Artikelliste und Export nach Feldwerten
Eigene Felder waren bisher nur im Artikelformular zu sehen. Zwei Luecken
daraus: In den Listen tauchten sie gar nicht auf, und es gab keinen Weg,
gezielt nach einem Feldwert zu exportieren.
SPALTEN. Je eigenem Feld eine Spalte in der Artikelliste, Schluessel
feld:<id> wie im Etikettenexport (die ID und nicht key, der ist nur je
Kategorie eindeutig). Reine Oberflaechenarbeit: GET /products liefert die
Werte laengst mit, Products.jsx warf sie nur weg. Filtern, Sortieren,
Ein-/Ausblenden und das Merken erbt die Spalte von DataTable.
Ja/Nein erscheint als Haken, liefert zum Filtern aber "Ja"/"Nein" - und
"Nein" auch ohne gespeicherten Wert: Wer den Haken wegnimmt, loescht die
Zeile, "nein" und "nie gesetzt" sind in den Daten dasselbe. Zahlen sortieren
als Zahl, sonst stuende "10" vor "9".
DataTable lernt dafuer defaultHidden: Alle Feldspalten starten verborgen und
sind erst auf Wunsch da - sonst hinge die Liste je nach Haushalt voller
Spalten, die fuer die meisten Zeilen gar nicht gelten.
Dabei einen vorhandenen Fehler gefunden: urlState las die Sortierung mit
split(":") und nahm das erste Stueck als Spalte. Bei "feld:12:asc" kam
dadurch "feld" heraus, und die Sortierung fiel beim Neuladen still weg. Jetzt
am LETZTEN Doppelpunkt getrennt.
EXPORT. Die Etikettenvorlagen bekommen einen Wertefilter: feldfilter in der
Konfiguration, mehrere Bedingungen UND-verknuepft, genau verglichen ohne
Ruecksicht auf Gross-/Kleinschreibung, leerer Wert heisst "ohne Wert".
Gefiltert wird in Python ueber die ohnehin geladenen Feldwerte - auf value
liegt kein Index, und "fehlende Zeile zaehlt als leer" braeuchte in SQL je
Bedingung ein NOT EXISTS.
Der Filter gilt auch fuer Einzelstuecke; der Wert haengt am Artikel des
Stuecks. Ein geloeschtes Feld laesst seine Bedingung wegfallen, genau wie
eine Spalte zu einem geloeschten Feld still verschwindet.
Die Sicherung schreibt den Filter wie die Spalten auf Kategorie::Label um -
ohne das zeigte er nach dem Zurueckspielen auf ein fremdes Feld, und der
Export gaebe mehr Zeilen aus als das Original. Dafuer ist _konfiguration aus
dem Router als konfiguration_schreiben in den Dienst gezogen: Sonst haetten
DREI Stellen denselben JSON-Text gebaut.
Zehn neue Tests, 540 gruen. Keine Schemaaenderung - konfiguration ist freies
JSON, alte Vorlagen lesen sich als leerer Filter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
83f7134459
commit
dd57d3356a
@@ -24,6 +24,7 @@ from ..models import (
|
||||
Category,
|
||||
EtikettenVorlage,
|
||||
FieldDefinition,
|
||||
FieldType,
|
||||
Item,
|
||||
Location,
|
||||
Product,
|
||||
@@ -231,10 +232,98 @@ def konfiguration_lesen(vorlage: EtikettenVorlage) -> dict:
|
||||
trenn = roh.get("trennzeichen") or ","
|
||||
if trenn not in (",", ";", "\t"):
|
||||
trenn = ","
|
||||
return {"spalten": spalten, "kategorie_ids": kategorie_ids, "trennzeichen": trenn}
|
||||
# Kaputte Eintraege fallen einzeln weg, nicht der ganze Filter - sonst
|
||||
# entschiede ein Tippfehler darueber, ob ein Export alles ausgibt.
|
||||
feldfilter = []
|
||||
for eintrag in roh.get("feldfilter", []) if isinstance(roh.get("feldfilter"), list) else []:
|
||||
if not isinstance(eintrag, dict):
|
||||
continue
|
||||
fid = eintrag.get("feld_id")
|
||||
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 ""})
|
||||
return {
|
||||
"spalten": spalten,
|
||||
"kategorie_ids": kategorie_ids,
|
||||
"trennzeichen": trenn,
|
||||
"feldfilter": feldfilter,
|
||||
}
|
||||
|
||||
|
||||
def _zeilen(db: Session, art: str, kategorie_ids: list[int]) -> list:
|
||||
def konfiguration_schreiben(
|
||||
spalten: list[str],
|
||||
kategorie_ids: list[int],
|
||||
trennzeichen: str,
|
||||
feldfilter: list[dict] | None = None,
|
||||
) -> str:
|
||||
"""Gegenstueck zu konfiguration_lesen.
|
||||
|
||||
Lag frueher als _konfiguration im Router. Hierher gezogen, weil sonst DREI
|
||||
Stellen denselben JSON-Text bauen (POST, PATCH und der Sicherungs-Import)
|
||||
und ein neuer Schluessel an jeder einzeln nachgetragen werden muesste.
|
||||
"""
|
||||
return json.dumps(
|
||||
{
|
||||
"spalten": spalten,
|
||||
"kategorie_ids": kategorie_ids,
|
||||
"trennzeichen": trennzeichen if trennzeichen in (",", ";", "\t") else ",",
|
||||
"feldfilter": feldfilter or [],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
def _feld_passt(fd: FieldDefinition, bedingung: str, ist: str | None) -> bool:
|
||||
"""Trifft der gespeicherte Wert die Bedingung?
|
||||
|
||||
Ja/Nein ist der heikle Fall: Nimmt man den Haken weg, loescht
|
||||
apply_field_values die Zeile (services/fields.py). „nein" und „nie gesetzt"
|
||||
sind in den Daten also dasselbe - beide muessen treffen, sonst faende
|
||||
„alle mit Nein" so gut wie nichts.
|
||||
|
||||
coerce_value wird NUR fuer Ja/Nein benutzt: Bei Auswahl, Zahl und Datum
|
||||
wirft es bei unpassendem Text, und ein vertippter Filterwert soll keinen
|
||||
Serverfehler beim Herunterladen ausloesen.
|
||||
"""
|
||||
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()
|
||||
|
||||
|
||||
def _nach_feldwerten(db: Session, art: str, zeilen: list, feldfilter: list[dict]) -> list:
|
||||
"""Zeilen auf die reduzieren, die ALLE Bedingungen erfuellen.
|
||||
|
||||
In Python und nicht in SQL: Auf ProductFieldValue.value liegt kein Index,
|
||||
„fehlende Zeile zaehlt als leer" braeuchte je Bedingung ein NOT EXISTS, und
|
||||
die Mengen sind haushaltsgross. _feldwerte holt ohnehin alles in einem Zug.
|
||||
|
||||
Ein geloeschtes Feld laesst seine Bedingung wegfallen - genau wie eine
|
||||
Spalte zu einem geloeschten Feld still verschwindet.
|
||||
"""
|
||||
bedingungen = []
|
||||
for eintrag in feldfilter:
|
||||
fd = db.get(FieldDefinition, eintrag["feld_id"])
|
||||
if fd is not None:
|
||||
bedingungen.append((fd, eintrag["wert"]))
|
||||
if not bedingungen:
|
||||
return zeilen
|
||||
|
||||
werte = _feldwerte(db, art, zeilen)
|
||||
passend = []
|
||||
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):
|
||||
passend.append(zeile)
|
||||
return passend
|
||||
|
||||
|
||||
def _zeilen(
|
||||
db: Session, art: str, kategorie_ids: list[int], feldfilter: list[dict] | None = None
|
||||
) -> list:
|
||||
if art == "locations":
|
||||
return db.query(Location).order_by(Location.name).all()
|
||||
|
||||
@@ -250,14 +339,16 @@ def _zeilen(db: Session, art: str, kategorie_ids: list[int]) -> list:
|
||||
frage = db.query(Product)
|
||||
if ids is not None:
|
||||
frage = frage.filter(Product.category_id.in_(ids or {-1}))
|
||||
return frage.order_by(Product.name).all()
|
||||
zeilen = frage.order_by(Product.name).all()
|
||||
else:
|
||||
frage = db.query(Item)
|
||||
if ids is not None:
|
||||
frage = frage.join(Product, Item.product_id == Product.id).filter(
|
||||
Product.category_id.in_(ids or {-1})
|
||||
)
|
||||
zeilen = frage.order_by(Item.id).all()
|
||||
|
||||
frage = db.query(Item)
|
||||
if ids is not None:
|
||||
frage = frage.join(Product, Item.product_id == Product.id).filter(
|
||||
Product.category_id.in_(ids or {-1})
|
||||
)
|
||||
return frage.order_by(Item.id).all()
|
||||
return _nach_feldwerten(db, art, zeilen, feldfilter) if feldfilter else zeilen
|
||||
|
||||
|
||||
def _feldwerte(db: Session, art: str, zeilen: list) -> dict[int, dict[int, str | None]]:
|
||||
@@ -335,7 +426,7 @@ def csv_erzeugen(db: Session, vorlage: EtikettenVorlage, basis_url: str = "") ->
|
||||
# Lieber die festen Spalten der Bezugsart als eine leere Datei.
|
||||
gueltig = list(SPALTEN.get(art, {}).keys())
|
||||
|
||||
zeilen = _zeilen(db, art, konf["kategorie_ids"])
|
||||
zeilen = _zeilen(db, art, konf["kategorie_ids"], konf["feldfilter"])
|
||||
kontext = Kontext(db=db, basis_url=basis_url, feldwerte=_feldwerte(db, art, zeilen))
|
||||
|
||||
puffer = io.StringIO()
|
||||
|
||||
Reference in New Issue
Block a user