Einkaufsliste bekommt ein Gedaechtnis - und den Knopf aus dem Rezept
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f02e9a3840
commit
795e233a1b
@@ -815,6 +815,91 @@ class IntegrationSecret(Base):
|
||||
)
|
||||
|
||||
|
||||
class ShoppingEntry(Base):
|
||||
"""Ein Eintrag, den jemand bewusst auf die Einkaufsliste gesetzt hat.
|
||||
|
||||
Die Einkaufsliste war bis hierher vollstaendig **gerechnet**: Artikel unter
|
||||
ihrem Mindestbestand, Gruppen unter ihrem, Bedarfe je Lagerort. Das bleibt
|
||||
die Staerke der Liste und wird nicht ersetzt. Was fehlte, war ein Ort fuer
|
||||
"Milch gehoert drauf, weil ich das sage" - und damit auch fuer "diese
|
||||
Zutaten fehlen mir fuer das Rezept".
|
||||
|
||||
Genau EINES von ``product_id``, ``group_id`` und ``free_text`` ist gesetzt.
|
||||
Geprueft wird das beim Anlegen (services/einkaufsliste.py), nicht in der
|
||||
Datenbank: Als CHECK-Bedingung waere es ueber drei nullbare Spalten
|
||||
unleserlich, und die Fehlermeldung waere keine, die man jemandem zeigen
|
||||
kann. Der Freitext ist unverzichtbar - fuer eine Rezeptzutat, die hier
|
||||
keinem Artikel entspricht ("Ras el Hanout"), ist er die einzige Form, in
|
||||
der sie auf den Zettel kommt.
|
||||
|
||||
``quantity_base`` steht in Basiseinheiten, wie alle Mengen im Projekt. Bei
|
||||
einem Freitext bleibt sie leer: "etwas Hefe" hat keine Zahl.
|
||||
|
||||
``recipe_slug``/``recipe_name`` halten die Herkunft fest. Sie sind mehr als
|
||||
Zierde: Derselbe Artikel kann fuer zwei Rezepte fehlen, und dann braucht man
|
||||
ihn zweimal. Je Rezept und Ziel gibt es deshalb hoechstens einen offenen
|
||||
Eintrag - ein zweiter Druck auf "fehlende Zutaten hinzufuegen" rechnet die
|
||||
Menge neu, statt die Liste zu verdoppeln.
|
||||
"""
|
||||
|
||||
__tablename__ = "shopping_entries"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
product_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("products.id", ondelete="CASCADE"), nullable=True, index=True
|
||||
)
|
||||
group_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("groups.id", ondelete="CASCADE"), nullable=True, index=True
|
||||
)
|
||||
free_text: Mapped[str | None] = mapped_column(String(120), nullable=True)
|
||||
quantity_base: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
# BaseUnit-Wert (piece/gram/milliliter) als String - wie ``Product
|
||||
# .secondary_base``, damit kein Postgres-Enumtyp nachzuziehen ist.
|
||||
base: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
# "manual" | "recipe"
|
||||
source: Mapped[str] = mapped_column(String(16), nullable=False, default="manual")
|
||||
recipe_slug: Mapped[str | None] = mapped_column(String(120), nullable=True, index=True)
|
||||
recipe_name: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
done: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
done_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
created_by: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=_now, nullable=False
|
||||
)
|
||||
|
||||
product: Mapped[Product | None] = relationship()
|
||||
group: Mapped[Group | None] = relationship()
|
||||
|
||||
|
||||
class ShoppingCheck(Base):
|
||||
"""Ein abgehakter Posten der GERECHNETEN Einkaufsliste.
|
||||
|
||||
Die gerechneten Vorschlaege haben keine Zeile in der Datenbank - sie
|
||||
entstehen bei jedem Aufruf neu aus den Mindestbestaenden. Abhaken liess sich
|
||||
deshalb nur im Browser merken und war nach dem Neuladen weg, mitten im
|
||||
Supermarkt.
|
||||
|
||||
Gemerkt wird hier nur der Schluessel des Postens, nicht der Posten selbst:
|
||||
``p12`` (Artikel), ``g3`` (Gruppe), ``l KUECHE p12`` (Artikel an einem
|
||||
Lagerort) - dieselben Schluessel, die die Oberflaeche ohnehin bildet. Damit
|
||||
bleibt die Liste gerechnet, und das Abhaken haengt sich nur daran. Sinkt der
|
||||
Bestand spaeter wieder, taucht der Posten mit seinem alten Haken auf; das
|
||||
Aufraeumen erledigt "Erledigte entfernen".
|
||||
"""
|
||||
|
||||
__tablename__ = "shopping_checks"
|
||||
|
||||
key: Mapped[str] = mapped_column(String(80), primary_key=True)
|
||||
checked_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=_now, nullable=False
|
||||
)
|
||||
user_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
|
||||
class MealieFoodMap(Base):
|
||||
"""Gemerkte Zuordnung: Mealie-Lebensmittel X ist bei uns Artikel/Gruppe Y.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user