feat(fach): Duplikate case-insensitiv verhindern, Verwendung anzeigen und ersetzen/zusammenführen
Anlegen prüft jetzt case-insensitiv auf Duplikate (409 statt "Fach A"
neben "fach a"). Neuer Endpunkt GET /faecher/{id}/objekte zeigt, welche
Objekte ein Fach über ihre Vorlage tatsächlich nutzen. POST
/faecher/{id}/ersetzen benennt um und zieht alle Vorlagenpositionen
mit; landet der neue Name auf einem bereits bestehenden Fach, werden
beide zusammengeführt. Frontend: Fach anklicken öffnet Verwendung +
Ersetzen-Eingabe direkt in der Fächer-Verwaltung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.models.stammdaten import Fach
|
||||
from app.models.vorlage import Vorlagenposition
|
||||
from tests.conftest import auth_header, login
|
||||
|
||||
|
||||
@@ -160,6 +163,105 @@ async def test_fach_lifecycle(client, admin_user):
|
||||
assert not any(f["id"] == fach_id for f in liste_nach_delete.json())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fach_duplikat_case_insensitiv_ist_409(client, admin_user):
|
||||
token = await login(client, "admin1")
|
||||
bereich = await client.post("/api/v1/bereiche", json={"name": "B-Dup"}, headers=auth_header(token))
|
||||
objekttyp = await client.post(
|
||||
"/api/v1/objekttypen",
|
||||
json={"bereich_id": bereich.json()["id"], "name": "Typ-Dup"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
objekttyp_id = objekttyp.json()["id"]
|
||||
|
||||
erster = await client.post(
|
||||
"/api/v1/faecher", json={"objekttyp_id": objekttyp_id, "name": "Hauptfach"}, headers=auth_header(token)
|
||||
)
|
||||
assert erster.status_code == 201
|
||||
|
||||
zweiter = await client.post(
|
||||
"/api/v1/faecher", json={"objekttyp_id": objekttyp_id, "name": "hauptfach"}, headers=auth_header(token)
|
||||
)
|
||||
assert zweiter.status_code == 409
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fach_verwendung_und_ersetzen_fuehrt_zusammen(
|
||||
client, db_session, admin_user, hauptserver_id, standort_factory
|
||||
):
|
||||
from app.schemas.vorlage import VorlagenpositionCreate
|
||||
from app.services.objekte import objekt_aus_vorlage
|
||||
from app.services.vorlagen import erstelle_vorlage
|
||||
|
||||
token = await login(client, "admin1")
|
||||
bereich = await client.post("/api/v1/bereiche", json={"name": "B-Merge"}, headers=auth_header(token))
|
||||
objekttyp = await client.post(
|
||||
"/api/v1/objekttypen",
|
||||
json={"bereich_id": bereich.json()["id"], "name": "Typ-Merge"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
objekttyp_id = objekttyp.json()["id"]
|
||||
material = await client.post(
|
||||
"/api/v1/materialien",
|
||||
json={"name": "Material-Merge", "einheit": "Stück", "materialtyp": "standard"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
material_id = material.json()["id"]
|
||||
|
||||
vorlage = await erstelle_vorlage(
|
||||
db_session,
|
||||
objekttyp_id=objekttyp_id,
|
||||
name="Vorlage-Merge",
|
||||
positionen=[VorlagenpositionCreate(material_id=material_id, sollmenge=1, fach="Reizverschlussfach gross")],
|
||||
)
|
||||
standort = await standort_factory("Standort-Merge")
|
||||
objekt = await objekt_aus_vorlage(
|
||||
db_session,
|
||||
code="MERGE-01",
|
||||
name="Objekt-Merge",
|
||||
standort_id=standort.id,
|
||||
vorlage=vorlage,
|
||||
zustaendiger_server_id=hauptserver_id,
|
||||
)
|
||||
|
||||
ziel_fach = await client.post(
|
||||
"/api/v1/faecher",
|
||||
json={"objekttyp_id": objekttyp_id, "name": "Reizverschlussfach Gross"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
quell_fach = await client.post(
|
||||
"/api/v1/faecher",
|
||||
json={"objekttyp_id": objekttyp_id, "name": "reizverschlussfach gross tmp"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
# Feld manuell auf den echten (klein geschriebenen) Namen zurücksetzen, um
|
||||
# die reale Dublette nachzustellen (Anlegen selbst blockt Duplikate ab).
|
||||
quell = (await db_session.execute(select(Fach).where(Fach.id == quell_fach.json()["id"]))).scalar_one()
|
||||
quell.name = "reizverschlussfach gross"
|
||||
await db_session.flush()
|
||||
|
||||
verwendung = await client.get(f"/api/v1/faecher/{quell.id}/objekte", headers=auth_header(token))
|
||||
assert verwendung.status_code == 200
|
||||
assert any(o["id"] == objekt.id for o in verwendung.json())
|
||||
|
||||
ersetzt = await client.post(
|
||||
f"/api/v1/faecher/{quell.id}/ersetzen",
|
||||
json={"neuer_name": "Reizverschlussfach Gross"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert ersetzt.status_code == 200
|
||||
assert ersetzt.json()["id"] == ziel_fach.json()["id"]
|
||||
|
||||
liste = await client.get("/api/v1/faecher", headers=auth_header(token))
|
||||
verbleibende = [f for f in liste.json() if f["objekttyp_id"] == objekttyp_id]
|
||||
assert len(verbleibende) == 1
|
||||
|
||||
position = await db_session.execute(
|
||||
select(Vorlagenposition).where(Vorlagenposition.vorlage_id == vorlage.id)
|
||||
)
|
||||
assert position.scalar_one().fach == "Reizverschlussfach Gross"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mitarbeiter_cannot_create_fach(client, mitarbeiter_user, admin_user, db_session):
|
||||
from app.models.stammdaten import Bereich, Objekttyp
|
||||
|
||||
Reference in New Issue
Block a user