138 lines
4.2 KiB
Python
138 lines
4.2 KiB
Python
"""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
|