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

137
backend/app/article_ops.py Normal file
View File

@@ -0,0 +1,137 @@
"""Artikelwechsel an einem Listeneintrag.
Der Name eines Eintrags gehört nicht dem Eintrag, sondern dem Artikel
dahinter - und der kann von mehreren Einträgen benutzt werden, trägt
einen Strichcode, Vorgaben für Markt und Warengruppe und hängt an der
Preishistorie. Ein Namenswechsel am Eintrag ist deshalb mehrdeutig:
"Mlich" -> "Milch" ein Tippfehler soll verschwinden
"Milch" -> "Butter" dieser Eintrag soll etwas anderes sein
Die Unterscheidung trifft diese Datei nach einer einfachen Regel:
Benutzt außer diesem Eintrag niemand den Artikel, wird er umbenannt -
der Tippfehler verschwindet dann restlos. Sonst wird der Eintrag auf
einen anderen Artikel umgehängt, und die übrigen Einträge bleiben
unberührt.
Wer einen Artikel ausdrücklich überall umbenennen will, tut das unter
"Artikel" - dort ist der Name ein Feld wie jedes andere.
"""
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.models import (
Article,
ArticleAttribute,
ListItem,
PricePoint,
ShoppingList,
)
from app.security import utcnow
def _other_item_count(db: Session, article_id: str, except_item_id: str) -> int:
return db.scalar(
select(func.count())
.select_from(ListItem)
.where(
ListItem.article_id == article_id,
ListItem.id != except_item_id,
ListItem.deleted_at.is_(None),
)
) or 0
def _is_disposable(db: Session, article: Article) -> bool:
"""Darf der Artikel verschwinden, wenn ihn niemand mehr benutzt?
Nur wenn nichts daran hängt, was jemand bewusst gepflegt hat:
kein Strichcode, keine Eigenschaften, keine Vorgaben, keine
beobachteten Preise. Sonst bleibt er im Bestand - dort stört er
nicht und lässt sich von Hand löschen.
"""
if article.barcode or article.note:
return False
if article.default_market_id or article.default_category_id:
return False
has_attributes = db.scalar(
select(func.count())
.select_from(ArticleAttribute)
.where(ArticleAttribute.article_id == article.id)
)
if has_attributes:
return False
has_prices = db.scalar(
select(func.count())
.select_from(PricePoint)
.where(PricePoint.article_id == article.id)
)
return not has_prices
def change_item_article(
db: Session, lst: ShoppingList, item: ListItem, new_name: str, rev: int
) -> str:
"""Setzt den Artikel eines Eintrags anhand eines Namens.
@returns die ID des jetzt zugeordneten Artikels
@raises ValueError bei leerem Namen
"""
name = (new_name or "").strip()[:200]
if not name:
raise ValueError("Artikelname fehlt")
current = db.get(Article, item.article_id)
if current is not None and current.name == name:
return current.id
# Vergleich unabhängig von Groß- und Kleinschreibung: Die Spalte
# steht auf utf8mb4_unicode_ci, die Datenbank erledigt das.
existing = db.scalar(
select(Article).where(
Article.list_id == lst.id,
Article.name == name,
Article.deleted_at.is_(None),
)
)
if existing is not None and existing.id == item.article_id:
return existing.id
if existing is None and current is not None and _other_item_count(
db, current.id, item.id
) == 0:
# Niemand sonst benutzt ihn: umbenennen. Damit verschwindet ein
# Tippfehler restlos, statt einen unbrauchbaren Artikel im
# Bestand zurückzulassen.
current.name = name
current.row_rev = rev
db.flush()
return current.id
if existing is not None:
target = existing
else:
target = Article(list_id=lst.id, name=name, row_rev=rev)
db.add(target)
db.flush()
item.article_id = target.id
item.row_rev = rev
db.flush()
# Bleibt der bisherige Artikel ohne Einträge zurück und hängt nichts
# Gepflegtes daran, verschwindet er mit.
if (
current is not None
and current.id != target.id
and _other_item_count(db, current.id, item.id) == 0
and _is_disposable(db, current)
):
current.deleted_at = utcnow()
current.row_rev = rev
return target.id