Einzelstuecke hatten bisher keinen Zustand - der Lagerort sagt, wohin ein
Stueck gehoert, aber nicht, woran es gerade ist. Neue Tabelle item_usages
nach dem Vorbild von item_loans: ein Zeitraum je Zustand, ended_at NULL
heisst "laeuft noch", und ein Teilindex laesst nur einen laufenden zu.
"Verfuegbar" wird bewusst nicht gespeichert - keine offene Zeile IST
verfuegbar, wie bei lots.opened_on und beim Verleih. Damit kostet der
Ruhezustand nichts und die Tabelle fuellt sich nicht mit Rauschen.
Abgeschlossene Zeilen bleiben stehen, sie sind der Verlauf.
Eigener Endpunkt (PUT /items/{id}/verwendung) und ausdruecklich nicht
PATCH /items/{id}: Das ist Admins vorbehalten, und die App schickt dort
immer alle Felder mitsamt null - jedes Speichern wuerde den Status
stillschweigend zuruecksetzen. Umstellen darf deshalb jeder angemeldete
Benutzer, wie Verleihen.
Der Zustand aendert den Bestand nicht und schreibt keine Bewegung: ein
getragenes Hemd ist weiterhin im Haushalt, anders als ein verliehenes.
Sichtbar nur bei Kategorien mit usage_tracking. Ein Haken an einer
Oberkategorie wandert in den Unterbaum, weil die Garderobe als
Kategoriebaum ja schon existiert. Die Markierung fahrt in beiden
Sicherungswegen mit; der Verlauf nicht, weil Einzelstuecke dort
ueberhaupt nicht vorkommen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
Die Suche verglich bisher einen einzigen Textschnipsel gegen den Namen. Wer
"Vogel Jack and Jones" eintippte, bekam nichts: Dieser Ausdruck steht in
keinem Artikelnamen, und die Marke wurde ueberhaupt nie durchsucht. Jetzt
wird der Suchtext in Woerter zerlegt, jedes muss in Name oder Marke
vorkommen, die Reihenfolge ist egal.
Gefiltert wird in Python und nicht per ILIKE, weil normalisieren() Umlaute
aufloest ("Muehle" findet "Mühle") - auf der Spalte faende ein ILIKE den
gespeicherten Wert nicht wieder. Dieselbe Form nutzt recipes.zuordnung_ziele
schon laenger. Alle Artikellisten haengen an dieser einen Route, die
Verbesserung wirkt also auch bei Gegenstaenden und beim Ein-/Auslagern.
Die Artikelzeile in der App zeigt nun die Marke, sonst waere ein Treffer
darauf nicht zu erklaeren.
Dazu die zweite Haelfte: delete_product hatte keinerlei Schutz und riss
Chargen, Einzelstuecke, Barcodes, Bilder und den ganzen Bewegungsverlauf per
Kaskade mit. Jetzt lehnt der Server mit 409 ab, solange Einzelstuecke oder
Chargen dranhaengen. Bewegungen allein halten bewusst nicht auf - sonst waere
jeder je benutzte Artikel fuer immer unloeschbar. Der Bestaetigungstext im
Web versprach den Verlust des Bestands und stimmte damit nicht mehr.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
macOS legt Dateinamen zerlegt ab - das "ae" in "Lagerplaetze.lbx" ist dort
nicht ein Zeichen, sondern "a" plus ein kombinierender Trema-Akzent. Genau
diese Form landet beim Hochladen in der Datenbank. Starlette schreibt
Header aber als latin-1, und fuer den Akzent gibt es dort nichts - die
Route starb mit UnicodeEncodeError und antwortete 500, obwohl der Name am
Bildschirm voellig harmlos aussah.
Der Header wird jetzt nach RFC 6266 zweigleisig gebaut: filename= traegt
eine ASCII-Fassung als Rueckfall, filename*= den echten Namen
prozentkodiert. Browser bevorzugen filename* und zeigen den Umlaut richtig.
Derselbe Riegel liegt auf dem CSV-Download, wo der frei getippte
Vorlagenname in den Header geht - gleicher Defekt, nur noch nicht
aufgefallen.
Fuer die Download-Route gab es bisher keinen Test; zwei kommen dazu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neu web/src/urlState.js mit useUrlWert/useUrlZahl/useUrlWerte. Zwei
Regeln stecken darin, beide damit die Adresszeile brauchbar bleibt:
geschrieben wird immer mit replace - sonst legt jeder Tastendruck in
einem Suchfeld einen Verlaufseintrag an und die Zurueck-Taste braeuchte
zwanzig Versuche, um die Seite zu verlassen -, und Vorgabewerte fliegen
aus der URL, sonst sammelt sich "?typ=&q=&kat=" an.
useUrlWerte gibt es, weil zwei getrennte Schreibvorgaenge im selben
Durchlauf einander ueberschreiben: Beide bekommen denselben
Ausgangsstand. Wo ein Klick zwei Dinge aendert ("Kategorie 12, Karte
Masse"), muss es ein Aufruf sein.
Damit wandern in die URL:
- Rezepte: Das Rezept steht jetzt im PFAD (/rezepte/<slug>), weil es
benennt, was man ansieht - so wie /products/:id und /i/:uid. Portionen
und Suchbegriff als Parameter. "Pfannkuchen fuer 12" ist damit ein
verschickbarer Link.
- Kategorien: Segment-Umschalter und die geoeffnete Seitenkarte
(?typ=food&kat=6&panel=masse). Gespeichert wird die Kennung, nicht das
Objekt - die Kategorien kommen erst vom Server.
- Gruppen: die geoeffnete Seitenkarte.
- Artikel: der Segment-Umschalter.
- Ein- und Auslagern: der gewaehlte Artikel als Kennung, nachgeladen beim
Aufruf. Eine halbfertige Buchung ueberlebt damit ein Neuladen.
Die Portionszahl landet NUR in der URL, wenn jemand sie selbst gestellt
hat. Die Vorbelegung aus dem Rezept bleibt draussen - sonst traegt jedes
geoeffnete Rezept ein ?portionen mit sich herum, das gar keine
Entscheidung ist.
Dazu im Backend: Ein unbekanntes Rezept ist jetzt 404 statt 502 und sagt
"Dieses Rezept gibt es in Mealie nicht (mehr)" statt "Mealie kennt
/api/recipes/x nicht". Das wurde noetig, weil ein Rezept seit diesem
Commit verlinkbar ist und ein alter Link irgendwann ins Leere zeigt -
eine gewoehnliche Auskunft, kein Gateway-Fehler.
Von Hand durchgeklickt: Rezept oeffnen und Portionen auf 12 ->
/rezepte/pfannkuchen?portionen=12, Neuladen bringt 250 g -> 750 g zurueck;
unbekannter Slug zeigt den klaren Hinweis und laesst die Liste stehen;
Kategorie-Karte und Segment ueberstehen F5, Schliessen raeumt die URL;
/checkin?artikel=1 laedt den Artikel nach, ?artikel=999 raeumt die
Adresse und zeigt die Suchmaske. 1 neuer Test, Suite bei 518.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Knopf im Rezept schrieb die fehlenden Zutaten weg, aber auf der
Uebersicht blieb davon nichts zu sehen: Die Karte "Einkaufsliste" holte
die drei GERECHNETEN Quellen einzeln und wusste von den gespeicherten
Eintraegen nichts. Wer die Uebersicht als Zettel benutzt, hat sie
schlicht nicht gesehen.
Die Karte holt jetzt alles mit einem Aufruf und zeigt oben den
Abschnitt "Selbst draufgesetzt" - eigene Eintraege und die aus einem
Rezept uebernommenen, mit Herkunft und Menge. Abgehakte verschwinden
dort, die Karte ist zum Einkaufen da und nicht zum Nachlesen.
Das Abhaken war in der Karte reiner Sitzungszustand und damit nach dem
Neuladen weg. Es benutzt jetzt dieselben Schluessel und dieselben Routen
wie die volle Seite: Der Haken wandert sofort, speichert im
Hintergrund, und springt zurueck, wenn das schiefgeht.
Dazu die Kennzahl "Nachzukaufen": Sie zaehlte nur die gerechneten
Zeilen und meldete "alles erreicht", waehrend auf der Liste fuenf Posten
standen. Offene eigene Eintraege zaehlen jetzt mit, abgehakte nicht -
die sind gekauft.
Von Hand geprueft: Zwei Eintraege angelegt, Kennzahl springt auf 2, die
Karte zeigt beide samt "2 Packungen", und ein Haken aus der Karte steht
anschliessend in der Datenbank. 1 neuer Test, Suite bei 517.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Endpunkte standen seit dem Backend-Commit, aber eintragen liess sich
nichts - die Mengen gingen nur per API rein. Damit war "2 EL Mehl" im
Verbrauchen-Dialog zwar vorgesehen, aber fuer niemanden erreichbar.
Zwei Stellen dafuer:
Stammdaten > Masse verwaltet nur die NAMEN ("Essloeffel", "Tasse"). Die
Seite ist absichtlich kurz, denn die Mengen gehoeren nicht an das Mass:
Ein Essloeffel Mehl sind etwa 10 g, einer Zucker etwa 12 g, und eine
Tasse Milch ist Milliliter. Ein globaler Wert waere bei jedem zweiten
Artikel falsch.
Kategorien > "Masse & Frist" ist deshalb der Ort, an dem die Arbeit
passiert. Beides steckt in einem Bedienfeld, weil es dieselbe Mechanik
ist: Der Wert gilt fuer die Kategorie und alles darunter, und der
naechste Vorfahr mit einem Eintrag gewinnt. Leere Felder erben - und
zeigen als Platzhalter, WAS sie erben ("erbt 5 Tage (Kategorie: Mehl &
Zucker)", "10 g" mit Abzeichen "geerbt"). Ohne das muesste man den
Kategorienbaum im Kopf verfolgen, um eine leere Eingabe zu verstehen.
Dafuer zwei kleine Ergaenzungen am Backend: CategoryOut nennt die
geerbte Frist samt Herkunft, und /categories/{id}/measures nimmt
?vererbt=true, um neben den eigenen auch die von oben geerbten Mengen zu
liefern. Die Bearbeitungsansicht ohne Flagge bleibt wie sie war.
Von Hand durchgeklickt - und dabei einen echten Fehler gefunden: Der
API-Aufruf im Browser hat das vererbt-Flag verschluckt, weil die
Funktion nur ein Argument annahm. Die Unterkategorie zeigte dadurch
leere Felder, obwohl der Server die geerbten Werte lieferte. Jetzt
stehen dort "10 g" und "3 g" mit der Quelle im Tooltip.
4 neue Tests, Suite bei 516.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Einkaufsliste war vollstaendig GERECHNET: Artikel unter ihrem
Mindestbestand, Gruppen unter ihrem, Bedarfe je Lagerort. Das bleibt, es
ist die Staerke der Liste. Was fehlte, war ein Ort fuer "Milch gehoert
drauf, weil ich das sage" - und damit auch fuer "diese Zutaten fehlen mir
fuer das Rezept". Der Knopf konnte nirgends hinschreiben.
Zwei Tabellen dafuer, und die Trennung ist der Kern:
shopping_entries haelt, was jemand bewusst draufgesetzt hat. Genau eines
von Artikel, Gruppe oder Freitext - der Freitext ist unverzichtbar, denn
eine Rezeptzutat ohne Artikel ("Ras el Hanout") kommt nur so auf den
Zettel, und gerade die unbekannte Zutat ist die, die man vergisst.
shopping_checks haelt nur den HAKEN an einem gerechneten Posten, nicht
den Posten. Die gerechneten Vorschlaege duerfen keine Zeile bekommen: Sie
aendern sich mit jedem Einlagern, eine materialisierte Kopie waere sofort
falsch. Abhaken ging deshalb bisher nur im Browser und war nach dem
Neuladen weg - mitten im Supermarkt.
Aus einem Rezept uebernommen wird die FEHLMENGE, nicht die Rezeptmenge:
Wer 250 g braucht und 100 g hat, kauft 150 g. Gerechnet wird frisch mit
der Portionszahl, die auf dem Schirm steht. Je Rezept und Ziel gibt es
hoechstens einen offenen Eintrag - ein zweiter Druck nach dem Hochdrehen
der Portionen rechnet die Menge neu, statt die Liste zu verdoppeln. Ueber
verschiedene Rezepte hinweg bleiben es getrennte Zeilen, denn derselbe
Artikel kann zweimal fehlen. Ein abgehakter Eintrag blockiert nichts -
abgehakt heisst gekauft.
Mengen formatiert dieselbe Funktion wie beim Rezeptabgleich, damit die
Liste eine Sprache spricht: "2 Packungen" statt "1500 ml", aufgerundet,
weil man keine halbe Packung kauft.
In der Oberfläche eine eigene Karte "Selbst draufgesetzt" mit Eingabefeld,
Herkunftsangabe ("fuer Pfannkuchen") und "Erledigte entfernen", das beide
Quellen zusammen aufraeumt - fuer den Nutzer ist es eine Liste. Das
Abhaken wandert sofort und speichert im Hintergrund; schlaegt es fehl,
springt der Haken zurueck. Die Seite holt jetzt alles mit einem Aufruf
statt mit drei.
Von Hand geprueft gegen eine Mealie-Attrappe: Pfannkuchen fuer 48
Portionen ergibt 2 Packungen Mehl, 2 Packungen Milch, 3 Packungen Eier
und "Ras el Hanout" als Notiz; derselbe Knopf bei 96 Portionen
aktualisiert diese vier statt acht daraus zu machen; ein Haken
ueberlebt das Neuladen. 17 neue Tests, Suite bei 512.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Backend konnte das seit dem letzten Commit, aber ohne Oberflaeche kam
niemand dran. Jetzt traegt jede Charge zwei Knoepfe:
"Anbrechen" spaltet eine Packung ab, gibt ihr ein "zu verbrauchen bis" und
laesst den Rest ungeoeffnet mit seinem eigenen MHD - mit Rueckfrage, weil
sich die abgespaltene Packung danach anders verhaelt.
"Verbrauchen" nimmt die VERBRAUCHTE Menge ("100 ml genommen", "2 von 6
Broetchen"), nicht den Rest - das ist die Zahl, die man kennt. War die
Charge noch zu, wird sie dabei angebrochen; der Haken dafuer ist
abwaehlbar, falls man nur eine Buchung nachtraegt. Die Einheitenauswahl
holt die Hausmasse der Kategorie dazu, deshalb laesst sich "2 EL" buchen
und landet bei Mehl als 20 g im Bestand.
Die Liste zeigt den Zustand jetzt auch: Spalte "Zustand" (angebrochen),
Spalte "Verbrauchen bis", und bei einer offenen Packung die Restmenge in
Basiseinheiten statt im Gebinde - "980 g von 1.000 g" sagt etwas,
"0,98 Packungen" nicht.
Gewarnt wird nach dem FRUEHEREN von MHD und Verbrauchsdatum (units.js::
faelligAm, Spiegelbild von Lot.faellig_am): Eine angebrochene Milch ist
nicht mehr bis zum aufgedruckten Datum gut. Dafuer ein eigenes Farb-Token
--open in ruhigem Blaugrau - angebrochen ist ein Zustand, keine Warnung.
Dazu ein Fehler, der die ganze Anzeige wertlos gemacht haette: In
list_all_lots fehlten opened_on, consume_by und opened_quantity. Die
Liste haette eine offene Packung wie jede andere Charge gezeigt, samt
falscher Ablaufwarnung. Test dagegen.
Von Hand durchgeklickt: 4x1l Milch anbrechen ergibt 1 l offen mit
Verbrauchsdatum 5.10. und 3 l zu mit MHD 15.11.; "2 EL" aus dem Mehl
ergibt 980 g offen und 1 Packung zu. Suite bei 495.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gefunden bei der Probe, ob ein Embedding-Modell die Zuordnung besser
hinbekommt: Die Sperrliste der Eigenschaftswoerter lief als eine einzige
Liste mit Praefix-Vergleich. "Frischkaese" fing mit "frisch" an, galt
damit als blosse Eigenschaft und fiel aus dem Vergleich - die Zutat
"Kaese" fand den Frischkaese deshalb NIE. Ebenso "Biomilch" (bio),
"Rohrzucker" (roh), "Kleingebaeck" (klein), "Ganzkornbrot" (ganz),
"Grobgriess" (grob).
Unsichtbar, weil kein falscher Treffer entstand, sondern ein richtiger
ausblieb - die Zutat stand einfach als "nicht zugeordnet" da.
Jetzt zwei Toepfe statt einem. Stammformen wie "getrock" oder "vegan"
vergleichen weiter ueber den Wortanfang, weil Deutsch beugt und diese
Stuecke keine Lebensmittelnamen anfangen. Kurze Alltagswoerter wie
"frisch", "bio", "roh", "klein" zaehlen nur noch als Eigenschaft, wenn
hoechstens eine Beugungsendung dranhaengt: "frische" ja, "frischkase"
nein.
Gegenprobe im Test: "frische Hefe" findet die "Frischen Kraeuter"
weiterhin nicht - die Sperre ist praeziser geworden, nicht schwaecher.
7 neue Tests, Suite bei 494.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der unscharfe Abgleich muss raten und wird das immer tun: "Lime Curd" hat
mit "Zitronencreme" kein Wort gemeinsam, und manchmal liegt er daneben.
Bisher war das eine Sackgasse - die Zutat blieb unzugeordnet, bei jedem
Aufruf neu.
Jetzt traegt jede Zutatenzeile einen Knopf "Zuordnen". Entschieden wird
gegen Artikel UND Gruppen, gemerkt unter dem normalisierten Namen des
Lebensmittels: "Schlagsahne", "schlagsahne" und "SCHLAGSAHNE " treffen
dieselbe Zeile. Ab dann gilt die Entscheidung fuer jedes Rezept, in dem
diese Zutat vorkommt - eine Zutat wird hoechstens einmal zugeordnet.
Schluessel ist der Name, nicht die Mealie-ID: IDs sind UUIDs, die beim
Neuanlegen eines Lebensmittels wechseln, und nur ueber den Namen greift
die Zuordnung auch fuer Zutaten ohne verknuepftes Lebensmittel ("2 Eier").
Im Auswahldialog stehen oben die Vorschlaege, die der Abgleich selbst
gefunden hat - meist ist der Richtige darunter, nur nicht an erster
Stelle -, darunter die freie Suche ueber alles. Die Basiseinheit steht
dabei, damit nicht wieder Milliliter auf ein Gramm-Produkt wandern.
Beim Raten bleiben Gegenstaende aussen vor; von Hand darf alles gewaehlt
werden ausser Einzelstuecken. Backpapier ist kein Lebensmittel und steht
trotzdem in Rezepten, waehrend ein Einzelstueck nie eine Zutat ist.
Zeigt das Ziel ins Leere, weil der Artikel geloescht wurde, faellt die
Zeile beim Laden still heraus: Auf SQLite greift kein Fremdschluessel,
und ein geloeschter Artikel darf kein Rezept mehr sprengen.
Die Zuordnungs-Routen stehen bewusst vor /recipes/{slug}, sonst liest
FastAPI "zuordnungen" als Rezept-Slug und fragt Mealie danach. Ein Test
wacht ueber die Reihenfolge, weil ein Umsortieren der Datei den Fehler
sonst still einbauen wuerde.
16 neue Tests, Suite bei 487.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"200 ml Schlagsahne" landete auf "Kraeuter Sahne Haehnchen" - beide teilen
das Wort "Sahne", der Rest ist Zufall. Die Einheit haette es verraten: Das
Haehnchen wird in Gramm gefuehrt, das Rezept sagt Milliliter. Wo sich die
Rezept-Einheit gar nicht umrechnen laesst, wird deshalb nicht mehr
zugeordnet, sondern offen gelassen - lieber "nichts Passendes gefunden"
samt Begruendung als eine falsche Behauptung.
Entscheidend ist, WARUM die Umrechnung scheitert, und nur einer der Gruende
sagt etwas ueber den Artikel:
- Bekannte Einheit falscher Art ohne Zweiteinheit -> spricht gegen den
Artikel. Milliliter an einem Gramm-Produkt ist ein anderes Produkt.
- Unbekanntes Wort oder nicht hinterlegtes Hausmass ("1 Tasse Linsen") ->
sagt gar nichts. Die Linsen koennen voellig richtig sein, es fehlt bloss
die Massangabe, und die laesst sich nachtragen.
- Gar keine Einheit ("3 Eier") -> passt immer.
Die erste Fassung hat das nicht unterschieden und damit "1 Tasse Linsen"
zu "nicht zugeordnet" gemacht, obwohl der Artikel stimmte - die beiden
bestehenden Tests haben das sofort gezeigt.
Bei mehreren Kandidaten gewinnt jetzt der mit passender Einheit, auch gegen
eine hoehere Namensaehnlichkeit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Wortanker nahm jedes geteilte Wort ab vier Zeichen - also auch
"vegan". Damit passte jedes vegane Produkt auf jede vegane Zutat: "vegane
Butter" landete auf "veganes Muehlen Knusper Bleu", "veganer griechischer
Joghurt" auf "Vegane Muehlen Nuggets" und "veganer Lime Curd" auf "Vegane
Muehlen-Schnitzel auf Basis von Soja". Geteilt wurde dort nur die
Eigenschaft, nicht die Sache.
Beschreibende Woerter begruenden deshalb keinen Treffer mehr - vegan,
vegetarisch, bio, frisch, geschmolzen, ungesalzen, geschaelt, geschnitten,
Zimmertemperatur, oder, optional und so weiter, dazu reine Zahlen. Als
Wortanfaenge aufgefuehrt, weil Deutsch beugt ("vegan", "vegane",
"veganer", "veganes"). Sie sperren aber nicht: "vegane Butter" findet
weiterhin eine "Vegane Butter", nur eben ueber das Wort "Butter".
Wichtig war, die Liste auf BEIDEN Seiten anzuwenden. Sonst steckt "vegan"
weiter im Produktnamen und der Teilstring-Vergleich greift von dort aus.
Gegenprobe mit dem echten Katalog: Von zehn Zutaten bleiben die drei
richtigen Treffer (Mehl, Vollmilch, Aepfel), die sieben erfundenen sind
weg.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Zuordnung war unbrauchbar: "Mehl Typ 405" landete auf "Muehlen
Geschnetzeltes", "Eier" auf "Reis", "Zucker" auf "Joystick fuer WII",
"Apfel" auf "Snack Collection Platten 4 (Waffeln)" und "Salz" auf "Shirt
Casual Fit". Drei Fehler steckten darin.
1. Gegenstaende standen ueberhaupt zur Auswahl. Der Kassenzettel-Abgleich
filtert sie seit immer heraus, mein Rezeptabgleich nicht - also konnten
ein Joystick, ein Shirt und eine Waffelplatte als Zutat gelten. Jetzt
zaehlen nur Lebensmittel, und Einzelstuecke bleiben ebenfalls draussen.
2. Entschieden hat eine Buchstaben-Aehnlichkeit. Die haelt "Eier" und
"Reis" fuer verwandt, weil sich Zeichen ueberlappen. Neu muss ein ganzes
Wort geteilt sein: "Mehl" in "Weissmehl Typ 550" ja, "Mehl" in "Muehlen
Geschnetzeltes" nein. Umlaute und Mehrzahl werden dabei zusammengezogen,
damit "Apfel" auch "Aepfel" findet.
3. Gesucht wurde im ganzen Katalog. Jetzt zuerst dort, wo es hingehoert:
Teilt der Zutatenname ein Wort mit einer Kategorie, gilt nur deren
Teilbaum - "Mehl Typ 405" sucht in "Mehl & Zucker", nicht im Fleisch.
Erst wenn darin nichts passt, zaehlt der Rest.
Die Punktzahl ist damit keine Sperre mehr, sondern nur noch die
Reihenfolge. Als Sperre war sie untauglich in beide Richtungen: "Vollmilch
Zimmertemperatur" gegen "H Vollmilch" kommt auf etwa 40 Punkte, weil das
lange Beiwort den Durchschnitt zieht - offensichtlich dasselbe Produkt.
"Mehl" gegen "Muehlen Geschnetzeltes" kam dagegen ueber 60.
Folge, die so gewollt ist: Wo nichts passt, steht jetzt "nicht zugeordnet"
und gar kein Vorschlag mehr. Ein Vorschlag, den niemand nachvollziehen
kann, ist schlimmer als keiner - er verleitet zum Durchklicken. Die
dauerhafte Loesung fuer echte Luecken ist die gemerkte Zuordnung
("Mealie-Lebensmittel X ist bei uns Y"), die noch aussteht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neue Seite "Rezepte" (nur sichtbar, wenn Mealie eingerichtet ist): links
die Rezeptliste mit Suche, rechts das gewaehlte Rezept mit allen Zutaten
und der einen Frage, die zaehlt - kann ich das heute kochen, oder muss ich
vorher einkaufen? Je Zutat steht der Zustand: vorhanden, zu wenig, fehlt,
Menge unklar oder nicht zugeordnet; Fehlendes ist rot.
Die Portionenzahl laesst sich hochdrehen. Vorrania rechnet die Mengen dann
hoch, bevor es sie mit dem Bestand vergleicht - ein Rezept fuer 4, das man
fuer 8 kocht, braucht die doppelte Menge, und genau danach soll der
Abgleich gehen. Nennt das Rezept keine Portionszahl, wird nicht skaliert
und die Seite sagt das auch.
Drei Dinge entscheidet der Abgleich nacheinander (services/rezepte.py,
bewusst ohne HTTP, damit sich alles mit erfundenen Zutaten pruefen laesst):
1. Welcher Artikel ist gemeint? Unscharf ueber den Namen, gegen Artikel
UND Gruppen - "Milch" ist markenuebergreifend gemeint, und genau dafuer
gibt es Gruppen. Die Schwelle liegt hoeher als beim Kassenzettel: Dort
sieht ein Mensch die Vorschlaege durch, hier darf nicht stillschweigend
das Falsche als vorhanden gelten. Unter der Schwelle bleibt es bei
"nicht zugeordnet" samt Kandidatenliste.
2. Wie viel ist das in Gramm? Ueber die Einheiten und die Hausmasse der
Kategorie - "2 EL Zucker" wird zu 24 g. Fehlt das Mass fuer diese
Kategorie, meldet die Zutat genau das zurueck (Massname und
Kategorie-ID), damit daraus ein "Mass jetzt anlegen" werden kann.
3. Reicht der Bestand? Bei Gruppen ueber dieselbe gewichtete Summe wie in
der Einkaufsliste, damit Untergruppen und Zweiteinheiten gleich zaehlen.
Die Fuzzy-Heuristik lag in routers/products.py und ist nach
services/matching.py gezogen: Der Rezeptabgleich stellt dieselbe Frage, und
zwei Kopien waeren mit der Zeit auseinandergelaufen. Der Kassenzettel-Scan
nutzt sie unveraendert weiter, test_receipt_match.py bleibt gruen.
Dazu zwei Dinge aus deiner Rueckmeldung zur Einstellungs-Karte:
- Der verbundene Zustand bleibt jetzt stehen, statt nur als Meldung
aufzublitzen: Adresse, Version und "Dauertoken hinterlegt" samt den
Knoepfen Pruefen / Zugang aendern / Trennen. Das Formular erscheint nur,
solange nichts eingerichtet ist - ein Dauerzustand gehoert nicht in ein
Eingabefeld, sonst sieht man nie, ob es geklappt hat.
- Leere Felder melden sich auf Deutsch ("Bitte das Passwort eingeben.")
statt mit der rohen Schema-Meldung "String should have at least 1
character", die niemandem sagt, welches Feld gemeint ist.
Der Knopf "Fehlende zur Einkaufsliste" fehlt noch: Vorranias Einkaufsliste
wird heute vollstaendig aus Mindestbestaenden gerechnet und hat keinen Ort
fuer einen eigenen Eintrag. Das ist der naechste Schritt; die Seite sagt es
auch so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unter Einstellungen gibt es jetzt eine Karte "Mealie (Rezepte)": Adresse,
Benutzername, Passwort. Vorrania meldet sich damit EINMAL an
(POST /api/auth/token), laesst sich von Mealie ueber
POST /api/users/api-tokens ein Dauertoken ausstellen und verwirft das
Passwort sofort - es steht in keiner Zeile der Datenbank, in keiner
Antwort und in keinem Fehlertext. Wer sein Passwort gar nicht erst
eintippen will, haekelt "Ich gebe lieber selbst ein Token ein" an.
Das Token liegt NICHT in den Einstellungen: GET /settings liefert jedem
angemeldeten Nutzer alle Zeilen im Klartext. Eine Ausschlussliste dort
waere beim naechsten Geheimnis vergessen, deshalb eine eigene Tabelle ohne
jeden Leseweg - setzen, loeschen, und die Auskunft "ist gesetzt". Was
keinen Ausgang hat, kann nicht durchrutschen. Ein Test prueft genau das.
Der Dienst folgt services/oidc.py, nicht off.py: Bei Open Food Facts ist
"kein Treffer" der Normalfall und wird still zu None, hier ist "Mealie
antwortet nicht" ein Fehler, den der Nutzer sehen muss. Die Kennungen
trennen auch, was verschieden ist - ein falsches Passwort (400) ist etwas
anderes als ein weggetretener Server (502).
Mealie 1.x legt die Haushalts-Routen unter /api/groups/, 2.x unter
/api/households/. Erkannt wird das zur Laufzeit mit einer Anfrage und
dann eine Stunde gepuffert; ueber die Versionsnummer waere es unzuverlaessig,
die melden Installationen uneinheitlich.
Rezeptliste und -abgleich kommen als Naechstes; dies ist der Teil, mit dem
sich der Server ueberhaupt erst verbinden laesst.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Mehrfachauswahl gab es schon, sie konnte aber nur den Lagerort setzen.
Jetzt kommen drei Aktionen dazu: komplett ausbuchen, von jeder Charge
dieselbe Teilmenge abbuchen, und loeschen als Korrektur.
Anders als bulk-location kein Sammel-UPDATE: Je Charge muss eine Bewegung
entstehen, sonst fehlte der Vorgang im Verlauf und in der
Verbrauchsstatistik. Schlaegt eine Charge fehl, weil sie zu wenig enthaelt,
wird alles zurueckgerollt - ein halb gebuchter Stand waere schlimmer als
gar keiner, dieselbe Haltung wie beim Stapel-Einlagern.
Die gemeinsame Teilmenge gibt es nur, wenn alle ausgewaehlten Chargen zum
selben Artikel gehoeren. "1 Packung" bedeutet bei Milch und Broetchen
Verschiedenes; der Server lehnt es ab, und die Oberflaeche sperrt das Feld
gleich mit einem Hinweis, statt den Fehler erst nach dem Klick zu zeigen.
Loeschen schreibt wie das einzelne Loeschen eine Korrektur-Bewegung
(adjust, negative Menge) statt einer Entnahme - es ist eine Fehleingabe,
kein Verbrauch, und darf die Verbrauchsstatistik nicht verfaelschen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ich hatte das nach dem frueheren wirksamen Datum sortiert, sodass eine
ungeoeffnete Packung mit knappem MHD vor einer offenen drankam. Das ist
falsch herum: Das MHD einer geschlossenen Packung ist konservativ und wird
real meist ueberlebt, eine offene verdirbt dagegen tatsaechlich. Und wer
die zweite aufmacht, waehrend die erste offen steht, hat am Ende zwei
angebrochene.
Angebrochenes kommt also immer zuerst; innerhalb beider Gruppen gilt
weiter FEFO ueber das naehere von MHD und Verbrauchsdatum.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eine Teilmenge auszulagern ging rechnerisch schon, aber es blieb keine Spur
davon: Eine Charge mit 900 von urspruenglich 1000 ml war von einer frisch
eingelagerten 900-ml-Charge nicht zu unterscheiden, und eine geoeffnete
Milch galt weiter bis zum aufgedruckten MHD.
Anbrechen spaltet jetzt eine Packung ab. Aus 4 x 1 l wird eine offene
Charge mit 1 l und eine ungeoeffnete mit 3 l - sonst truegen die
restlichen 3 l das kurze Verbrauchsdatum mit, obwohl sie noch zu sind. Der
Gesamtbestand aendert sich dabei nicht, es ist eine Umbuchung wie das
Aufteilen und schreibt wie dieses keine Bewegung.
Verbrauchen geht in einem Schritt: Wer "100 ml verbraucht" eintraegt,
bricht die Charge dabei an (abschaltbar ueber auto_open). Gerechnet wird
vor dem Anbrechen - scheitert die Einheit, soll keine Packung grundlos
offen stehen.
MHD und "zu verbrauchen bis" stehen nebeneinander und bleiben beide
sichtbar; gewarnt wird nach dem FRUEHEREN (Lot.faellig_am). Dafuer musste
in /expiring die Vorbedingung "best_before IS NOT NULL" weg, sonst fehlte
ausgerechnet die angebrochene Packung ohne aufgedrucktes MHD. ExpiringItem
traegt beide Daten plus warn_date und warn_source, und best_before ist dort
jetzt optional.
Die Frist kommt aus drei Ebenen: Artikel schlaegt Kategorie, Kategorie
schlaegt ueber ihre Oberkategorien hinweg den globalen Wert. Der Helfer
dafuer (services/vererbung.py) liest die Kategoriekette von
services/fields.py rueckwaerts - dort werden Felder ALLER Vorfahren
gesammelt, hier gewinnt der naechste. Dieselbe Mechanik traegt die
Hausmasse.
Hausmasse sind der zweite Teil: "2 EL Mehl" liess sich bisher nicht
buchen, weil eine globale Einheit "Essloeffel" bei Mehl und Zucker
zwangslaeufig gleich schwer waere. Der Name ist deshalb global, die Menge
haengt an Kategorie und Artikel. to_base loest sie nur mit hausmasse=True
auf - beim Auslagern und Verbrauchen ja, beim Einlagern und bei
Mindestbestaenden ausdruecklich nicht, denn ein Loeffel ist ein
Schaetzwert. Verwaltete Einheiten werden zuerst geprueft, damit ein Mass
namens "Liter" die echte Einheit nicht verdeckt.
Zwei Stellen waeren sonst still kaputtgegangen:
- Die Konsolidierung haette die offene Packung beim naechsten Einlagern am
selben Ort einkassiert und ihr Verbrauchsdatum verschluckt. Angebrochene
Chargen werden deshalb nie zusammengefasst - in consolidate_lot_group
und im Start-Aufraeumer, beide bisher ohne jeden Test. Das Netz dafuer
ist jetzt da (test_konsolidierung.py).
- FEFO sortiert nach dem wirksamen Datum statt nur nach dem MHD. Eine
offene Packung kommt damit meist von selbst zuerst, aber eine
ungeoeffnete, die morgen ablaeuft, geht vor - Verderb schlaegt
Bequemlichkeit. Erst bei gleichem Datum wird das Angebrochene vorgezogen.
Dashboard bekommt "angebrochen" als fuenften Zustand, mit Vorrang
abgelaufen > bald > angebrochen > in Ordnung: Was offen ist und morgen
ablaeuft, gehoert in die Warnung, nicht in den ruhigeren Zustand.
Oberflaechen in Web und App folgen in eigenen Commits.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Anmeldung galt fest 7 Tage - und die Frist lief ab dem *Anmelden*, nicht
ab der letzten Nutzung. Wer die App taeglich benutzte, flog trotzdem
punktgenau nach einer Woche hinaus.
Beides ist jetzt geregelt:
- Einstellungen -> Anmeldung: 7 Tage bis 1 Jahr oder unbegrenzt. Gilt fuer
Website und App gemeinsam, weil beide dasselbe Token bekommen - auch nach
Single Sign-on.
- POST /auth/refresh schiebt die Frist beim Start nach vorn. Damit zaehlt die
eingestellte Dauer ab dem letzten Besuch, und eine geaenderte Einstellung
greift ohne Neuanmeldung.
"Unbegrenzt" heisst ein Token ganz ohne exp. Das ist bewusst folgenreich: Es
gibt keine Sperrliste, eine erteilte Anmeldung laesst sich nicht einzeln
zurueckziehen. Die Oberflaeche sagt das dazu und nennt den einzigen Ausweg
(JWT_SECRET aendern).
/auth/refresh nimmt bewusst nur JWTs an, keine langlebigen API-Token: aus
einem jederzeit widerrufbaren Zugang (Home Assistant) waere sonst ein
unwiderruflicher geworden.
Der Rueckfallwert aus der Umgebung steigt von 7 auf 30 Tage - er gilt nur noch,
solange nichts eingestellt ist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Etikettenexport konnte genau eine Sache, und zwar unveraenderlich: sechs
feste Spalten ueber Einzelstuecke. Was fehlte, war genau das, was ein Etikett
brauchbar macht - die eigenen Felder. Eine Kabellaenge oder die Kapazitaet einer
Powerbank steht laengst am Artikel, kam aber in keiner CSV vor. Und jede
Zusammenstellung war beim naechsten Mal wieder weg: Kategorien im React-State,
Trennzeichen im localStorage, Spalten hartkodiert.
Jetzt gibt es benannte Vorlagen mit eigener Spaltenwahl und Reihenfolge, fuer
Einzelstuecke, Artikel und Lagerorte. Eine Tabelle nach dem Vorbild von
DashboardLayout: benannt, Nutzlast als JSON-Text. Ohne user_id - eine Vorlage
gehoert allen, nicht einem Konto.
Spaltenschluessel fuer eigene Felder ist die ID, nicht der key: FieldDefinition.key
ist nur JE KATEGORIE eindeutig, zwei Kategorien duerfen beide "kapazitaet" fuehren.
Die CSV entsteht jetzt im Server statt im Browser. Mit waehlbaren Spalten,
eigenen Feldern und drei Bezugsarten gehoert das an eine Stelle - und zwar
dorthin, wo es sich pruefen laesst. Formel-Injektionsschutz, BOM und CRLF sind
aus der Browser-Fassung mitgenommen. Der Grund, aus dem der QR-Inhalt bisher im
Browser entstand ("nur die Oberflaeche kennt die oeffentliche Adresse"), gilt
allein fuer Lagerorte; dafuer reicht sie ihre Herkunft als Parameter durch.
Die beiden alten Abschnitte weichen den Vorlagen. Beim ersten Start entstehen
zwei mitgelieferte, die genau die bisherigen Spalten fuehren - wer nichts
einstellt, bekommt dasselbe Ergebnis wie vorher.
Dazu laesst sich eine P-touch-Datei hinterlegen. Geprueft wird ueber Endung und
ZIP-Inhalt (label.xml), nicht ueber den Content-Type: Fuer .lbx meldet der
Browser meist application/octet-stream. Eine .lbx merkt sich den Pfad zu ihrer
Datenquelle - auf einem fremden Rechner muss die CSV in P-touch einmal neu
verknuepft werden. Das laesst sich von hier aus nicht loesen; die Datei
dabeizuhaben erspart trotzdem das Suchen.
Sicherung auf Version 4: Vorlagen kommen mit, Kategorien als Pfad und eigene
Felder als Label, weil IDs zwischen zwei Instanzen nicht dieselben sind. Die
.lbx als Base64 - das erste Binaerformat in dieser Sicherung, vertretbar bei
wenigen Kilobyte und einer Vorlage, die ohne ihre Datei nur eine halbe waere.
23 neue Tests (343 gesamt), dazu im Browser durchgespielt: Kabel-Kategorie mit
eigenem Feld, Spalte hinzugefuegt, CSV mit "Länge (m)" und dem Wert, .lbx
hoch- und wieder heruntergeladen (Pruefsumme identisch), PNG und Nicht-ZIP
sauber abgelehnt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RUECKGABE
Der Knopf war da, nur nicht zu finden: ein 16-px-Symbol in einer Spalte ohne
Ueberschrift, 52 px breit, ganz rechts in einer horizontal scrollenden Tabelle.
Beschriftet war er allein ueber title= - ein Tooltip, den auf einem Touchgeraet
nie jemand sieht.
Jetzt traegt er Text ("Zuruecknehmen"), die Spalte heisst "Verleih" und ist
breit genug, und beide Knoepfe haben ein aria-label. Dazu eine neue Seite
"Verliehen" in der Seitenleiste: alles Draussene an einem Ort, Rueckgabe direkt
aus der Liste. Bisher gab es diese Uebersicht nur als zwei Dashboard-Karten, und
die stehen nicht im Standard-Layout - wer sie nicht selbst hinzufuegte, hatte
gar keine.
In der App fuehrte der einzige Weg ueber das Bearbeiten-Formular des Stuecks;
jetzt geht es per Wischgeste und Kontextmenue direkt aus der Liste. Der
Verleih-Filter dort sass hinter einem nackten Symbol und ist beschriftet.
Zurueckgenommen wird nur noch nach Rueckfrage, in Web und App.
ZUBEHOER
Ein Netzteil gehoert zu einem Laptop: items.parent_item_id, genau ein Geraet je
Teil, beliebig viele Teile je Geraet. Eine Spalte genuegt dafuer, es braucht
keine Verknuepfungstabelle. ON DELETE SET NULL, damit das Netzteil den Laptop
ueberlebt statt mitgeloescht zu werden - und weil SQLite Fremdschluessel nicht
erzwingt, loest der Router die Zuordnung beim Loeschen zusaetzlich selbst.
Ringe (A traegt B, B traegt A) weist die Zuordnung mit 409 ab; die Elternkette
wird dafuer hochgelaufen, wie beim Kategoriebaum.
Die Beziehung laeuft ueber eigene Endpunkte und bewusst NICHT ueber ItemUpdate:
dessen Schema sendet immer alle Felder mit, auch leere, damit sich Felder
loeschen lassen. Genau so gingen schon einmal Kaufpreis und Waehrung verloren,
als ein Aufrufer ein Feld vergass.
Beim Verleihen fragt die Oberflaeche, welche Teile mitgehen (vorausgewaehlt:
alles, was im Haus ist), beim Zuruecknehmen kommen sie zusammen zurueck. Ohne
das Spiegelbild kaeme das Geraet zurueck und sein Netzteil bliebe ewig
"verliehen", weil niemand mehr danach sucht. Ungefragt nimmt der Server nie
etwas mit.
In der App sind zubehoer und gehoert_zu optional dekodiert: Die App spricht mit
mehreren Serverprofilen, und ein aelterer Server schickt die Felder nicht - ein
Vorgabewert reicht dafuer nicht, Swift nimmt ihn beim Dekodieren nicht her.
12 neue Tests (320 gesamt). Der Umlauf ist ausserdem gegen ein lokales Backend
im Browser durchgespielt: verleihen mit Zubehoer, Rueckfrage, beide zusammen
zurueck.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei Luecken, die zusammen den Eindruck machten, ein angelegtes Einzelstueck
sei verschwunden.
Der Verlauf kannte nur das Verschwinden. remove_item schreibt seit jeher eine
Bewegung, create_items nicht - Abgaenge von Einzelstuecken tauchten also auf,
Zugaenge nie, im Verlauf wie im Dashboard. Jetzt schreibt das Anlegen je Stueck
eine in-Bewegung mit UID (oder der eigenen Notiz, falls angegeben), Lagerort und
Benutzer, spiegelbildlich zum Entfernen. Der Bestand bleibt unberuehrt: er
zaehlt bei Einzelstuecken die Zeilen in items und rechnet nicht aus Bewegungen.
Und der Weg dorthin fehlte. ProductFormView liefert den angelegten Artikel, aber
zwei der drei Aufrufer warfen ihn weg ("{ _ in showNew = false }") - nur der
Einlagern-Weg nutzte ihn. "Einzelstuecke anlegen" steckt ausschliesslich in der
Artikelansicht, die man vom Scan-Tab und aus den Listen nach dem Anlegen gar
nicht erreicht. Man musste den frischen Artikel von Hand wiederfinden.
Bei Gegenstaenden mit eigener UID je Stueck oeffnet sich jetzt direkt das
Anlege-Formular. Gesetzt wird das im onDismiss des Artikelformulars: beides im
selben Zug zu setzen verschluckt SwiftUI, weil das zweite Sheet erscheinen soll,
waehrend das erste noch verschwindet.
Der Parameter von create_items heisst dadurch user statt _; die drei Testaufrufe
sind mitgezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Lagerort bleibt beim Verleihen stehen - er sagt weiter, wohin das
Stueck nach der Rueckgabe zurueckgehoert. Ein Lagerort-Wert "verliehen"
haette diese Information vernichtet. Stattdessen eine eigene Tabelle
item_loans mit Entleiher und Grund als Freitext, Verleihdatum und einem
optionalen Rueckgabedatum: "kurz mal mitgenommen" soll nicht an einem
Datumszwang scheitern.
Zurueckgegebene Vorgaenge bleiben stehen, damit die Tabelle gleichzeitig
die Verleih-Historie je Stueck ist. Ein Teilindex auf die offenen
Vorgaenge haelt fest, dass ein Stueck nicht zweimal gleichzeitig
verliehen sein kann - wie bei den "Ueberall"-Mindestbestaenden, weil
Postgres NULLs in UNIQUE als verschieden zaehlt.
Verliehen heisst nicht im Bestand: Ein Stueck, das nicht im Haus ist,
zaehlt nirgends mehr mit - weder am Lagerort noch im Gesamtbestand.
Entschieden wird das an einer Stelle (services/loans.py::nicht_verliehen),
die alle fuenf Zaehlungen benutzen. Sonst behauptete "Was liegt hier?"
weiterhin, die Kamera liege im Regal. Folge: Faellt ein verliehenes
Stueck unter den Mindestbestand, erscheint der Artikel auf der
Einkaufsliste.
Verleihen und Zuruecknehmen darf jeder angemeldete Benutzer - es ist eine
Buchung wie Ein- und Auslagern, keine Stammdatenaenderung - und wird mit
dem Konto protokolliert. Nur das Loeschen eines Vorgangs bleibt Admins
vorbehalten, weil es Historie vernichtet.
Bewusst keine Movement-Bewegung je Verleih: Der Verlauf rechnet den
Bestand rueckwaerts aus vorzeichenbehafteten Bewegungen, ein Abgang ohne
Gegenbuchung wuerde diese Kurve verfaelschen.
GET /loans?due_in= liefert die faelligen Verleihe und ist damit schon die
Datenquelle fuer die spaetere Rueckgabe-Erinnerung der App. Oberflaeche in
Web und App fehlt noch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Sitzungs-Token benannte den Benutzer ueber seinen Namen (sub = username).
Damit war der Name die Kennung der Sitzung - ein Umbenennen haette jede laufende
Anmeldung dieses Benutzers ungueltig gemacht, und deshalb war es gar nicht erst
vorgesehen. Beim Anmeldedienst geaenderte Namen liessen sich in Vorrania
folglich nirgends nachziehen.
Neue Token tragen die ID (sub = "<id>", zusaetzlich uid), und deps.py loest die
Sitzung darueber auf. Alles andere haengt ohnehin schon an der ID: Bewegungen,
Dashboards, API-Tokens. Der Name ist damit reine Anzeige.
Benutzer lassen sich jetzt unter "Benutzer" umbenennen - auch sich selbst, ohne
aus der Sitzung zu fliegen. Der Anmeldeweg selbst sucht weiterhin ueber den
Namen; das ist das Feld im Formular und soll so bleiben.
Bestandsschutz: Token ohne uid werden weiter ueber den Namen aufgeloest, damit
beim Update niemand abgemeldet wird. Der Zweig darf nach Ablauf der
Token-Lebensdauer weg. Das zusaetzliche uid unterscheidet alt von neu eindeutig -
ein Benutzer koennte schlicht "42" heissen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wer sich zum ersten Mal ueber den Anmeldedienst anmeldet und hier noch kein
Konto hat, bekam bisher wortlos ein neues - mit Rolle "Benutzer" und ohne
Verlauf. Wer schon ein Konto hatte, wollte aber fast immer genau das verknuepfen
und stand dann mit zwei Konten da.
Jetzt zeigt der Rueckweg eine Entscheidungsseite: bestehendes Konto verknuepfen
oder ein neues anlegen. Das Verknuepfen verlangt einmalig Benutzername und
Passwort des bestehenden Kontos. Genau dieser Nachweis ist der Grund, warum der
Anmeldeweg selbst nie fragen darf, welches Konto gemeint ist - ohne ihn koennte
sich jeder mit einem beliebigen Konto beim Anmeldedienst an ein fremdes
Vorrania-Konto haengen.
Die geprueften Angaben liegen dafuer kurzlebig und einmalig in
oidc_pending_links; vom Code steht dort nur der SHA-256-Hash, wie bei den
API-Tokens. Ein falsch getipptes Passwort verbraucht den Vorgang nicht, und
unbekannter Benutzername und falsches Passwort antworten gleich - sonst liessen
sich vorhandene Konten abzaehlen.
AUTO_CREATE=false blendet den Knopf zum Anlegen aus und laesst sich auch ueber
den Dialog nicht umgehen. Der App-Weg bleibt unveraendert, weil er keinen
Zwischenschritt anzeigen kann.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
In Authentik hat jede Anwendung standardmaessig ihren eigenen Issuer, abgeleitet
aus dem Application-Slug. Web- und App-Client brauchen dort aber zwei getrennte
Anwendungen, weil ein Provider entweder vertraulich ODER oeffentlich ist.
Bisher wurde das App-Token gegen den Issuer und den Schluesselsatz der
Web-Anwendung geprueft - die App-Anmeldung waere damit an der iss-Pruefung
gescheitert. Discovery und JWKS werden jetzt je Adresse gepuffert statt je
Anbieter, und OIDC_<KUERZEL>_NATIVE_DISCOVERY_URL benennt die Anwendung des
App-Clients. Ohne die Variable bleibt es bei einer gemeinsamen Adresse.
Ausserdem im README: der Signing Key muss gesetzt sein, sonst signiert Authentik
mit HS256 und das Token wird abgelehnt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neben dem Passwort-Login gibt es jetzt einen zweiten, gleichberechtigten Weg:
OpenID Connect gegen einen selbst gehosteten Anmeldedienst. Ohne Konfiguration
aendert sich nichts - keine SSO-Knoepfe, Passwort-Login wie bisher.
Der Anmeldedienst ist NUR fuer die Anmeldung zustaendig. Danach stellt Vorrania
sein eigenes Token aus wie beim Passwort-Login, deshalb bleiben deps.py, die
Rollenpruefung und die langlebigen API-Tokens unveraendert.
Backend
- Anbieter kommen aus der Umgebung (OIDC_PROVIDERS + OIDC_<KUERZEL>_*). Ein
zweiter Anbieter ist damit reine Konfiguration, ohne Codeaenderung.
- Browser-Weg (vertraulicher Client) und App-Weg (oeffentlicher Client, PKCE)
enden beide im selben Token-Schema.
- state/nonce/PKCE-Verifier liegen in einer kurzlebigen Tabelle statt in einem
Cookie: Starlettes CORS-Schicht spiegelt bei allow_origins=["*"] zusammen mit
allow_credentials die Herkunft zurueck, sobald ein Request ein Cookie traegt.
Ohne Cookie entsteht diese Wechselwirkung erst gar nicht - und der Ablauf
bleibt mit der db-Fixture pruefbar.
- Zuordnung ueber die sub des Anbieters; die Erstverknuepfung laeuft ueber die
BESTAETIGTE E-Mail-Adresse. Rollen kommen nie aus dem Anbieter, und ein
gleichlautender preferred_username uebernimmt nie ein bestehendes Konto.
- users.password_hash darf jetzt leer sein (reine SSO-Konten). verify_password
faengt das ab, sonst antwortete der Login mit 500 statt 401.
Verknuepfen bestehender Konten
- Neue Seite "Mein Konto" (fuer alle Benutzer, nicht nur Admins): verknuepfen
und wieder loesen. Die Berechtigung stammt aus der laufenden Sitzung, es
braucht also weder eine hinterlegte Adresse noch Admin-Rechte.
- Der Start laeuft ueber POST, weil eine Seitennavigation das Bearer-Token
nicht mitschicken kann.
- Die letzte Anmeldemoeglichkeit eines Kontos laesst sich nicht loesen.
iOS
- AppAuth (erste Paketabhaengigkeit) mit PKCE im System-Browser, nie in einem
eingebetteten WebView, und ohne Client-Secret.
- offline_access nur im App-Weg: der Browser-Weg braucht kein Refresh-Token und
bekaeme sonst einen ueberfluessigen Zustimmungsdialog.
- Anmeldezustand je Serverprofil im Keychain; abgelaufene Token erneuert die App
still im Hintergrund. Gleichzeitige 401er teilen sich eine Erneuerung.
Deployment
- docker-compose reicht die ganze .env durch. Einzeln aufzuzaehlen ginge nur mit
fest verdrahteten Kuerzeln - genau das soll ein weiterer Anbieter nicht
erfordern.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Warum eine angeklickte Artikelseite nur zur Haelfte lud: Die Bild-Route wird je
Tabellenzeile einmal aufgerufen, und ein Browser haelt je Host nur rund sechs
Verbindungen offen. Was diese Route aufhaelt, legt die ganze Oberflaeche lahm.
Sie hielt gleich dreifach auf:
1. Fehlte die lokale Kopie, holte ``images.ensure`` das Bild MITTEN in der
Anfrage von der fremden Adresse - bis zu 15 Sekunden, je Zeile, und bei
jedem Aufruf erneut, weil ein Fehlschlag nirgends vermerkt wurde. Artikel
aus einem Backup-Import haben genau diese Ausgangslage. Gemessen: 2,01 s
je Aufruf, dreimal hintereinander. Jetzt: 0,01 s, das Nachholen laeuft nach
der Antwort im Hintergrund und ein toter Link wird eine Stunde gesperrt.
2. Ein Vorschaubild ist 34x34 Pixel gross - ausgeliefert wurde das Original.
Gemessen: 2530 KB je Briefmarke, bei 40 Zeilen 99 MB fuer eine Liste. Neu
erzeugt die Bild-Route (Pillow) eine Vorschau und legt sie daneben ab;
``?thumb=1`` liefert sie aus. Gemessen: 0,6 KB.
3. If-None-Match wurde ignoriert. Nach den 5 Minuten Cache-Frist lud der
Browser jedes Bild komplett neu - das erklaert, warum es "alle 20 Minuten"
wieder losging. Jetzt 304 ohne Daten.
Dazu im Web: laufende Bildabrufe werden beim Seitenwechsel abgebrochen (sonst
steht die neue Seite hinter Bildern Schlange, die niemand mehr sieht), und wo
``image_version`` bekannt ist, entfaellt die Anfrage fuer bildlose Artikel ganz.
Und der Verbindungsvorrat der Datenbank: 5 (+10) gegen 40 Arbeits-Threads von
FastAPI. Ab der 16. gleichzeitigen Anfrage wartete eine Route stillschweigend
30 Sekunden - von aussen ein haengender Server. Jetzt 10 (+20) mit 10 Sekunden
Frist: lieber ein Fehler als eine halbe Minute Stille.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die drei Einheiten-Arten waren bisher strikt getrennt: BASE_OF_KIND bildet
count/weight/volume 1:1 auf Stueck/Gramm/Milliliter ab, ohne jeden Faktor
dazwischen. Zwei Stellen setzten das durch - to_base lehnte artfremde Einheiten
beim Ein-/Auslagern ab, und group_min_context filterte stueckweise gefuehrte
Artikel aus einer Kilogramm-Gruppe stillschweigend heraus. Letzteres war der
Anlass: eine Gruppe "Wurst" in kg sah Bratwuerste in Stueck gar nicht.
Ein Artikel darf jetzt eine Zweiteinheit tragen: "3 Stueck ≙ 250 g". Gespeichert
wird das eingegebene PAAR, nicht der Faktor - wer 3 und 250 eintippt, sieht beim
naechsten Oeffnen genau das wieder. Das hat auch einen rechnerischen Grund:
250 * 3 / 250 ist exakt 3, der Umweg ueber 250/3 ergibt 3,0000000000000004 und
liefe damit gegen die Bestandspruefung beim Auslagern.
Der Artikel bleibt in seiner Basiseinheit gefuehrt; die Bruecke ist reine
Rechnung. Gruppen zaehlen artfremde Artikel jetzt mit ihrem Faktor mit
(GroupMinContext.faktoren), Bestandssummen laufen dafuer je Artikel gewichtet -
weiterhin zwei Abfragen, nur mit GROUP BY. Ein-/Auslagern in der Fremdeinheit
geht, krumme Mengen werden bewusst gebucht statt gerundet: 100 g sind 1,2 Stueck,
und Runden wuerde stumm etwas anderes buchen als angegeben.
WICHTIGE KORREKTUR am urspruenglichen Plan: die Teilmengen-Bedingung in
_gruppen_bedarfe konnte NICHT bleiben. Sie war bisher zugleich ein
Einheiten-Schutz, weil Artikel verschiedener Arten zwangslaeufig disjunkt waren.
Mit der Bruecke gilt sie ploetzlich auch zwischen einer Stueck- und einer
Gramm-Gruppe - und _netted_topups haette einen Bedarf in Stueck von einem in
Gramm abgezogen. Jetzt wird nur noch zwischen Gruppen derselben Basiseinheit
verrechnet.
Open Food Facts: "3 x 80 g" verlor bisher den Multiplikator, weil der Regex den
ersten Zahl-Einheit-Treffer nahm. parse_gebinde liefert jetzt Gesamtmenge UND
Stueckzahl und belegt die Zweiteinheit vor; parse_quantity behaelt seinen
schmalen Vertrag.
18 neue Tests. Dass test_wrong_kind_rejected und
test_einheitenfilter_gilt_auch_fuer_untergruppen unveraendert gruen bleiben, ist
selbst der Beleg: ohne Bruecke aendert sich nichts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das JSON-Backup schrieb bei Gruppen nur Name, Mindestbestand und Einheit -
Gebinde und Ort-Bedarfe fehlten schon vorher, ein Restore verlor sie also
stillschweigend. Mit den Obergruppen waere die komplette Hierarchie dazu
gekommen.
Format v3: Gruppen bringen jetzt parents (als NAMEN, nicht IDs - die sind
zwischen zwei Instanzen nicht gleich), Gebinde-Felder und ihre Mindestbestaende
je Ort mit; Artikel ebenso. Ort null = "Ueberall". Der Import laeuft dafuer
zweiphasig, weil Kanten erst gesetzt werden koennen, wenn alle Gruppen und
Lagerorte existieren, und weist Ringe ab - eine beschaedigte Datei darf keinen
einschleusen, der danach jede Auswertung im Kreis laufen liesse.
v2-Sicherungen bleiben lesbar; ihnen fehlen die neuen Listen einfach. Die
CSV-Spalte "mindestbestand" meint weiterhin den Bedarf ohne Ortsangabe und
landet in der Ueberall-Zeile.
maintenance.py raeumt die n:m-Zeilen jetzt ausdruecklich ab: das Core-DELETE
nimmt sie nicht mit, und SQLite erzwingt keine Fremdschluessel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei zusammenhaengende Umbauten, weil sie dieselben Stellen betreffen.
Obergruppen: Gruppen bilden jetzt einen gerichteten azyklischen Graphen statt
einer flachen Liste. Eine Gruppe darf unter MEHREREN Obergruppen haengen -
"Grillwurst" unter "Wurst" UND unter "Grillgut"; mit einem einzelnen parent_id
waere genau das nicht abbildbar. Bestand und Mindestbestand einer Gruppe zaehlen
den gesamten Untergraphen, wobei eine ueber zwei Wege erreichbare Untergruppe
nur einmal zaehlt (services/gruppen.py arbeitet durchgaengig mit Mengen).
Product.group_id bleibt unveraendert - ein Artikel haengt weiter an genau einer
Gruppe.
Mindestbestaende: der separate Gesamt-Mindestbestand entfaellt. Er wird zur
Zeile mit location_id NULL ("Ueberall") und ist damit die Wurzel ueber allen
Lagerorten - dieselbe Verrechnung wie bei verschachtelten Orten greift jetzt
auch zwischen Ueberall und Kueche, wodurch derselbe Artikel nicht mehr doppelt
in der Einkaufsliste steht. Alle Werte liegen einheitlich in Basiseinheiten
statt in drei verschiedenen Einheiten nebeneinander; das Umrechnen beim
Umschalten der Erfassungseinheit entfaellt dadurch ersatzlos.
_netted_topups nimmt die Hierarchie jetzt als Parameter und faltet damit
Lagerort-Baum und Gruppen-Graph. Verrechnet wird zwischen zwei Gruppen nur,
wenn die zaehlenden Artikel der Untergruppe eine Teilmenge der Obergruppe sind -
zaehlt die Obergruppe in Kilogramm und die Untergruppe in Stueck, kommt ein Kauf
dort oben nicht an.
Die vierfach kopierte Bestandssumme wandert in Sammelabfragen
(summe_bestand_base), sonst vervielfacht der transitive Teilgraph die Abfragen.
Einmalige Datenwanderung beim Start (Merker in den Einstellungen), 18 neue
Tests - darunter Doppelzaehlung ueber zwei Wege, Ringschutz und die bewusst
offene Grenze bei zwei Obergruppen mit gemeinsamer Untergruppe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gruppen bekommen ein eigenes Richt-Gebinde (package_size/label) plus
min_stock_in_packages. Weil die Produkte einer Gruppe unterschiedlich
große Packungen haben können (Pesto 99 g vs. 160 g), legt die Gruppe einen
gemeinsamen Richtwert fest (1 Glas ≈ X g).
- Neuer Helfer group_min_context() zentralisiert, in welcher Einheit der
Gruppen-Mindestbestand zaehlt (Gebinde ODER verwaltete Einheit).
- Einkaufsliste (gesamt + je Ort), Dashboard-Bedarf und GroupOut nutzen ihn;
need.text kommt so als 'x Glaeser (y g)'.
- update_group rechnet bestehende Werte beim Umschalten der Einheit um, damit
der physische Bedarf gleich bleibt.
- Migration: groups.package_size/package_label/min_stock_in_packages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Statt 'fehlt 500 Gramm' jetzt die Produktgroesse als Leitangabe:
'fehlt 3 Glaeser (500 g)'. Auf ganze Gebinde aufgerundet, weil man nur
ganze Glaeser/Dosen kauft; Basiseinheit als kleiner Hinweis.
Backend: neues ShoppingNeed-Objekt an jeder Einkaufslisten-Zeile (Produkte
+ Gruppen, gesamt + je Ort) - damit die Angabe auch direkt ueber die API
kommt ('x Glaeser (y g)'), inkl. Pluralisierung aus der Gebinde-Tabelle.
Gruppen leiten das Gebinde ab, wenn alle passenden Produkte dasselbe haben,
sonst bleibt es bei der Basiseinheit. Bestehende Felder unveraendert.
Frontend: gemeinsame Komponente ShoppingNeedText fuer Karte und volle Seite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
shopping_list_by_location zaehlte Ober- und Unterort unabhaengig, obwohl der
Ober-Subtree den Unterort schon enthaelt - Bedarf wurde doppelt gemeldet. Jetzt
werden Bedarfe je Produkt/Gruppe von unten nach oben verrechnet (_netted_topups):
was in einen Unterort gekauft wird, deckt den Oberort mit. Im Mehl-Beispiel (Lemgo
5, Kueche 2, je 1 fehlend) meldet der Server nur noch 1 (Kueche) statt 2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/lots/rows liefert jetzt die Art (Lebensmittel/Gegenstand) mit und listet nur
Charge-Artikel (Lebensmittel + Verbrauchsgegenstand). Lots von "Menge je
Lagerort" (und etwaige Einzelstueck-Altlasten) fallen raus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
list_products rief product_to_out je Artikel auf - pro Artikel mehrere Abfragen
(Bestand, abgelaufene Chargen, Codes, Bild, Lazy-Relationen). Bei vielen
Artikeln = hunderte Queries und mehrere Sekunden. Neu: products_to_out_bulk holt
Bestand/Abgelaufen/Codes/Bild je einmal fuer alle und laedt Relationen eager
(joinedload/selectinload). Ergebnis identisch (Test), aber nur noch wenige Queries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neue Spalte updated_at, die bei jeder Stammdaten-Aenderung automatisch nachzieht
(onupdate). Migration legt sie nullable an und setzt Altbestaende auf created_at.
In ProductOut ausgegeben - Basis fuer eine "Zuletzt geaendert"-Spalte im Web.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
product_tracking leitet die Art bisher nur aus der Kategorie ab. Beim Umstellen
Lebensmittel -> Verbrauchsgegenstand faellt die (Lebensmittel-)Kategorie weg,
sodass der Artikel faelschlich "food" blieb und in der Lebensmittelliste hing.
individual/bulk implizieren jetzt "object", unabhaengig von der Kategorie.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neues Flag bulk auf Product: Gegenstand als Charge mit Menge/Einheit/MHD/
Mindestbestand fuehren (z.B. Sonnencreme in ml), ohne eindeutigen Code.
Migration (ADD COLUMN), Schemas und Anlege-Pfad ergaenzt. Kernlogik nutzt
foodLike = kein Gegenstand ODER bulk; Bestand/Einkaufsliste unveraendert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neuer Endpoint GET /items liefert alle Einzelstuecke ueber alle Produkte, angereichert mit Produkt-, Kategorie-, Lagerort- und Shop-Angaben (Beziehungen vorgeladen). ItemOut bekommt category_id/category_name. ProductOut bekommt image_version (Epoch der letzten Bildaenderung, None ohne Bild) - identisch zum ETag der Bild-Route, damit Clients Bilder cachen und nur bei Aenderung neu laden. Grundlage fuer die neue Einzelstueck-Liste (Web+iOS) und den iOS-Bild-Cache.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1) Beleganalyse erkennt Datumsangaben mit Monatsnamen (DE+EN, z.B. Amazon 'Bestelldatum 29 April 2026'), nicht nur 29.04.2026 - so wird das Kaufdatum solcher Rechnungen gefunden. 2) Beim direkten Anlegen eines Einzelstuecks mit Beleg werden jetzt alle erkannten Preise als Vorschlag angeboten (Dropdown), statt stillschweigend nur den wahrscheinlichsten zu nehmen. 3) Alle Lagerort-Dropdowns zeigen den vollen Pfad (Hedingen -> Keller -> Schublade 1) statt nur den Namen, damit gleichnamige Orte unterscheidbar sind.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neuer Endpunkt GET /location/{code} liefert alle Artikel an einem Lagerort inkl. der Unterorte (Lot-Bestaende und Einzelstuecke), sortiert nach Name mit Menge in Artikeleinheiten. In der App loest der /l/-QR beim Nachschlagen jetzt diese Liste auf; ein Tipp auf einen Eintrag oeffnet den Artikel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lagerort-IDs sind jetzt ein zufaelliger 10-Zeichen-Code (wie die Einzelstueck-UIDs) statt einer fortlaufenden Zahl - so kollidiert die Stammdaten-Sicherung zwischen zwei Instanzen praktisch nie mehr, und der Code ist zugleich der Inhalt des QR /l/<code>. Alle Fremdschluessel (lots, movements, items, Mindestbestaende, parent_id) ziehen mit; die Umstellung laeuft einmalig und transaktional beim Serverstart (_migrate_locations_to_code) und rollt bei Fehlern komplett zurueck. Vor dem Deploy ein DB-Backup machen.
iOS-Einlagern oeffnet nicht mehr sofort die Kamera, sondern ein Formular mit Artikelsuche; die Kamera kommt erst per Button. Im Formular laesst sich der Lagerort zusaetzlich per /l/-QR scannen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Backend: services/master_data.py exportiert/importiert Kategorien (inkl.
eigener Felder), Lagerorte, Einheiten und Gebinde als JSON – MIT IDs, damit
gedruckte QR-Codes und Verweise nach dem Wiederherstellen passen. Import-Modus
skip (Vorhandenes lassen) oder overwrite (per ID aktualisieren); Namens-
Konflikte werden uebersprungen, nicht abgebrochen. Postgres-Sequenzen werden
danach angehoben. Endpunkte GET /export/master-data, POST /import/master-data.
5 neue Tests, Suite 162 gruen.
- Web: neue Karte "Stammdaten sichern & wiederherstellen (JSON)" in Import/Export.
- Web: beim Lagerort-Anlegen bleibt der uebergeordnete Ort in der Auswahl.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Web: der übergeordnete Lagerort wird beim Anlegen jetzt über den
aufklappbaren Baum (CategorySelect) gewählt statt über das flache Dropdown.
- Backend: Mindestbestand je Lagerort wird jetzt gegen den Bestand INKL. aller
Unter-Lagerorte geprueft. Ein Bedarf auf „Hedingen" gilt also als gedeckt,
wenn der Vorrat in „Hedingen -> Keller" liegt. Helfer
location_subtree_stock_base; 2 neue Tests, Suite 157 gruen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bei einer Rechnung mit mehreren Artikeln soll man den Zeilenpreis des passenden
Produkts wählen können. Die Kandidatenliste enthaelt daher wieder alle
geldartigen Betraege (Zeilen- und Summenbetraege), nur ohne MwSt-Satz und
Nicht-Geld-Zahlen; der Auto-Tipp bleibt die Gesamtsumme und steht vorne.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Anhand echter Galaxus-Rechnungen nachgeschärft:
- Preis: MwSt-Satz ("8.10 %") wird als Prozentangabe ignoriert; von den
Betraegen nahe einem Summen-Stichwort gewinnt der GRÖSSTE (Endbetrag inkl.,
steht rechts) statt des naechstgelegenen (bisher der Exkl.-Betrag).
- Kaufdatum: eindeutige Stichworte (Rechnungs-/Bestell-/Kaufdatum) haben Vorrang;
"Fälligkeitsdatum"/Zahlungsziel werden nicht mehr als Kaufdatum gewertet.
- Garantieende: ein ausdruecklich genanntes Enddatum im Garantie-Umfeld
(z.B. "… - 06.10.2027") hat Vorrang vor dem gerechneten. 4 neue Tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>