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>
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>
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>
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>
- guess_acquired_on (Kaufdatum) und guess_shop (bekannter Shop erkannt -> id;
sonst Kandidatenname zum Anlegen) ergaenzt.
- Upload liefert zusaetzlich suggested_acquired_on / shop_id / shop_name.
- Neuer POST /items/analyze-document: analysiert einen Beleg OHNE zu speichern
(zum Vorbefuellen beim Anlegen). Gemeinsamer Helfer _doc_suggestions.
- 6 neue Tests; Suite 150 gruen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Findet die Beleg-Analyse mehrere Beträge, liefert der Upload jetzt eine
Kandidatenliste (bester Tipp zuerst). Web und iOS bieten dann ein Dropdown zur
Auswahl des richtigen Kaufpreises, statt nur den automatischen Tipp. 1 neuer
Test; Backend-Suite gruen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
guess_price_cents zieht den Kaufpreis aus dem Belegtext: bevorzugt den Betrag
nach einem Summen-Stichwort (Gesamtbetrag/Total/…), sonst den groessten Betrag.
Versteht Tausendertrenner (1'299.00) und deutsches Format (1.299,00). Der
Upload liefert den Vorschlag als suggested_price_cents mit. 5 neue Tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Item: price_cents + currency (Kaufpreis rappen-/centgenau). Migration ergaenzt.
- Neue Tabelle item_documents (mehrere Belege je Stueck, PDF oder Bild); Blob
wird verzoegert geladen, damit Item-Listen leicht bleiben.
- Endpunkte: Upload (POST), Ansehen/Download (GET), Loeschen (DELETE) je Stueck.
Dateiname beim Download gehaertet (keine Header-Injektion).
- services/warranty.py: schaetzt aus PDF-Text lokal ein Garantieende
(Zeitraum + Kaufdatum, oder Datum nahe Garantie-Stichwort); pypdf ergaenzt.
Der Upload liefert den Vorschlag zurueck. 10 neue Tests, Suite 136 gruen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gegenstands-Produkte koennen als Einzelstuecke gefuehrt werden (Product.individual):
jedes physische Stueck ist ein Item mit eigener kurzer UID (fuer QR), eigenem
Lagerort, Kaufdatum, Garantie, Bezugsquelle und Notiz. So laesst sich dasselbe
Modell mehrfach getrennt fuehren (Powerbank 2024 + 2025).
Neu: models.Item + Product.individual (+Migration), Schemas, services/items.py
(UID-Erzeugung, Anreicherung), routers/items.py (CRUD, /items/by-uid/{uid} zur
QR-Aufloesung, /items/{id}/remove mit Grund als Bewegung). current_stock zaehlt
bei Einzelstueck-Produkten die Items. Tests + HTTP-Smoke gruen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>