feat(material): Material-Dubletten zusammenführen
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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user