Zwei Luecken, die zusammen den Eindruck machten, ein angelegtes Einzelstueck
sei verschwunden.
Der Verlauf kannte nur das Verschwinden. remove_item schreibt seit jeher eine
Bewegung, create_items nicht - Abgaenge von Einzelstuecken tauchten also auf,
Zugaenge nie, im Verlauf wie im Dashboard. Jetzt schreibt das Anlegen je Stueck
eine in-Bewegung mit UID (oder der eigenen Notiz, falls angegeben), Lagerort und
Benutzer, spiegelbildlich zum Entfernen. Der Bestand bleibt unberuehrt: er
zaehlt bei Einzelstuecken die Zeilen in items und rechnet nicht aus Bewegungen.
Und der Weg dorthin fehlte. ProductFormView liefert den angelegten Artikel, aber
zwei der drei Aufrufer warfen ihn weg ("{ _ in showNew = false }") - nur der
Einlagern-Weg nutzte ihn. "Einzelstuecke anlegen" steckt ausschliesslich in der
Artikelansicht, die man vom Scan-Tab und aus den Listen nach dem Anlegen gar
nicht erreicht. Man musste den frischen Artikel von Hand wiederfinden.
Bei Gegenstaenden mit eigener UID je Stueck oeffnet sich jetzt direkt das
Anlege-Formular. Gesetzt wird das im onDismiss des Artikelformulars: beides im
selben Zug zu setzen verschluckt SwiftUI, weil das zweite Sheet erscheinen soll,
waehrend das erste noch verschwindet.
Der Parameter von create_items heisst dadurch user statt _; die drei Testaufrufe
sind mitgezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
131 lines
4.3 KiB
Python
131 lines
4.3 KiB
Python
"""Einzelstücke: Kaufpreis und Belege (Metadaten in der Ausgabe, Dateinamen-Härtung)."""
|
|
|
|
import pytest
|
|
|
|
from app.models import (
|
|
Category,
|
|
CategoryTracking,
|
|
Item,
|
|
ItemDocument,
|
|
Location,
|
|
Movement,
|
|
MovementType,
|
|
Product,
|
|
Role,
|
|
User,
|
|
)
|
|
from app.routers.items import _safe_filename, create_items, get_item, list_all_items
|
|
from app.schemas import ItemCreate
|
|
from app.services.stock import current_stock
|
|
|
|
|
|
@pytest.fixture()
|
|
def admin(db):
|
|
person = User(username="admin", password_hash="x", role=Role.admin)
|
|
db.add(person)
|
|
db.commit()
|
|
db.refresh(person)
|
|
return person
|
|
|
|
|
|
def _product(db):
|
|
cat = Category(name="Elektronik", tracking=CategoryTracking.object.value)
|
|
db.add(cat)
|
|
db.flush()
|
|
product = Product(name="Kamera", category_id=cat.id, individual=True)
|
|
db.add(product)
|
|
db.commit()
|
|
db.refresh(product)
|
|
return product
|
|
|
|
|
|
def test_item_kaufpreis_round_trip(db, admin):
|
|
product = _product(db)
|
|
out = create_items(
|
|
product.id, ItemCreate(count=1, price_cents=49900, currency="CHF"), db=db, user=admin
|
|
)
|
|
assert out[0].price_cents == 49900
|
|
assert out[0].currency == "CHF"
|
|
|
|
|
|
def test_item_out_listet_belege(db, admin):
|
|
product = _product(db)
|
|
item_id = create_items(product.id, ItemCreate(count=1), db=db, user=admin)[0].id
|
|
db.add(ItemDocument(item_id=item_id, filename="rechnung.pdf",
|
|
content_type="application/pdf", data=b"%PDF-1.4 x"))
|
|
db.commit()
|
|
|
|
fresh = get_item(item_id, db=db, _=admin)
|
|
assert len(fresh.documents) == 1
|
|
assert fresh.documents[0].filename == "rechnung.pdf"
|
|
assert fresh.documents[0].content_type == "application/pdf"
|
|
|
|
|
|
def test_list_all_items_angereichert(db, admin):
|
|
cat = Category(name="Elektronik", tracking=CategoryTracking.object.value)
|
|
db.add(cat)
|
|
db.flush()
|
|
product = Product(name="Kamera", brand="Sony", category_id=cat.id, individual=True)
|
|
db.add(product)
|
|
db.flush()
|
|
ort = Location(name="Keller")
|
|
db.add(ort)
|
|
db.commit()
|
|
create_items(product.id, ItemCreate(count=2, location_id=ort.id), db=db, user=admin)
|
|
|
|
alle = list_all_items(db=db, _=admin)
|
|
assert len(alle) == 2
|
|
e = alle[0]
|
|
assert e.product_name == "Kamera"
|
|
assert e.product_brand == "Sony"
|
|
assert e.category_name == "Elektronik"
|
|
assert e.location_id == ort.id
|
|
assert e.location_name == "Keller"
|
|
|
|
|
|
def test_safe_filename_entfernt_header_zeichen():
|
|
assert _safe_filename('a"b\r\nc.pdf') == "abc.pdf"
|
|
assert _safe_filename(" ") == "beleg"
|
|
assert _safe_filename(None) == "beleg"
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Verlauf: Anlegen und Entfernen muessen beide auftauchen
|
|
# --------------------------------------------------------------------------
|
|
def test_anlegen_schreibt_bewegung_je_stueck(db, admin):
|
|
"""Frueher kannte der Verlauf nur das Verschwinden, nie das Entstehen."""
|
|
product = _product(db)
|
|
ort = Location(name="Keller")
|
|
db.add(ort)
|
|
db.commit()
|
|
|
|
create_items(product.id, ItemCreate(count=3, location_id=ort.id), db=db, user=admin)
|
|
|
|
bewegungen = db.query(Movement).filter(Movement.product_id == product.id).all()
|
|
assert len(bewegungen) == 3
|
|
for b in bewegungen:
|
|
assert b.type == MovementType.in_
|
|
assert b.quantity == 1
|
|
assert b.unit_used == "Stück"
|
|
assert b.location_id == ort.id
|
|
assert b.user_id == admin.id
|
|
# Die UID gehoert in den Eintrag, sonst sagt der Verlauf nicht, welches Stueck.
|
|
uids = {i.uid for i in db.query(Item).filter(Item.product_id == product.id)}
|
|
assert {b.note.removeprefix("Einzelstück ") for b in bewegungen} == uids
|
|
|
|
|
|
def test_anlegen_uebernimmt_die_notiz_als_grund(db, admin):
|
|
"""Eine eigene Notiz ist aussagekraeftiger als die UID - sie gewinnt."""
|
|
product = _product(db)
|
|
create_items(product.id, ItemCreate(count=1, note="Geschenk von Oma"), db=db, user=admin)
|
|
|
|
bewegung = db.query(Movement).filter(Movement.product_id == product.id).one()
|
|
assert bewegung.note == "Geschenk von Oma"
|
|
|
|
|
|
def test_anlegen_veraendert_den_bestand_nicht_doppelt(db, admin):
|
|
"""Der Bestand zaehlt Zeilen in items - die Bewegung darf nichts dazutun."""
|
|
product = _product(db)
|
|
create_items(product.id, ItemCreate(count=2), db=db, user=admin)
|
|
assert current_stock(db, product.id) == 2.0
|