neue Archivfunktion und Optimierung an Artikelbearbeitung
This commit is contained in:
137
backend/app/article_ops.py
Normal file
137
backend/app/article_ops.py
Normal 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
|
||||
Reference in New Issue
Block a user