feat(material): Material-Dubletten zusammenführen
CI / backend-tests (push) Failing after 2m6s
CI / frontend-build (push) Successful in 17s

Verwendung je Material einsehbar (Objektposition/Vorlagenposition/
Bestand/Kontrollposition/Fehlbestand/Ausgabe/Nachfüllung/
Materialbewegung), POST /materialien/{id}/ersetzen hängt alle
Referenzen auf ein Ziel-Material um und löscht die Quelle. Bricht bei
Konflikten ab (z.B. dasselbe Objekt hätte danach zwei Positionen für
dasselbe Material) statt Daten stillschweigend zu verlieren. Admin-UI:
"Zusammenführen" je Material zeigt Verwendung + Ziel-Auswahl.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-07 00:23:35 +02:00
co-authored by Claude Sonnet 5
parent 5f568c365c
commit 06baf03708
5 changed files with 306 additions and 13 deletions
+84
View File
@@ -65,6 +65,90 @@ async def test_material_not_found(client, admin_user):
assert response.status_code == 404
@pytest.mark.asyncio
async def test_material_zusammenfuehren_ohne_konflikt(client, db_session, admin_user, objekt_mit_position):
"""Import-Dublette: quelle_id verschwindet, Referenzen zeigen auf ziel_material_id."""
from app.models.fehlbestand import Fehlbestand, FehlbestandStatus
from datetime import datetime, timezone
objekt, material_ziel = objekt_mit_position
token = await login(client, "admin1")
quelle = await client.post(
"/api/v1/materialien",
json={"name": "Kompresse (Dublette)", "einheit": "Stück", "materialtyp": "standard"},
headers=auth_header(token),
)
quelle_id = quelle.json()["id"]
db_session.add(
Fehlbestand(
erzeugt_von_server_id=objekt.zustaendiger_server_id,
objekt_id=objekt.id,
material_id=quelle_id,
standort_id=objekt.standort_id,
sollmenge=5,
istmenge=0,
fehlmenge=5,
entstanden_am=datetime.now(timezone.utc),
festgestellt_von=admin_user.id,
status=FehlbestandStatus.offen,
)
)
await db_session.flush()
verwendung = await client.get(f"/api/v1/materialien/{quelle_id}/verwendung", headers=auth_header(token))
assert verwendung.status_code == 200
assert verwendung.json()["fehlbestand"] == 1
ergebnis = await client.post(
f"/api/v1/materialien/{quelle_id}/ersetzen",
json={"ziel_material_id": material_ziel.id},
headers=auth_header(token),
)
assert ergebnis.status_code == 200
assert ergebnis.json()["erfolgreich"] is True
assert ergebnis.json()["konflikte"] == []
geloescht = await client.get(f"/api/v1/materialien/{quelle_id}", headers=auth_header(token))
assert geloescht.status_code == 404
fb = await db_session.execute(
select(Fehlbestand).where(Fehlbestand.objekt_id == objekt.id, Fehlbestand.sollmenge == 5)
)
assert fb.scalar_one().material_id == material_ziel.id
@pytest.mark.asyncio
async def test_material_zusammenfuehren_mit_konflikt_wird_abgelehnt(
client, db_session, admin_user, objekt_mit_position, material_factory
):
"""Beide Materialien haben eine Position am selben Objekt - Merge würde die
UNIQUE(objekt_id, material_id)-Regel verletzen, muss also abgelehnt werden."""
from app.models.objektposition import Objektposition
objekt, material_ziel = objekt_mit_position
token = await login(client, "admin1")
material_quelle = await material_factory("Kompresse (Konflikt)")
db_session.add(
Objektposition(objekt_id=objekt.id, material_id=material_quelle.id, sollmenge_vorlage=1, istmenge=0)
)
await db_session.flush()
ergebnis = await client.post(
f"/api/v1/materialien/{material_quelle.id}/ersetzen",
json={"ziel_material_id": material_ziel.id},
headers=auth_header(token),
)
assert ergebnis.status_code == 200
assert ergebnis.json()["erfolgreich"] is False
assert len(ergebnis.json()["konflikte"]) == 1
weiterhin_da = await client.get(f"/api/v1/materialien/{material_quelle.id}", headers=auth_header(token))
assert weiterhin_da.status_code == 200
@pytest.mark.asyncio
async def test_admin_can_update_bereich(client, admin_user):
token = await login(client, "admin1")