"""Verleih von Einzelstücken: Bestand, offener Verleih, Fälligkeit, Historie.""" from datetime import date, timedelta import pytest from fastapi import HTTPException from sqlalchemy.exc import IntegrityError from app import crud from app.models import ( Category, CategoryTracking, ItemLoan, Location, Product, Role, User, ) from app.routers.items import create_items, get_item, list_all_items from app.routers.loans import ( delete_loan, item_loans, lend_item, list_loans, return_item, update_loan, ) from app.routers.views import location_contents from app.schemas import ItemCreate, LoanCreate, LoanReturn, LoanUpdate from app.services.stock import current_stock, location_stock_base @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 @pytest.fixture() def keller(db): ort = Location(name="Keller") db.add(ort) db.commit() db.refresh(ort) return ort @pytest.fixture() def kamera(db): cat = Category(name="Elektronik", tracking=CategoryTracking.object.value) db.add(cat) db.flush() # package_size=1 -> Artikeleinheit == Basiseinheit, macht die Menge klar. product = Product(name="Kamera", category_id=cat.id, individual=True, package_size=1) db.add(product) db.commit() db.refresh(product) return product def _stueck(db, admin, product, ort=None): return create_items( product.id, ItemCreate(count=1, location_id=(ort.id if ort else None)), db=db, _=admin, )[0] # -------------------------------------------------------------------------- # Bestand: verliehen heisst nicht im Haus # -------------------------------------------------------------------------- def test_verliehenes_stueck_zaehlt_nirgends_als_bestand(db, admin, kamera, keller): item = _stueck(db, admin, kamera, keller) assert current_stock(db, kamera.id) == 1.0 assert location_stock_base(db, kamera, keller.id) == 1.0 lend_item(item.id, LoanCreate(borrower="Markus"), db=db, user=admin) assert current_stock(db, kamera.id) == 0.0 assert location_stock_base(db, kamera, keller.id) == 0.0 # Auch die Artikel-Liste (gebuendelte Abfrage) darf es nicht mitzaehlen. assert crud.products_to_out_bulk(db, [kamera])[0].stock == 0.0 def test_lagerort_inhalt_zeigt_verliehenes_nicht(db, admin, kamera, keller): item = _stueck(db, admin, kamera, keller) inhalt = location_contents(code=keller.id, db=db, _=admin) assert [e.name for e in inhalt.products] == ["Kamera"] lend_item(item.id, LoanCreate(borrower="Markus"), db=db, user=admin) # „Was liegt hier?" wuerde sonst behaupten, die Kamera liege im Keller. assert location_contents(code=keller.id, db=db, _=admin).products == [] def test_rueckgabe_bringt_den_bestand_zurueck(db, admin, kamera, keller): item = _stueck(db, admin, kamera, keller) lend_item(item.id, LoanCreate(borrower="Markus"), db=db, user=admin) zurueck = return_item(item.id, LoanReturn(), db=db, _=admin) assert zurueck.returned_on == date.today() assert current_stock(db, kamera.id) == 1.0 # Der Lagerort stand die ganze Zeit still - dorthin gehoert es zurueck. assert location_stock_base(db, kamera, keller.id) == 1.0 def test_lagerort_bleibt_waehrend_des_verleihs_stehen(db, admin, kamera, keller): item = _stueck(db, admin, kamera, keller) lend_item(item.id, LoanCreate(borrower="Markus"), db=db, user=admin) frisch = get_item(item.id, db=db, _=admin) assert frisch.location_id == keller.id assert frisch.location_name == "Keller" # -------------------------------------------------------------------------- # Verleihen und zuruecknehmen # -------------------------------------------------------------------------- def test_verleih_merkt_name_grund_und_frist(db, admin, kamera): item = _stueck(db, admin, kamera) frist = date.today() + timedelta(days=14) loan = lend_item( item.id, LoanCreate(borrower=" Markus ", reason=" Hochzeit ", due_on=frist), db=db, user=admin, ) assert loan.borrower == "Markus" # Freitext getrimmt assert loan.reason == "Hochzeit" assert loan.lent_on == date.today() assert loan.due_on == frist assert loan.returned_on is None assert loan.username == "admin" # wer herausgegeben hat assert loan.item_uid == item.uid assert loan.product_name == "Kamera" def test_verleih_ohne_frist_ist_erlaubt(db, admin, kamera): item = _stueck(db, admin, kamera) loan = lend_item(item.id, LoanCreate(borrower="Markus"), db=db, user=admin) assert loan.due_on is None assert loan.days_overdue is None def test_item_out_zeigt_den_offenen_verleih(db, admin, kamera): item = _stueck(db, admin, kamera) lend_item(item.id, LoanCreate(borrower="Markus", reason="Ferien"), db=db, user=admin) frisch = get_item(item.id, db=db, _=admin) assert frisch.loan is not None assert frisch.loan.borrower == "Markus" assert frisch.loan.reason == "Ferien" return_item(item.id, LoanReturn(), db=db, _=admin) assert get_item(item.id, db=db, _=admin).loan is None def test_liste_aller_stuecke_zeigt_den_verleih(db, admin, kamera): item = _stueck(db, admin, kamera) lend_item(item.id, LoanCreate(borrower="Markus"), db=db, user=admin) alle = list_all_items(db=db, _=admin) assert len(alle) == 1 assert alle[0].loan is not None assert alle[0].loan.borrower == "Markus" def test_zweimal_verleihen_ohne_rueckgabe_scheitert(db, admin, kamera): item = _stueck(db, admin, kamera) lend_item(item.id, LoanCreate(borrower="Markus"), db=db, user=admin) with pytest.raises(HTTPException) as fehler: lend_item(item.id, LoanCreate(borrower="Anna"), db=db, user=admin) assert fehler.value.status_code == 409 def test_nach_rueckgabe_wieder_verleihbar(db, admin, kamera): item = _stueck(db, admin, kamera) lend_item(item.id, LoanCreate(borrower="Markus"), db=db, user=admin) return_item(item.id, LoanReturn(), db=db, _=admin) zweiter = lend_item(item.id, LoanCreate(borrower="Anna"), db=db, user=admin) assert zweiter.borrower == "Anna" assert current_stock(db, kamera.id) == 0.0 def test_rueckgabe_ohne_offenen_verleih_scheitert(db, admin, kamera): item = _stueck(db, admin, kamera) with pytest.raises(HTTPException) as fehler: return_item(item.id, LoanReturn(), db=db, _=admin) assert fehler.value.status_code == 409 def test_leerer_name_wird_abgelehnt(db, admin, kamera): item = _stueck(db, admin, kamera) with pytest.raises(HTTPException) as fehler: lend_item(item.id, LoanCreate(borrower=" "), db=db, user=admin) assert fehler.value.status_code == 400 def test_frist_vor_verleihdatum_wird_abgelehnt(db, admin, kamera): item = _stueck(db, admin, kamera) with pytest.raises(HTTPException) as fehler: lend_item( item.id, LoanCreate(borrower="Markus", due_on=date.today() - timedelta(days=1)), db=db, user=admin, ) assert fehler.value.status_code == 400 def test_rueckgabe_vor_verleihdatum_wird_abgelehnt(db, admin, kamera): item = _stueck(db, admin, kamera) lend_item(item.id, LoanCreate(borrower="Markus"), db=db, user=admin) with pytest.raises(HTTPException) as fehler: return_item( item.id, LoanReturn(returned_on=date.today() - timedelta(days=1)), db=db, _=admin, ) assert fehler.value.status_code == 400 def test_verleih_eines_unbekannten_stuecks_ist_404(db, admin): with pytest.raises(HTTPException) as fehler: lend_item(999, LoanCreate(borrower="Markus"), db=db, user=admin) assert fehler.value.status_code == 404 # -------------------------------------------------------------------------- # Faelligkeit und Uebersicht # -------------------------------------------------------------------------- def test_ueberfaellige_tage_werden_gerechnet(db, admin, kamera): item = _stueck(db, admin, kamera) lend_item( item.id, LoanCreate( borrower="Markus", lent_on=date.today() - timedelta(days=10), due_on=date.today() - timedelta(days=3), ), db=db, user=admin, ) assert get_item(item.id, db=db, _=admin).loan.days_overdue == 3 def test_frist_heute_ist_noch_nicht_ueberfaellig(db, admin, kamera): item = _stueck(db, admin, kamera) lend_item(item.id, LoanCreate(borrower="Markus", due_on=date.today()), db=db, user=admin) assert get_item(item.id, db=db, _=admin).loan.days_overdue is None def test_uebersicht_zeigt_offene_faelligste_zuerst(db, admin, kamera): spaet = _stueck(db, admin, kamera) frueh = _stueck(db, admin, kamera) ohne = _stueck(db, admin, kamera) zurueck = _stueck(db, admin, kamera) lend_item(spaet.id, LoanCreate(borrower="Spaet", due_on=date.today() + timedelta(days=20)), db=db, user=admin) lend_item(frueh.id, LoanCreate(borrower="Frueh", due_on=date.today() + timedelta(days=2)), db=db, user=admin) lend_item(ohne.id, LoanCreate(borrower="Ohne"), db=db, user=admin) lend_item(zurueck.id, LoanCreate(borrower="Zurueck"), db=db, user=admin) return_item(zurueck.id, LoanReturn(), db=db, _=admin) offen = list_loans(db=db, _=admin) # Fristen aufsteigend, Verleihe ohne Frist hinten, Zurueckgegebene gar nicht. assert [v.borrower for v in offen] == ["Frueh", "Spaet", "Ohne"] alle = list_loans(open_only=False, db=db, _=admin) assert "Zurueck" in [v.borrower for v in alle] def test_uebersicht_grenzt_auf_faellige_ein(db, admin, kamera): bald = _stueck(db, admin, kamera) spaet = _stueck(db, admin, kamera) ueberfaellig = _stueck(db, admin, kamera) ohne = _stueck(db, admin, kamera) lend_item(bald.id, LoanCreate(borrower="Bald", due_on=date.today() + timedelta(days=3)), db=db, user=admin) lend_item(spaet.id, LoanCreate(borrower="Spaet", due_on=date.today() + timedelta(days=30)), db=db, user=admin) lend_item( ueberfaellig.id, LoanCreate(borrower="Ueberfaellig", lent_on=date.today() - timedelta(days=9), due_on=date.today() - timedelta(days=2)), db=db, user=admin, ) lend_item(ohne.id, LoanCreate(borrower="Ohne"), db=db, user=admin) namen = [v.borrower for v in list_loans(due_in=7, db=db, _=admin)] # Ueberfaellige sind immer dabei, „Ohne Frist" ist nie faellig. assert namen == ["Ueberfaellig", "Bald"] # -------------------------------------------------------------------------- # Historie und Korrektur # -------------------------------------------------------------------------- def test_historie_bleibt_nach_der_rueckgabe(db, admin, kamera): item = _stueck(db, admin, kamera) lend_item(item.id, LoanCreate(borrower="Markus"), db=db, user=admin) return_item(item.id, LoanReturn(), db=db, _=admin) lend_item(item.id, LoanCreate(borrower="Anna"), db=db, user=admin) historie = item_loans(item.id, db=db, _=admin) assert {v.borrower for v in historie} == {"Markus", "Anna"} assert len(historie) == 2 def test_korrektur_aendert_name_und_frist(db, admin, kamera): item = _stueck(db, admin, kamera) loan = lend_item(item.id, LoanCreate(borrower="Markus"), db=db, user=admin) neu = date.today() + timedelta(days=5) korrigiert = update_loan( loan.id, LoanUpdate(borrower="Markus Meier", due_on=neu), db=db, _=admin ) assert korrigiert.borrower == "Markus Meier" assert korrigiert.due_on == neu def test_nachtraegliche_rueckgabe_per_korrektur(db, admin, kamera): item = _stueck(db, admin, kamera) loan = lend_item( item.id, LoanCreate(borrower="Markus", lent_on=date.today() - timedelta(days=5)), db=db, user=admin, ) gestern = date.today() - timedelta(days=1) update_loan(loan.id, LoanUpdate(returned_on=gestern), db=db, _=admin) assert get_item(item.id, db=db, _=admin).loan is None assert current_stock(db, kamera.id) == 1.0 def test_wieder_oeffnen_scheitert_bei_offenem_zweitverleih(db, admin, kamera): item = _stueck(db, admin, kamera) erster = lend_item(item.id, LoanCreate(borrower="Markus"), db=db, user=admin) return_item(item.id, LoanReturn(), db=db, _=admin) lend_item(item.id, LoanCreate(borrower="Anna"), db=db, user=admin) with pytest.raises(HTTPException) as fehler: update_loan(erster.id, LoanUpdate(returned_on=None), db=db, _=admin) assert fehler.value.status_code == 409 def test_loeschen_entfernt_den_vorgang(db, admin, kamera): item = _stueck(db, admin, kamera) loan = lend_item(item.id, LoanCreate(borrower="Markus"), db=db, user=admin) delete_loan(loan.id, db=db, _=admin) assert item_loans(item.id, db=db, _=admin) == [] assert current_stock(db, kamera.id) == 1.0 def test_datenbank_verhindert_zwei_offene_verleihe(db, admin, kamera): """Der Teilindex schuetzt die Regel auch ohne den Router-Check.""" item = _stueck(db, admin, kamera) db.add(ItemLoan(item_id=item.id, borrower="Markus", lent_on=date.today())) db.commit() db.add(ItemLoan(item_id=item.id, borrower="Anna", lent_on=date.today())) with pytest.raises(IntegrityError): db.commit() db.rollback() def test_zurueckgegebene_verleihe_duerfen_sich_haeufen(db, admin, kamera): """Der Index gilt nur fuer offene – Historie darf beliebig lang werden.""" item = _stueck(db, admin, kamera) for name in ("Markus", "Anna", "Beat"): db.add(ItemLoan(item_id=item.id, borrower=name, lent_on=date.today(), returned_on=date.today())) db.commit() assert len(item_loans(item.id, db=db, _=admin)) == 3