"""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