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:
Scarriffle
2026-10-16 16:02:53 +02:00
co-authored by Claude Opus 5
parent 83f7134459
commit dd57d3356a
10 changed files with 590 additions and 45 deletions
+12 -17
View File
@@ -12,7 +12,6 @@ vorbehalten, Exportieren darf jeder Angemeldete – es ist ein Lesevorgang.
from __future__ import annotations
import io
import json
import re
import unicodedata
import zipfile
@@ -34,6 +33,7 @@ from ..services.etiketten import (
ARTEN,
csv_erzeugen,
konfiguration_lesen,
konfiguration_schreiben,
verfuegbare_spalten,
)
@@ -129,22 +129,12 @@ def _als_out(vorlage: EtikettenVorlage) -> EtikettenVorlageOut:
spalten=konf["spalten"],
kategorie_ids=konf["kategorie_ids"],
trennzeichen=konf["trennzeichen"],
feldfilter=konf["feldfilter"],
datei_name=vorlage.datei_name,
updated_at=vorlage.updated_at,
)
def _konfiguration(spalten: list[str], kategorie_ids: list[int], trennzeichen: str) -> str:
return json.dumps(
{
"spalten": spalten,
"kategorie_ids": kategorie_ids,
"trennzeichen": trennzeichen if trennzeichen in (",", ";", "\t") else ",",
},
ensure_ascii=False,
)
@router.get("/spalten", response_model=list[SpalteOut])
def spalten(
art: str = "items",
@@ -194,8 +184,12 @@ def vorlage_anlegen(
name=name,
art=payload.art,
position=(hoechste.position + 1) if hoechste else 0,
konfiguration=_konfiguration(
payload.spalten, payload.kategorie_ids, payload.trennzeichen
konfiguration=konfiguration_schreiben(
payload.spalten,
payload.kategorie_ids,
payload.trennzeichen,
# json.dumps kann mit Pydantic-Modellen nichts anfangen.
[f.model_dump() for f in payload.feldfilter],
),
)
db.add(vorlage)
@@ -231,14 +225,15 @@ def vorlage_aendern(
if "position" in data:
vorlage.position = data["position"]
# Die drei Konfigurationswerte liegen zusammen in einem JSON-Text; wer nur
# Die Konfigurationswerte liegen zusammen in einem JSON-Text; wer nur
# einen schickt, darf die anderen nicht verlieren.
if {"spalten", "kategorie_ids", "trennzeichen"} & data.keys():
if {"spalten", "kategorie_ids", "trennzeichen", "feldfilter"} & data.keys():
alt = konfiguration_lesen(vorlage)
vorlage.konfiguration = _konfiguration(
vorlage.konfiguration = konfiguration_schreiben(
data.get("spalten", alt["spalten"]),
data.get("kategorie_ids", alt["kategorie_ids"]),
data.get("trennzeichen", alt["trennzeichen"]),
data.get("feldfilter", alt["feldfilter"]),
)
db.commit()
+38 -6
View File
@@ -23,7 +23,7 @@ from ..services.dates import MONTH, clean_precision, normalize_best_before
from ..services import gruppen as gruppen_graph
from ..services.group_codes import sync as sync_group_code
from ..services.min_stock import lies_ueberall, schreibe_ueberall
from ..services.etiketten import FELD_PRAEFIX, konfiguration_lesen
from ..services.etiketten import FELD_PRAEFIX, konfiguration_lesen, konfiguration_schreiben
from ..models import (
BaseUnit,
Category,
@@ -493,6 +493,17 @@ def _vorlagen_exportieren(db: Session) -> list[dict]:
)
else:
spalten.append(schluessel)
# Der Wertefilter zeigt ebenfalls auf Feld-IDs und braucht dieselbe
# Umschreibung wie die Spalten - sonst zeigt er nach dem Zurueckspielen
# auf ein fremdes Feld oder ist still weg.
feldfilter = []
for bedingung in konf["feldfilter"]:
fd = db.get(FieldDefinition, bedingung["feld_id"])
if fd is None:
continue
pfad = _category_path(db, db.get(Category, fd.category_id))
feldfilter.append({"feld": f"{pfad}::{fd.label}", "wert": bedingung["wert"]})
zeilen.append({
"name": v.name,
"art": v.art,
@@ -502,6 +513,7 @@ def _vorlagen_exportieren(db: Session) -> list[dict]:
_category_path(db, db.get(Category, kid)) for kid in konf["kategorie_ids"]
],
"trennzeichen": konf["trennzeichen"],
"feldfilter": feldfilter,
"datei_name": v.datei_name,
"datei_base64": base64.b64encode(v.datei).decode() if v.datei else None,
})
@@ -544,6 +556,25 @@ def _vorlagen_importieren(db: Session, zeilen: list, errors: list[str]) -> int:
if fd is not None:
spalten.append(f"{FELD_PRAEFIX}{fd.id}")
feldfilter: list[dict] = []
for bedingung in entry.get("feldfilter", []):
if not isinstance(bedingung, dict):
continue
pfad, _, label = str(bedingung.get("feld", "")).rpartition("::")
kat_id = pfad_zu_id.get(pfad)
fd = (
db.query(FieldDefinition)
.filter(
FieldDefinition.category_id == kat_id,
FieldDefinition.label == label,
).first()
if kat_id else None
)
# Wie bei den Spalten: fehlt das Feld hier, faellt die Bedingung
# weg statt die ganze Vorlage scheitern zu lassen.
if fd is not None:
feldfilter.append({"feld_id": fd.id, "wert": bedingung.get("wert", "")})
kategorie_ids = [
pfad_zu_id[p] for p in entry.get("kategorien", []) if p in pfad_zu_id
]
@@ -552,11 +583,12 @@ def _vorlagen_importieren(db: Session, zeilen: list, errors: list[str]) -> int:
name=name,
art=entry.get("art") or "items",
position=int(entry.get("position") or 0),
konfiguration=json.dumps({
"spalten": spalten,
"kategorie_ids": kategorie_ids,
"trennzeichen": entry.get("trennzeichen") or ",",
}, ensure_ascii=False),
konfiguration=konfiguration_schreiben(
spalten,
kategorie_ids,
entry.get("trennzeichen") or ",",
feldfilter,
),
datei_name=entry.get("datei_name"),
datei=base64.b64decode(roh_datei) if roh_datei else None,
))
+15
View File
@@ -1126,6 +1126,18 @@ class SpalteOut(BaseModel):
titel: str
class FeldFilter(BaseModel):
"""Eine Bedingung auf einem eigenen Feld.
Mehrere Bedingungen muessen alle zutreffen. Verglichen wird genau, aber
ohne Ruecksicht auf Gross-/Kleinschreibung. Ein leerer ``wert`` trifft die
Zeilen OHNE Wert in diesem Feld.
"""
feld_id: int
wert: str = ""
class EtikettenVorlageOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
@@ -1137,6 +1149,7 @@ class EtikettenVorlageOut(BaseModel):
spalten: list[str] = []
kategorie_ids: list[int] = []
trennzeichen: str = ","
feldfilter: list[FeldFilter] = []
# Nur der Name; die Binaerdaten gehen ueber einen eigenen Endpunkt.
datei_name: str | None = None
updated_at: datetime
@@ -1148,6 +1161,7 @@ class EtikettenVorlageCreate(BaseModel):
spalten: list[str] = []
kategorie_ids: list[int] = []
trennzeichen: str = ","
feldfilter: list[FeldFilter] = []
class EtikettenVorlageUpdate(BaseModel):
@@ -1158,6 +1172,7 @@ class EtikettenVorlageUpdate(BaseModel):
spalten: list[str] | None = None
kategorie_ids: list[int] | None = None
trennzeichen: str | None = None
feldfilter: list[FeldFilter] | None = None
position: int | None = None
+101 -10
View File
@@ -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()
+241 -2
View File
@@ -54,7 +54,7 @@ def kabel(db):
return cat, fd
def _vorlage(db, spalten, art="items", kategorie_ids=None, trenn=","):
def _vorlage(db, spalten, art="items", kategorie_ids=None, trenn=",", feldfilter=None):
v = EtikettenVorlage(
name=f"Test {len(db.query(EtikettenVorlage).all())}",
art=art,
@@ -62,6 +62,7 @@ def _vorlage(db, spalten, art="items", kategorie_ids=None, trenn=","):
"spalten": spalten,
"kategorie_ids": kategorie_ids or [],
"trennzeichen": trenn,
"feldfilter": feldfilter or [],
}),
)
db.add(v)
@@ -226,7 +227,12 @@ def test_kaputte_konfiguration_ergibt_vorgaben(db):
db.commit()
konf = konfiguration_lesen(v)
assert konf == {"spalten": [], "kategorie_ids": [], "trennzeichen": ","}
assert konf == {
"spalten": [],
"kategorie_ids": [],
"trennzeichen": ",",
"feldfilter": [],
}
def test_unerlaubtes_trennzeichen_faellt_auf_komma_zurueck(db):
@@ -399,3 +405,236 @@ def test_lbx_mit_schlichtem_namen_bleibt_unveraendert(db, admin):
assert 'filename="kabel.lbx"' in antwort.headers["content-disposition"]
_disposition_pruefen(antwort, "kabel.lbx")
# --------------------------------------------------------------------------
# Wertefilter: nur Zeilen mit bestimmten Feldwerten
# --------------------------------------------------------------------------
@pytest.fixture()
def haken(db):
"""Kategorie „Kleidung" mit einem Ja/Nein-Feld „Verliehen"."""
cat = Category(name="Kleidung", tracking=CategoryTracking.object.value)
db.add(cat)
db.flush()
fd = FieldDefinition(
category_id=cat.id, label="Verliehen", key="verliehen", field_type="boolean"
)
db.add(fd)
db.commit()
db.refresh(cat)
db.refresh(fd)
return cat, fd
def _artikel(db, cat, name, fd=None, wert=None):
"""Artikel, optional mit einem Feldwert. ``wert=None`` legt KEINE Zeile an."""
product = Product(name=name, category_id=cat.id, package_size=1)
db.add(product)
db.flush()
if fd is not None and wert is not None:
db.add(ProductFieldValue(
product_id=product.id, field_definition_id=fd.id, value=wert
))
db.commit()
db.refresh(product)
return product
def _namen(db, vorlage):
"""Die Namensspalte des erzeugten CSV, ohne Kopfzeile."""
return _zeilen(csv_erzeugen(db, vorlage))[1:]
def test_feldfilter_waehlt_nur_passende_artikel(db, haken):
cat, fd = haken
_artikel(db, cat, "Jacke", fd, "true")
_artikel(db, cat, "Hose", fd, "false")
v = _vorlage(db, ["name"], art="products",
feldfilter=[{"feld_id": fd.id, "wert": "ja"}])
assert _namen(db, v) == ["Jacke"]
def test_feldfilter_nein_trifft_auch_artikel_ohne_zeile(db, haken):
"""Der Kern: Haken wegnehmen loescht die Zeile.
„nein" und „nie gesetzt" sind in den Daten dasselbe. Ohne diese Gleichung
faende „alle mit Nein" nur die, die einmal angehakt und wieder abgehakt
wurden – und das ist fast niemand.
"""
cat, fd = haken
_artikel(db, cat, "Jacke", fd, "true")
_artikel(db, cat, "Hose", fd, "false")
_artikel(db, cat, "Schal") # nie angefasst, gar keine Zeile
v = _vorlage(db, ["name"], art="products",
feldfilter=[{"feld_id": fd.id, "wert": "nein"}])
assert sorted(_namen(db, v)) == ["Hose", "Schal"]
def test_leerer_wert_trifft_artikel_ohne_wert(db, kabel):
cat, fd = kabel
_artikel(db, cat, "Mit Laenge", fd, "5")
_artikel(db, cat, "Ohne Laenge")
v = _vorlage(db, ["name"], art="products",
feldfilter=[{"feld_id": fd.id, "wert": ""}])
assert _namen(db, v) == ["Ohne Laenge"]
def test_feldfilter_vergleicht_genau_nicht_teilweise(db, kabel):
cat, fd = kabel
_artikel(db, cat, "Kurz", fd, "3")
_artikel(db, cat, "Lang", fd, "30")
v = _vorlage(db, ["name"], art="products",
feldfilter=[{"feld_id": fd.id, "wert": "3"}])
assert _namen(db, v) == ["Kurz"]
def test_mehrere_bedingungen_muessen_alle_zutreffen(db, haken):
cat, fd_bool = haken
fd_text = FieldDefinition(
category_id=cat.id, label="Farbe", key="farbe", field_type="text"
)
db.add(fd_text)
db.commit()
db.refresh(fd_text)
blau = _artikel(db, cat, "Blaue Jacke", fd_bool, "true")
db.add(ProductFieldValue(
product_id=blau.id, field_definition_id=fd_text.id, value="blau"
))
rot = _artikel(db, cat, "Rote Jacke", fd_bool, "true")
db.add(ProductFieldValue(
product_id=rot.id, field_definition_id=fd_text.id, value="rot"
))
db.commit()
v = _vorlage(db, ["name"], art="products", feldfilter=[
{"feld_id": fd_bool.id, "wert": "ja"},
{"feld_id": fd_text.id, "wert": "blau"},
])
assert _namen(db, v) == ["Blaue Jacke"]
def test_feldfilter_und_kategorie_greifen_zusammen(db, haken):
cat, fd = haken
zweite = Category(name="Werkzeug", tracking=CategoryTracking.object.value)
db.add(zweite)
db.commit()
db.refresh(zweite)
_artikel(db, cat, "Jacke", fd, "true")
# Gleicher Feldwert, andere Kategorie – die Kategorie muss ihn aussperren.
fremder = _artikel(db, zweite, "Hammer")
db.add(ProductFieldValue(
product_id=fremder.id, field_definition_id=fd.id, value="true"
))
db.commit()
v = _vorlage(db, ["name"], art="products", kategorie_ids=[cat.id],
feldfilter=[{"feld_id": fd.id, "wert": "ja"}])
assert _namen(db, v) == ["Jacke"]
def test_feldfilter_wirkt_auch_bei_einzelstuecken(db, haken):
"""Der Wert haengt am Artikel – das Stueck erbt ihn."""
cat, fd = haken
verliehen = _artikel(db, cat, "Jacke", fd, "true")
daheim = _artikel(db, cat, "Hose", fd, "false")
db.add(Item(uid="UIDAAAAA", product_id=verliehen.id))
db.add(Item(uid="UIDBBBBB", product_id=daheim.id))
db.commit()
v = _vorlage(db, ["produkt"], art="items",
feldfilter=[{"feld_id": fd.id, "wert": "ja"}])
assert _namen(db, v) == ["Jacke"]
def test_geloeschtes_feld_im_filter_zerreisst_den_export_nicht(db, haken):
"""Wie bei einer Spalte zu einem geloeschten Feld: die Bedingung faellt weg.
Der Export wird dadurch WEITER, nicht leer – die harmlosere der beiden
Fehlrichtungen, und hier festgehalten, damit sie nicht versehentlich kippt.
"""
cat, fd = haken
_artikel(db, cat, "Jacke", fd, "true")
_artikel(db, cat, "Hose", fd, "false")
v = _vorlage(db, ["name"], art="products",
feldfilter=[{"feld_id": fd.id, "wert": "ja"}])
db.delete(fd)
db.commit()
assert sorted(_namen(db, v)) == ["Hose", "Jacke"]
def test_kaputter_feldfilter_ergibt_keinen_filter(db):
for nr, roh in enumerate(('{"feldfilter": "quatsch"}',
'{"feldfilter": [{"feld_id": "abc"}]}',
'{"feldfilter": [42]}')):
v = EtikettenVorlage(name=f"Kaputt {nr}", art="items", konfiguration=roh)
db.add(v)
db.commit()
assert konfiguration_lesen(v)["feldfilter"] == []
def test_feldfilter_uebersteht_die_sicherung(db, haken):
"""Feld-IDs sind zwischen zwei Instanzen nicht dieselben.
Deshalb steht in der Sicherung „Kategorie::Label" statt einer ID – und
zwar fuer Spalten UND Filter. Fehlte die Umschreibung beim Filter, zeigte
er nach dem Zurueckspielen auf ein fremdes Feld oder waere still weg, und
der Export gaebe mehr Zeilen aus als das Original.
"""
import json as _json
from app.routers.transfer import _import_json, export_backup_json
from app.seed import ensure_builtin_units
cat, fd = haken
person = User(username="chef", password_hash="x", role=Role.admin)
db.add(person)
_artikel(db, cat, "Jacke", fd, "true")
_artikel(db, cat, "Hose", fd, "false")
v = _vorlage(db, ["name"], art="products",
feldfilter=[{"feld_id": fd.id, "wert": "ja"}])
v.name = "Verliehenes"
db.commit()
inhalt = export_backup_json(db=db, _=person).body
gesichert = _json.loads(inhalt)["etiketten_vorlagen"][0]
# Nach Pfad und Label, nie nach ID.
assert gesichert["feldfilter"] == [{"feld": "Kleidung::Verliehen", "wert": "ja"}]
from app.database import Base
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
motor = create_engine(
"sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool
)
Base.metadata.create_all(bind=motor)
zweite = sessionmaker(bind=motor)()
ensure_builtin_units(zweite)
zweiter_nutzer = User(username="chef", password_hash="x", role=Role.admin)
zweite.add(zweiter_nutzer)
zweite.commit()
_import_json(zweite, inhalt, zweiter_nutzer, "add")
neu = zweite.query(EtikettenVorlage).filter(
EtikettenVorlage.name == "Verliehenes"
).one()
neues_feld = zweite.query(FieldDefinition).filter(
FieldDefinition.label == "Verliehen"
).one()
konf = konfiguration_lesen(neu)
# Auf die ID der ZWEITEN Instanz umgeschrieben, Wert unveraendert.
assert konf["feldfilter"] == [{"feld_id": neues_feld.id, "wert": "ja"}]
assert _namen(zweite, neu) == ["Jacke"]
zweite.close()