From c0c484cf8a9dfb4cb340d48714d4eac17a9b1ef8 Mon Sep 17 00:00:00 2001 From: patrick Date: Sat, 12 Sep 2026 23:04:38 +0200 Subject: [PATCH] feat(pruefung): INSP-002 Phase 3 - Objekttyp-Intervall als Fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prüftermine sind sicherheits-/haftungsrelevant (Nutzer-Vorgabe) - Änderung bewusst additiv statt Ersetzung: Objektposition.pruefintervall_monate bleibt Override wie bisher, NUR bei fehlendem Override greift jetzt der Objekttyp-Standard (objekttyp_pruefart) statt naechste_pruefung leer zu lassen. Kein Migrations-/Schema-Change, Regressionstest für bestehenden Pfad ergänzt. Außerdem: WH-006-Ist-Stand-Notiz korrigiert (war als "fehlt komplett" markiert, ist laut eigenem DoD seit 2026-09-10 fertig). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01VDMvneg6wKfXdXvTYNd3dN --- DEVLOG.md | 136 +++++++++++++++++++++++++ arbeitskacheln/07_warehouse.md | 12 +-- arbeitskacheln/09_inspections.md | 12 +++ backend/app/services/geraet_instanz.py | 13 ++- backend/app/services/pruefung.py | 20 ++++ backend/tests/test_geraet_instanz.py | 67 ++++++++++++ 6 files changed, 250 insertions(+), 10 deletions(-) diff --git a/DEVLOG.md b/DEVLOG.md index 72ab638..d5bc21c 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -15109,3 +15109,139 @@ Keine Commits in dieser Session. - arbeitskacheln/04_assets.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ --- +## 2026-09-12 22:43 – 22:47 (4m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +- 4a0d0fd feat(verleih): Übergabeliste zum Unterschreiben (ASSET-013 Verleih-Teil) + +### Geänderte Dateien +- DEVLOG.md | 12 ++++++ +- arbeitskacheln/04_assets.md | 29 ++++++++++---- +- backend/app/api/v1/endpoints/verleih.py | 42 ++++++++++++++++++++- +- backend/app/services/uebergabeliste.py | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/tests/test_verleih.py | 57 ++++++++++++++++++++++++++++ +- frontend/src/api/client.ts | 5 +++ +- frontend/src/pages/admin/VerleihSection.tsx | 25 +++++++++--- + +--- +## 2026-09-12 22:48 – 22:48 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 12 ++++++ +- arbeitskacheln/04_assets.md | 29 ++++++++++---- +- backend/app/api/v1/endpoints/verleih.py | 42 ++++++++++++++++++++- +- backend/app/services/uebergabeliste.py | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/tests/test_verleih.py | 57 ++++++++++++++++++++++++++++ +- frontend/src/api/client.ts | 5 +++ +- frontend/src/pages/admin/VerleihSection.tsx | 25 +++++++++--- + +--- +## 2026-09-12 22:48 – 22:48 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 12 ++++++ +- arbeitskacheln/04_assets.md | 29 ++++++++++---- +- backend/app/api/v1/endpoints/verleih.py | 42 ++++++++++++++++++++- +- backend/app/services/uebergabeliste.py | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/tests/test_verleih.py | 57 ++++++++++++++++++++++++++++ +- frontend/src/api/client.ts | 5 +++ +- frontend/src/pages/admin/VerleihSection.tsx | 25 +++++++++--- + +--- +## 2026-09-12 22:48 – 22:49 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 12 ++++++ +- arbeitskacheln/04_assets.md | 29 ++++++++++---- +- backend/app/api/v1/endpoints/verleih.py | 42 ++++++++++++++++++++- +- backend/app/services/uebergabeliste.py | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/tests/test_verleih.py | 57 ++++++++++++++++++++++++++++ +- frontend/src/api/client.ts | 5 +++ +- frontend/src/pages/admin/VerleihSection.tsx | 25 +++++++++--- + +--- +## 2026-09-12 22:50 – 22:50 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 12 ++++++ +- arbeitskacheln/04_assets.md | 29 ++++++++++---- +- backend/app/api/v1/endpoints/verleih.py | 42 ++++++++++++++++++++- +- backend/app/services/uebergabeliste.py | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/tests/test_verleih.py | 57 ++++++++++++++++++++++++++++ +- frontend/src/api/client.ts | 5 +++ +- frontend/src/pages/admin/VerleihSection.tsx | 25 +++++++++--- + +--- +## 2026-09-12 22:51 – 22:51 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 12 ++++++ +- arbeitskacheln/04_assets.md | 29 ++++++++++---- +- backend/app/api/v1/endpoints/verleih.py | 42 ++++++++++++++++++++- +- backend/app/services/uebergabeliste.py | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/tests/test_verleih.py | 57 ++++++++++++++++++++++++++++ +- frontend/src/api/client.ts | 5 +++ +- frontend/src/pages/admin/VerleihSection.tsx | 25 +++++++++--- + +--- +## 2026-09-12 22:52 – 22:52 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 12 ++++++ +- arbeitskacheln/04_assets.md | 29 ++++++++++---- +- backend/app/api/v1/endpoints/verleih.py | 42 ++++++++++++++++++++- +- backend/app/services/uebergabeliste.py | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/tests/test_verleih.py | 57 ++++++++++++++++++++++++++++ +- frontend/src/api/client.ts | 5 +++ +- frontend/src/pages/admin/VerleihSection.tsx | 25 +++++++++--- + +--- +## 2026-09-12 22:53 – 22:53 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 12 ++++++ +- arbeitskacheln/04_assets.md | 29 ++++++++++---- +- backend/app/api/v1/endpoints/verleih.py | 42 ++++++++++++++++++++- +- backend/app/services/uebergabeliste.py | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/tests/test_verleih.py | 57 ++++++++++++++++++++++++++++ +- frontend/src/api/client.ts | 5 +++ +- frontend/src/pages/admin/VerleihSection.tsx | 25 +++++++++--- + +--- diff --git a/arbeitskacheln/07_warehouse.md b/arbeitskacheln/07_warehouse.md index 145eac0..654335d 100644 --- a/arbeitskacheln/07_warehouse.md +++ b/arbeitskacheln/07_warehouse.md @@ -178,11 +178,11 @@ Details zu WH-001 … WH-007 (vollständig). --- -**MABEA-Ist-Stand-Abgleich (aktualisiert 2026-09-06):** WH-001..005+007 sind umgesetzt -— `lagerort` (Hierarchie), `lagerplatz`, `bestand` je Lagerplatz+Material, Ein-/ +**MABEA-Ist-Stand-Abgleich (aktualisiert 2026-09-12):** WH-001..007 sind alle +umgesetzt — `lagerort` (Hierarchie), `lagerplatz`, `bestand` je Lagerplatz+Material, Ein-/ Auslagerung sowie atomare Umlagerung (`POST /lagerplaetze/{id}/umlagern`) mit lückenlosem `materialbewegung`-Protokoll (eine Zeile mit Von+Nach statt zwei -Einzelbuchungen). **WH-006 (Inventur)** fehlt weiterhin komplett — MABEA hat kein -Inventurlauf-Konzept mit Korrekturbuchung, nur objektbezogene Kontrolle. -`Lagerbewegung` (Objekt-Ebene) bleibt unverändert parallel bestehen, keine Vermischung -mit dem Material-/Lagerplatz-Bewegungsprotokoll. +Einzelbuchungen). **WH-006 (Inventur) ist seit 2026-09-10 fertig** (siehe DoD oben, +diese Zusammenfassung war veraltet stehengeblieben — Einzelkachel-DoD gilt, nicht +diese Zeile). `Lagerbewegung` (Objekt-Ebene) bleibt unverändert parallel bestehen, +keine Vermischung mit dem Material-/Lagerplatz-Bewegungsprotokoll. diff --git a/arbeitskacheln/09_inspections.md b/arbeitskacheln/09_inspections.md index c0f9f3d..1953764 100644 --- a/arbeitskacheln/09_inspections.md +++ b/arbeitskacheln/09_inspections.md @@ -88,6 +88,18 @@ Details zu INSP-001 … INSP-006 (vollständig). (`geraet_instanz.py:aendere_status()`) mit Regressionsrisiko für bestehende, produktive Prüftermine — explizit als eigener, künftiger Schritt zurückgestellt, nicht Teil dieser Phase-2-Runde. + **Phase 3 (2026-09-12, Nutzer-Vorgabe: Prüftermine sind sicherheits-/ + haftungsrelevant, siehe Memory `project_pruefttermine_kritisch`):** rein + additiver Fallback statt Ersetzung — `hole_effektives_intervall_monate()` + (`app/services/pruefung.py`) nutzt `Objektposition.pruefintervall_monate` + weiterhin als Override (unverändertes Verhalten für bereits konfigurierte + Objekte), fällt NUR wenn dieser `NULL` ist auf `objekttyp_pruefart` zurück. + `aendere_status()` ruft diesen Helper jetzt statt direkt + `objektposition.pruefintervall_monate` zu lesen. Kein Migrations-/Schema- + Change. Tests: Regressionstest (Override greift weiter unverändert), + neuer Fallback-Test (Objekttyp-Standard greift bei fehlendem Override), + Nullfall-Test (weder Override noch Objekttyp-Standard → weiterhin kein + `naechste_pruefung`, wie bisher). `backend/tests/test_geraet_instanz.py`. ## INSP-003 — Prüfdurchführung/-ergebnis diff --git a/backend/app/services/geraet_instanz.py b/backend/app/services/geraet_instanz.py index 8ac616a..c53ed93 100644 --- a/backend/app/services/geraet_instanz.py +++ b/backend/app/services/geraet_instanz.py @@ -81,13 +81,18 @@ async def aendere_status( if pruefdatum is not None: instanz.pruefdatum = pruefdatum - objektposition = await db.get(Objektposition, instanz.objektposition_id) - if objektposition is not None and objektposition.pruefintervall_monate: - instanz.naechste_pruefung = _plus_monate(pruefdatum, objektposition.pruefintervall_monate) - pruefart_id = ( await db.execute(select(Pruefart.id).where(Pruefart.name == GERAETESICHERHEITSPRUEFUNG)) ).scalar_one() + + objektposition = await db.get(Objektposition, instanz.objektposition_id) + if objektposition is not None: + intervall_monate = await pruefung_service.hole_effektives_intervall_monate( + db, objektposition=objektposition, pruefart_id=pruefart_id + ) + if intervall_monate: + instanz.naechste_pruefung = _plus_monate(pruefdatum, intervall_monate) + await pruefung_service.erstelle_pruefung_fuer_geraet( db, geraet_instanz_id=instanz.id, diff --git a/backend/app/services/pruefung.py b/backend/app/services/pruefung.py index 6e7505a..1dea0d4 100644 --- a/backend/app/services/pruefung.py +++ b/backend/app/services/pruefung.py @@ -5,6 +5,8 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.geraet_instanz import GeraetInstanz +from app.models.objekt import Objekt +from app.models.objektposition import Objektposition from app.models.pruefung import ObjekttypPruefart, Pruefart, Pruefung, PruefungErgebnis from app.services.fristen import ampel_aus_tagen @@ -58,6 +60,24 @@ async def entferne_objekttyp_pruefart(db: AsyncSession, *, objekttyp_id: int, pr return True +async def hole_effektives_intervall_monate( + db: AsyncSession, *, objektposition: Objektposition, pruefart_id: int +) -> int | None: + """INSP-002 Phase 3: individuelle Objektposition-Angabe (Override) sticht + weiterhin, wie bisher - additiver Fallback auf den Objekttyp-Standard + (`objekttyp_pruefart`), NUR wenn kein Override gesetzt ist. Reine + Ergänzung, ändert das Ergebnis für bereits konfigurierte Objekte (mit + gesetztem `pruefintervall_monate`) nicht.""" + if objektposition.pruefintervall_monate is not None: + return objektposition.pruefintervall_monate + + objekt = await db.get(Objekt, objektposition.objekt_id) + if objekt is None: + return None + zuordnung = await db.get(ObjekttypPruefart, (objekt.objekttyp_id, pruefart_id)) + return zuordnung.intervall_monate if zuordnung else None + + async def erstelle_pruefung_fuer_geraet( db: AsyncSession, *, diff --git a/backend/tests/test_geraet_instanz.py b/backend/tests/test_geraet_instanz.py index 7272628..871d007 100644 --- a/backend/tests/test_geraet_instanz.py +++ b/backend/tests/test_geraet_instanz.py @@ -149,6 +149,73 @@ async def test_endpunkte_crud_und_pruefdatum(client, db_session, objekt_mit_posi assert leer.json() == [] +@pytest.mark.asyncio +async def test_naechste_pruefung_faellt_auf_objekttyp_intervall_zurueck( + client, db_session, objekt_mit_position, materialverantwortlicher_user +): + """INSP-002 Phase 3: OHNE individuelles pruefintervall_monate an der + Objektposition wird jetzt der Objekttyp-Standard (objekttyp_pruefart) + als Fallback genutzt statt naechste_pruefung einfach leer zu lassen.""" + from app.models.pruefung import GERAETESICHERHEITSPRUEFUNG, Pruefart + from app.services.pruefung import setze_objekttyp_pruefart + + objekt, material = objekt_mit_position + pruefart_id = ( + await db_session.execute(select(Pruefart.id).where(Pruefart.name == GERAETESICHERHEITSPRUEFUNG)) + ).scalar_one() + await setze_objekttyp_pruefart( + db_session, objekttyp_id=objekt.objekttyp_id, pruefart_id=pruefart_id, intervall_monate=6 + ) + await db_session.commit() + + token = await login(client, "materialverantwortlicher1") + positionen = await client.get(f"/api/v1/objekte/{objekt.id}/positionen", headers=auth_header(token)) + position_id = positionen.json()[0]["id"] + assert positionen.json()[0]["pruefintervall_monate"] is None # kein Override gesetzt + + erstellt = await client.post( + f"/api/v1/objektpositionen/{position_id}/geraete", + json={"seriennummer": "SN-FALLBACK"}, + headers=auth_header(token), + ) + geraet_id = erstellt.json()["id"] + + geprueft = await client.patch( + f"/api/v1/geraete/{geraet_id}", + json={"status": "einsatzbereit", "pruefdatum": "2026-01-15"}, + headers=auth_header(token), + ) + assert geprueft.status_code == 200 + assert geprueft.json()["naechste_pruefung"] == "2026-07-15" + + +@pytest.mark.asyncio +async def test_naechste_pruefung_bleibt_leer_ohne_override_und_ohne_objekttyp_intervall( + client, objekt_mit_position, materialverantwortlicher_user +): + """Regressionsschutz: weder Override noch Objekttyp-Standard gesetzt -> + unverändertes bisheriges Verhalten (kein naechste_pruefung).""" + objekt, material = objekt_mit_position + token = await login(client, "materialverantwortlicher1") + positionen = await client.get(f"/api/v1/objekte/{objekt.id}/positionen", headers=auth_header(token)) + position_id = positionen.json()[0]["id"] + + erstellt = await client.post( + f"/api/v1/objektpositionen/{position_id}/geraete", + json={"seriennummer": "SN-KEIN-INTERVALL"}, + headers=auth_header(token), + ) + geraet_id = erstellt.json()["id"] + + geprueft = await client.patch( + f"/api/v1/geraete/{geraet_id}", + json={"status": "einsatzbereit", "pruefdatum": "2026-01-15"}, + headers=auth_header(token), + ) + assert geprueft.status_code == 200 + assert geprueft.json()["naechste_pruefung"] is None + + @pytest.mark.asyncio async def test_defekt_kann_fehlbestand_erzeugen(client, objekt_mit_position, materialverantwortlicher_user): objekt, material = objekt_mit_position