Gruppen-Codes: Dublette behoben und Altbestand nachgezogen

Zwei Fehler in der EAN-Liste einer Gruppe, beide gemeldet und nachgestellt.

Derselbe Code stand zweimal in der Liste: einmal schreibgeschuetzt mit der
Kennzeichnung "Artikel", einmal darunter mit Notizfeld. Grund war, dass
_group_to_out die Codes der Artikel unabhaengig von den Gruppen-Codes
zusammengestellt hat - seit die Zuordnung automatisch einen Gruppen-Code
anlegt, trifft beides auf denselben Code zu. Die schreibgeschuetzte Zeile stand
oben, deshalb war das Notizfeld darunter leicht zu uebersehen.

Jetzt gibt es eine Zeile je Code. Der Gruppen-Code fuehrt den Artikel mit, ueber
den er dazugehoert (neues Feld product_name in BarcodeOut), zeigt weiterhin die
Kennzeichnung "Artikel" - und hat trotzdem ein Notizfeld. Der Muelleimer
entfaellt bei diesen Codes, denn sie kaemen beim naechsten Speichern des
Artikels sofort zurueck; dafuer muss der Artikel die Gruppe wechseln.

Zweitens fehlte fuer bestehende Daten der Code ganz. Die automatische Pflege
greift nur beim Anlegen und Aendern eines Artikels; Zuordnungen, die es vorher
schon gab, hatten nie einen Gruppen-Code bekommen. In der Verwaltung stand der
Code deshalb ausschliesslich als schreibgeschuetzte Artikel-Zeile - genau die
Stelle, an der sich keine Notiz hinterlegen liess. Neu holt backfill() das beim
Start nach: fuer jeden Artikel mit Gruppe und Barcode wird der Gruppen-Code
angelegt, sofern er fehlt. Gefahrlos wiederholbar.

Getestet: 58 pytest-Tests gruen, einer neu (Backfill legt den fehlenden Code an
und beim zweiten Lauf nichts doppelt). Der gemeldete Fall wurde vorher gegen die
laufende API nachgestellt - Altbestand ohne Gruppen-Code und ein doppelt
gelisteter Code nach einer Neuanlage - und danach als behoben bestaetigt: eine
Zeile je Code, mit Artikelnamen und Notizfeld. Web-Build laeuft durch.
Die Oberflaeche habe ich nicht selbst bedient.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-22 21:41:43 +02:00
parent dfbc62398d
commit 5293810529
8 changed files with 102 additions and 14 deletions

View File

@@ -23,6 +23,7 @@ from .routers import (
views,
)
from .seed import ensure_builtin_categories, ensure_builtin_units, ensure_first_admin
from .services.group_codes import backfill as backfill_group_codes
settings = get_settings()
@@ -69,6 +70,8 @@ async def lifespan(app: FastAPI):
ensure_builtin_units(db)
ensure_builtin_categories(db)
ensure_first_admin(db)
# Codes bestehender Gruppen-Zuordnungen nachziehen.
backfill_group_codes(db)
finally:
db.close()
yield

View File

