neue Archivfunktion und Optimierung an Artikelbearbeitung

This commit is contained in:
2026-08-09 18:24:04 +02:00
parent dd51532533
commit b80e2d5a6c
23 changed files with 1185 additions and 77 deletions

View File

@@ -4,7 +4,9 @@ Alle Schreibzugriffe erhöhen den Revisionszähler der Liste und schreiben
den neuen Wert in `row_rev` der geänderten Zeile.
"""
from fastapi import APIRouter, HTTPException, status
from datetime import timedelta
from fastapi import APIRouter, HTTPException, Query, status
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
@@ -23,6 +25,7 @@ from app.schemas import MessageOut
from app.product_lookup import lookup
from app.schemas_shopping import (
ArticleIn,
ArticleSuggestion,
ArticleOut,
ArticleUpdateIn,
AttributeOut,
@@ -263,6 +266,85 @@ def get_articles(
return [_article_out(a) for a in rows]
SUGGEST_MONTHS = 24
SUGGEST_LIMIT = 8
SUGGEST_MIN_CHARS = 3
@router.get("/articles/suggest", response_model=list[ArticleSuggestion])
def suggest_articles(
lst: ReadableList,
db: DbSession,
q: str = Query(min_length=SUGGEST_MIN_CHARS, max_length=200),
):
"""Vorschlaege aus dem, was auf dieser Liste schon eingetragen wurde.
Quelle sind die Listeneintraege der letzten 24 Monate - auch die
bereits geloeschten, denn gerade die abgehakten und aufgeraeumten
sind die interessanten. Was jemand vor drei Jahren einmal gekauft
hat, hilft dagegen nicht mehr und wuerde die Liste nur verlaengern.
Zurueckgegeben werden die zuletzt verwendeten Angaben: Menge,
Gebinde, Eigenschaft, Markt und Warengruppe. Wer den Vorschlag
antippt, hat die Felder gefuellt und kann sie noch aendern.
"""
needle = q.strip()
if len(needle) < SUGGEST_MIN_CHARS:
return []
# LIKE-Sonderzeichen maskieren, sonst faende die Eingabe "50%" alles.
escaped = (
needle.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
)
cutoff = utcnow() - timedelta(days=SUGGEST_MONTHS * 30)
rows = db.execute(
select(ListItem, Article)
.join(Article, Article.id == ListItem.article_id)
.where(
ListItem.list_id == lst.id,
ListItem.created_at >= cutoff,
Article.deleted_at.is_(None),
Article.name.like(f"%{escaped}%", escape="\\"),
)
.order_by(ListItem.created_at.desc())
.limit(600)
).all()
# Je Artikel den juengsten Eintrag behalten und mitzaehlen, wie oft
# er vorkam - haeufig Verwendetes soll oben stehen.
best: dict[str, dict] = {}
for item, article in rows:
entry = best.get(article.id)
if entry is None:
best[article.id] = {"item": item, "article": article, "count": 1}
else:
entry["count"] += 1
def rank(entry: dict) -> tuple[int, float]:
# Erst Haeufigkeit, dann Aktualitaet.
return (-entry["count"], -entry["item"].created_at.timestamp())
out = []
for entry in sorted(best.values(), key=rank)[:SUGGEST_LIMIT]:
item, article = entry["item"], entry["article"]
out.append(
ArticleSuggestion(
article_id=article.id,
name=article.name,
count=item.count,
pack_size=item.pack_size,
pack_unit=item.pack_unit,
variant=item.variant,
market_id=item.market_id or article.default_market_id,
category_id=item.category_id or article.default_category_id,
times_used=entry["count"],
last_used_at=item.created_at,
)
)
return out
@router.get("/barcode/{barcode}", response_model=ProductLookupOut)
def resolve_barcode(barcode: str, lst: ReadableList, db: DbSession):
"""Strichcode auflösen - erst im eigenen Bestand, dann außerhalb.