import pytest from sqlalchemy import select from app.models.fehlbestand import Fehlbestand from app.models.kontrolle import Kontrolle, KontrollStatus, Kontrollposition from tests.conftest import auth_header, login async def _starte(client, token, objekt_id, uebernehmen=False): return await client.post( f"/api/v1/objekte/{objekt_id}/kontrollen", json={"uebernehmen": uebernehmen}, headers=auth_header(token), ) @pytest.mark.asyncio async def test_fehlbestand_entsteht_bei_abweichung(client, db_session, objekt_mit_position, mitarbeiter_user): """U1: Fehlbestand entsteht automatisch bei Ist < Soll.""" objekt, material = objekt_mit_position token = await login(client, "mitarbeiter1") start = await _starte(client, token, objekt.id) assert start.status_code == 201 kontrolle_id = start.json()["id"] put_response = await client.put( f"/api/v1/kontrollen/{kontrolle_id}/positionen/{material.id}", json={"istmenge": "8"}, headers=auth_header(token), ) assert put_response.status_code == 200 assert put_response.json()["abweichung"] is True result = await db_session.execute(select(Fehlbestand).where(Fehlbestand.kontrolle_id == kontrolle_id)) fehlbestand = result.scalar_one() assert float(fehlbestand.fehlmenge) == 2 assert fehlbestand.status.value == "offen" @pytest.mark.asyncio async def test_ueberbestand_erzeugt_keinen_fehlbestand(client, db_session, objekt_mit_position, mitarbeiter_user): objekt, material = objekt_mit_position token = await login(client, "mitarbeiter1") start = await _starte(client, token, objekt.id) kontrolle_id = start.json()["id"] put_response = await client.put( f"/api/v1/kontrollen/{kontrolle_id}/positionen/{material.id}", json={"istmenge": "12"}, headers=auth_header(token), ) assert put_response.status_code == 200 assert put_response.json()["abweichung"] is True # protokolliert... result = await db_session.execute(select(Fehlbestand).where(Fehlbestand.kontrolle_id == kontrolle_id)) assert result.scalar_one_or_none() is None # ...aber KEIN Fehlbestand (Prompt 02.4) @pytest.mark.asyncio async def test_abschluss_mit_fehlender_position_ist_409(client, objekt_mit_position, mitarbeiter_user): """U11: Abschluss mit unbestätigten Positionen -> Fehler mit Liste.""" objekt, material = objekt_mit_position token = await login(client, "mitarbeiter1") start = await _starte(client, token, objekt.id) kontrolle_id = start.json()["id"] response = await client.post( f"/api/v1/kontrollen/{kontrolle_id}/abschliessen", headers=auth_header(token) ) assert response.status_code == 409 assert material.id in response.json()["detail"]["fehlende_material_ids"] @pytest.mark.asyncio async def test_abschluss_erfolgreich_wenn_vollstaendig(client, objekt_mit_position, mitarbeiter_user): objekt, material = objekt_mit_position token = await login(client, "mitarbeiter1") start = await _starte(client, token, objekt.id) kontrolle_id = start.json()["id"] await client.put( f"/api/v1/kontrollen/{kontrolle_id}/positionen/{material.id}", json={"istmenge": "10"}, headers=auth_header(token), ) response = await client.post( f"/api/v1/kontrollen/{kontrolle_id}/abschliessen", headers=auth_header(token) ) assert response.status_code == 200 assert response.json()["status"] == "abgeschlossen" @pytest.mark.asyncio async def test_abbruch_verwirft_fehlbestand_und_positionen( client, db_session, objekt_mit_position, mitarbeiter_user ): """U12: abgebrochene Kontrolle - keine Ist-Übernahme, kein Fehlbestand, Objekt bleibt unkontrolliert (Kontrolle-Datensatz selbst bleibt aber erhalten).""" objekt, material = objekt_mit_position token = await login(client, "mitarbeiter1") start = await _starte(client, token, objekt.id) kontrolle_id = start.json()["id"] await client.put( f"/api/v1/kontrollen/{kontrolle_id}/positionen/{material.id}", json={"istmenge": "8"}, headers=auth_header(token), ) response = await client.post( f"/api/v1/kontrollen/{kontrolle_id}/abbrechen", json={"grund": "Einsatzalarmierung"}, headers=auth_header(token), ) assert response.status_code == 200 assert response.json()["status"] == "abgebrochen" fb_result = await db_session.execute(select(Fehlbestand).where(Fehlbestand.kontrolle_id == kontrolle_id)) assert fb_result.scalar_one_or_none() is None pos_result = await db_session.execute( select(Kontrollposition).where(Kontrollposition.kontrolle_id == kontrolle_id) ) assert pos_result.scalar_one_or_none() is None kontrolle = await db_session.get(Kontrolle, kontrolle_id) assert kontrolle is not None # Datensatz bleibt erhalten (Prompt 16.6) assert kontrolle.status == KontrollStatus.abgebrochen @pytest.mark.asyncio async def test_objekt_sperre_zweiter_zugriff_ist_409( client, objekt_mit_position, mitarbeiter_user, mitarbeiter2_user ): objekt, _material = objekt_mit_position token1 = await login(client, "mitarbeiter1") token2 = await login(client, "mitarbeiter2") erster = await _starte(client, token1, objekt.id) assert erster.status_code == 201 zweiter = await _starte(client, token2, objekt.id) assert zweiter.status_code == 409 assert zweiter.json()["detail"]["benutzer"] == "Test Mitarbeiter" @pytest.mark.asyncio async def test_uebernahme_beendet_alte_kontrolle_und_verwirft_ihren_fehlbestand( client, db_session, objekt_mit_position, mitarbeiter_user, mitarbeiter2_user ): objekt, material = objekt_mit_position token1 = await login(client, "mitarbeiter1") token2 = await login(client, "mitarbeiter2") erster = await _starte(client, token1, objekt.id) alte_kontrolle_id = erster.json()["id"] await client.put( f"/api/v1/kontrollen/{alte_kontrolle_id}/positionen/{material.id}", json={"istmenge": "5"}, headers=auth_header(token1), ) uebernahme = await _starte(client, token2, objekt.id, uebernehmen=True) assert uebernahme.status_code == 201 neue_kontrolle_id = uebernahme.json()["id"] assert neue_kontrolle_id != alte_kontrolle_id alte_kontrolle = await db_session.get(Kontrolle, alte_kontrolle_id) assert alte_kontrolle.status == KontrollStatus.abgebrochen fb_result = await db_session.execute( select(Fehlbestand).where(Fehlbestand.kontrolle_id == alte_kontrolle_id) ) assert fb_result.scalar_one_or_none() is None @pytest.mark.asyncio async def test_fremder_benutzer_kann_position_nicht_erfassen( client, objekt_mit_position, mitarbeiter_user, mitarbeiter2_user ): objekt, material = objekt_mit_position token1 = await login(client, "mitarbeiter1") token2 = await login(client, "mitarbeiter2") start = await _starte(client, token1, objekt.id) kontrolle_id = start.json()["id"] response = await client.put( f"/api/v1/kontrollen/{kontrolle_id}/positionen/{material.id}", json={"istmenge": "5"}, headers=auth_header(token2), ) assert response.status_code == 403 @pytest.mark.asyncio async def test_verschiedene_objekte_parallel_kontrollierbar( client, db_session, hauptserver_id, objekt_mit_position, standort_factory, objekttyp_factory, material_factory, mitarbeiter_user, mitarbeiter2_user, ): """Sperre gilt pro Objekt, nicht global (Prompt 02.8 Klarstellung).""" from app.schemas.vorlage import VorlagenpositionCreate from app.services.objekte import objekt_aus_vorlage from app.services.vorlagen import erstelle_vorlage objekt_a, _material_a = objekt_mit_position token1 = await login(client, "mitarbeiter1") token2 = await login(client, "mitarbeiter2") objekttyp_b = await objekttyp_factory("Objekttyp-B") standort_b = await standort_factory("Standort-B") material_b = await material_factory("Verbandspäckchen") vorlage_b = await erstelle_vorlage( db_session, objekttyp_id=objekttyp_b.id, name="Vorlage-B", positionen=[VorlagenpositionCreate(material_id=material_b.id, sollmenge=5)], ) objekt_b = await objekt_aus_vorlage( db_session, code="OB-01", name="Objekt B", standort_id=standort_b.id, vorlage=vorlage_b, zustaendiger_server_id=hauptserver_id, ) erster = await _starte(client, token1, objekt_a.id) assert erster.status_code == 201 zweiter = await _starte(client, token2, objekt_b.id) assert zweiter.status_code == 201