Refactor: große Dateien in kleinere Module/Komponenten aufgeteilt
Explizite Vorgabe: keine wachsenden Monolith-Dateien. AdminPage.tsx (349
Zeilen) in pages/admin/{Standort,Material,Objekt}Section.tsx aufgeteilt,
KontrollPage.tsx (277 Zeilen) in pages/kontrolle/{useKontrolle,
PositionCard,StatusBadge,types} aufgeteilt, services/kontrolle.py (332
Zeilen) in ein Package (lifecycle/erfassung/exceptions) aufgeteilt -
öffentliche API bleibt über __init__.py unverändert re-exportiert, kein
bestehender Import musste angepasst werden. Reines Refactoring, keine
Verhaltensänderung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.fehlbestand import Fehlbestand
|
||||
from app.models.kontrolle import Kontrolle, KontrollStatus, Kontrollposition
|
||||
from app.models.mindermenge import MindermengeStatus, MindermengenGenehmigung
|
||||
from app.models.objektposition import Objektposition, ObjektpositionStatus
|
||||
from app.services import historie as historie_service
|
||||
|
||||
from .exceptions import ObjektGesperrtError, UnvollstaendigError
|
||||
|
||||
|
||||
async def aktive_kontrolle(db: AsyncSession, objekt_id: int) -> Kontrolle | None:
|
||||
result = await db.execute(
|
||||
select(Kontrolle).where(
|
||||
Kontrolle.objekt_id == objekt_id, Kontrolle.status == KontrollStatus.in_bearbeitung
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def starte_kontrolle(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
objekt_id: int,
|
||||
benutzer_id: int,
|
||||
uebernehmen: bool,
|
||||
zustaendiger_server_id: int,
|
||||
) -> Kontrolle:
|
||||
laufende = await aktive_kontrolle(db, objekt_id)
|
||||
if laufende is not None:
|
||||
if not uebernehmen:
|
||||
raise ObjektGesperrtError(laufende)
|
||||
# Übernahme (Prompt 02.8): alte Kontrolle wird abgebrochen, nicht gelöscht -
|
||||
# bleibt als Datensatz nachvollziehbar (Prompt 16.6-Analogie).
|
||||
laufende.status = KontrollStatus.abgebrochen
|
||||
laufende.abbruch_grund = f"Übernommen durch Benutzer {benutzer_id}"
|
||||
laufende.beendet_am = datetime.now(timezone.utc)
|
||||
# Vom vorherigen Inhaber während der abgebrochenen Kontrolle erzeugte
|
||||
# Fehlbestände dürfen nicht stehen bleiben (Prompt 16.6: kein Fehlbestand
|
||||
# durch eine abgebrochene Kontrolle).
|
||||
await _verwirf_kontrolldaten(db, laufende.id)
|
||||
await historie_service.log(
|
||||
db,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
benutzer_id=benutzer_id,
|
||||
ereignistyp="kontrolle_uebernommen",
|
||||
entitaet_typ="kontrolle",
|
||||
entitaet_id=laufende.id,
|
||||
alter_wert={"benutzer_id": laufende.benutzer_id},
|
||||
neuer_wert={"benutzer_id": benutzer_id},
|
||||
)
|
||||
|
||||
neue_kontrolle = Kontrolle(
|
||||
erzeugt_von_server_id=zustaendiger_server_id,
|
||||
objekt_id=objekt_id,
|
||||
benutzer_id=benutzer_id,
|
||||
status=KontrollStatus.in_bearbeitung,
|
||||
gestartet_am=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(neue_kontrolle)
|
||||
await db.flush()
|
||||
|
||||
await _lasse_mindermengen_ablaufen(
|
||||
db, objekt_id=objekt_id, neue_kontrolle=neue_kontrolle, zustaendiger_server_id=zustaendiger_server_id
|
||||
)
|
||||
|
||||
await historie_service.log(
|
||||
db,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
benutzer_id=benutzer_id,
|
||||
ereignistyp="kontrolle_gestartet",
|
||||
entitaet_typ="kontrolle",
|
||||
entitaet_id=neue_kontrolle.id,
|
||||
neuer_wert={"objekt_id": objekt_id, "status": "in_bearbeitung"},
|
||||
)
|
||||
return neue_kontrolle
|
||||
|
||||
|
||||
async def _lasse_mindermengen_ablaufen(
|
||||
db: AsyncSession, *, objekt_id: int, neue_kontrolle: Kontrolle, zustaendiger_server_id: int
|
||||
) -> None:
|
||||
"""Karte 08 / Prompt 04.4 / Sprintplan U6: Genehmigung gilt automatisch nur bis
|
||||
zur nächsten Kontrolle desselben Objekts - kein Zeit-Trigger nötig, der Start
|
||||
der nächsten Kontrolle IST der Ablauf-Zeitpunkt."""
|
||||
result = await db.execute(
|
||||
select(MindermengenGenehmigung)
|
||||
.join(Fehlbestand, MindermengenGenehmigung.fehlbestand_id == Fehlbestand.id)
|
||||
.where(Fehlbestand.objekt_id == objekt_id, MindermengenGenehmigung.status == MindermengeStatus.aktiv)
|
||||
)
|
||||
for genehmigung in result.scalars().all():
|
||||
genehmigung.status = MindermengeStatus.abgelaufen
|
||||
genehmigung.beendet_am = datetime.now(timezone.utc)
|
||||
genehmigung.beendende_kontrolle_id = neue_kontrolle.id
|
||||
await historie_service.log(
|
||||
db,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
benutzer_id=None,
|
||||
ereignistyp="mindermenge_abgelaufen",
|
||||
entitaet_typ="mindermengen_genehmigung",
|
||||
entitaet_id=genehmigung.id,
|
||||
alter_wert={"status": "aktiv"},
|
||||
neuer_wert={"status": "abgelaufen", "beendende_kontrolle_id": str(neue_kontrolle.id)},
|
||||
)
|
||||
await db.flush()
|
||||
|
||||
|
||||
async def _verwirf_kontrolldaten(db: AsyncSession, kontrolle_id) -> None:
|
||||
fehlbestaende = await db.execute(select(Fehlbestand).where(Fehlbestand.kontrolle_id == kontrolle_id))
|
||||
for fehlbestand in fehlbestaende.scalars().all():
|
||||
await db.delete(fehlbestand)
|
||||
positionen = await db.execute(select(Kontrollposition).where(Kontrollposition.kontrolle_id == kontrolle_id))
|
||||
for position in positionen.scalars().all():
|
||||
await db.delete(position)
|
||||
await db.flush()
|
||||
|
||||
|
||||
async def fehlende_material_ids(db: AsyncSession, kontrolle: Kontrolle) -> list[int]:
|
||||
result = await db.execute(
|
||||
select(Objektposition.material_id).where(
|
||||
Objektposition.objekt_id == kontrolle.objekt_id,
|
||||
Objektposition.ist_status == ObjektpositionStatus.aktiv,
|
||||
)
|
||||
)
|
||||
erwartete = {m for (m,) in result.all()}
|
||||
|
||||
result = await db.execute(
|
||||
select(Kontrollposition.material_id).where(Kontrollposition.kontrolle_id == kontrolle.id)
|
||||
)
|
||||
erfasste = {m for (m,) in result.all()}
|
||||
|
||||
return sorted(erwartete - erfasste)
|
||||
|
||||
|
||||
async def abschliesse_kontrolle(db: AsyncSession, *, kontrolle: Kontrolle, zustaendiger_server_id: int) -> None:
|
||||
fehlende = await fehlende_material_ids(db, kontrolle)
|
||||
if fehlende:
|
||||
raise UnvollstaendigError(fehlende)
|
||||
kontrolle.status = KontrollStatus.abgeschlossen
|
||||
kontrolle.beendet_am = datetime.now(timezone.utc)
|
||||
await db.flush()
|
||||
await historie_service.log(
|
||||
db,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
benutzer_id=kontrolle.benutzer_id,
|
||||
ereignistyp="kontrolle_abgeschlossen",
|
||||
entitaet_typ="kontrolle",
|
||||
entitaet_id=kontrolle.id,
|
||||
neuer_wert={"status": "abgeschlossen"},
|
||||
)
|
||||
|
||||
|
||||
async def abbreche_kontrolle(
|
||||
db: AsyncSession, *, kontrolle: Kontrolle, grund: str | None, zustaendiger_server_id: int
|
||||
) -> None:
|
||||
"""Prompt 16.6: keine Ist-Übernahme (Kontrolle rührt Objektposition ohnehin nie
|
||||
an), kein Fehlbestand aus dieser Kontrolle bleibt bestehen. Kontrolle selbst
|
||||
bleibt als Datensatz erhalten (Status "abgebrochen"), nicht gelöscht."""
|
||||
await _verwirf_kontrolldaten(db, kontrolle.id)
|
||||
kontrolle.status = KontrollStatus.abgebrochen
|
||||
kontrolle.abbruch_grund = grund
|
||||
kontrolle.beendet_am = datetime.now(timezone.utc)
|
||||
await db.flush()
|
||||
await historie_service.log(
|
||||
db,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
benutzer_id=kontrolle.benutzer_id,
|
||||
ereignistyp="kontrolle_abgebrochen",
|
||||
entitaet_typ="kontrolle",
|
||||
entitaet_id=kontrolle.id,
|
||||
neuer_wert={"status": "abgebrochen", "grund": grund},
|
||||
)
|
||||
Reference in New Issue
Block a user