@@ -23,21 +23,36 @@ def _group_to_out(db: Session, group: Group) -> GroupOut:
out = GroupOut.model_validate(group)
products = db.query(Product).filter(Product.group_id == group.id).all()
out.product_count = len(products)
out.barcodes = [
BarcodeOut.model_validate(b)
for b in db.query(Barcode).filter(Barcode.group_id == group.id).order_by(Barcode.id).all()
]
# Codes der Artikel in dieser Gruppe mitliefern. Sie waren bisher nirgends
# sichtbar, wodurch eine Kategorie mit Artikeln "0 EANs" anzeigte.
product_codes: list[ProductBarcodeOut] = []
# Zu welchem Artikel gehoert ein Code? Der Gruppen-Code entsteht beim
# Zuordnen automatisch; die Herkunft soll trotzdem sichtbar bleiben.
name_zu_code: dict[str, str] = {}
for product in products:
if product.barcode:
name_zu_code[product.barcode] = product.name
for alias in db.query(Barcode).filter(Barcode.product_id == product.id):
name_zu_code[alias.code] = product.name
out.barcodes = []
for b in db.query(Barcode).filter(Barcode.group_id == group.id).order_by(Barcode.id).all():
eintrag = BarcodeOut.model_validate(b)
eintrag.product_name = name_zu_code.get(b.code)
out.barcodes.append(eintrag)
# Codes der Artikel in dieser Gruppe mitliefern. Sie waren bisher nirgends
# sichtbar, wodurch eine Gruppe mit Artikeln "0 EANs" anzeigte.
# Was schon als Gruppen-Code gefuehrt wird, hier ueberspringen - sonst
# steht derselbe Code zweimal in der Liste, einmal davon schreibgeschuetzt.
schon_da = {b.code for b in out.barcodes}
product_codes: list[ProductBarcodeOut] = []
for product in products:
if product.barcode and product.barcode not in schon_da:
product_codes.append(
ProductBarcodeOut(
code=product.barcode, product_id=product.id, product_name=product.name
)
)
for alias in db.query(Barcode).filter(Barcode.product_id == product.id).order_by(Barcode.id):
if alias.code in schon_da:
continue
product_codes.append(
ProductBarcodeOut(
code=alias.code, product_id=product.id, product_name=product.name

View File

@@ -46,6 +46,8 @@ class BarcodeOut(BaseModel):
id: int
code: str
note: str | None = None
# Nur bei Gruppen-Codes gefüllt: der Artikel, über den der Code hier steht.
product_name: str | None = None
class ProductBarcodeOut(BaseModel):

View File

@@ -64,6 +64,28 @@ def sync(db: Session, product: Product) -> None:
db.add(Barcode(code=code, group_id=product.group_id))
def backfill(db: Session) -> int:
"""Holt die Codes bestehender Zuordnungen nach.
``sync`` greift nur beim Anlegen und Ändern eines Artikels. Zuordnungen, die
es vor dieser Funktion schon gab, hätten sonst nie einen Gruppen-Code
bekommen: In der Verwaltung stand der Code dann ausschließlich als
schreibgeschützte Artikel-Zeile, ohne Möglichkeit für eine Notiz.
Läuft beim Start und ist gefahrlos wiederholbar.
"""
betroffen = (
db.query(Product)
.filter(Product.group_id.isnot(None), Product.barcode.isnot(None))
.all()
)
vorher = db.query(Barcode).filter(Barcode.group_id.isnot(None)).count()
for product in betroffen:
sync(db, product)
db.commit()
return db.query(Barcode).filter(Barcode.group_id.isnot(None)).count() - vorher
def detach(db: Session, product: Product) -> None:
"""Vor dem Löschen eines Artikels dessen Gruppen-Code aufräumen."""
code = (product.barcode or "").strip()

View File

@@ -171,3 +171,22 @@ def test_fremder_alias_wird_nicht_ueberschrieben(db):
assert _gruppen_codes(db, mehl) == set()
assert db.query(Barcode).filter(Barcode.code == "4001").one().product_id == anderer.id
def test_backfill_holt_bestehende_zuordnungen_nach(db):
"""Zuordnungen von vor der automatischen Codepflege bekommen ihren Code."""
from app.services.group_codes import backfill
mehl = Group(name="Mehl")
db.add(mehl)
db.commit()
# Bewusst ohne sync() angelegt - so sah der Altbestand aus.
db.add(Product(name="Aldi Mehl", barcode="4001", group_id=mehl.id))
db.commit()
assert _gruppen_codes(db, mehl) == set()
assert backfill(db) == 1
assert _gruppen_codes(db, mehl) == {"4001"}
# Wiederholtes Ausfuehren legt nichts doppelt an.
assert backfill(db) == 0
assert _gruppen_codes(db, mehl) == {"4001"}