Sprint 4: Fehlbestand-Statusmaschine komplett, Nachfüllung sofort/später/teilweise
CI / backend-tests (push) Successful in 40s
CI / backend-tests (push) Successful in 40s
- Nachfuellung-Modell: einzige Aktion, die Objektposition.istmenge tatsächlich ändert (Vier-Kernbegriffe). Deckt Sofort-Nachfüllung während Kontrolle (Karte 07) und spätere/externe Nachfüllung (Prompt 02.5) mit derselben Logik ab - Wichtiger Fix vor dem ersten Testlauf: Objektposition.istmenge wird bei Nachfüllung auf den neuen Wert GESETZT (fehlbestand.istmenge + menge), nicht unabhängig inkrementiert - sonst bliebe sie beim initialen "nicht kontrolliert"-Stand (0) stehen, obwohl die Kontrolle bereits einen realen Zählwert kennt - Statusübergänge: offen -> nachgefuellt_teilweise (Fehlmenge>0) -> erledigt (Fehlmenge=0), ausschließlich automatisch bei Ist=Soll (U3) - Überbestand durch Nachfüllung erlaubt, als Info gekennzeichnet, kein Fehler (E4) - KontrollpositionRead liefert jetzt fehlbestand_id mit, damit die UI direkt eine Sofort-Nachfüllung anbieten kann, ohne separat nachzufragen - Tests: U3, U4 (Sofort-Nachfüllung ohne Zwischenstatus), U5 (Teilnachfüllung), Überbestand-Fall, Nachfüllung auf bereits erledigten Fehlbestand -> 409 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
@@ -1199,3 +1199,45 @@ Keine Commits in dieser Session.
|
||||
- backend/tests/test_vorlagen.py | 34 ++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-03 23:34 – 23:40 (6m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
- 8fd507c Sprint 3: Kontroll-Kern (Statusmaschine, Objekt-Sperre, automatische Fehlbestand-Erzeugung)
|
||||
|
||||
### Geänderte Dateien
|
||||
- DEVLOG.md | 39 +++++++++++++
|
||||
- backend/app/api/v1/api.py | 3 +-
|
||||
- backend/app/api/v1/endpoints/kontrollen.py | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/models/__init__.py | 9 +++
|
||||
- backend/app/models/fehlbestand.py | 47 +++++++++++++++
|
||||
- backend/app/models/historie.py | 27 +++++++++
|
||||
- backend/app/models/kontrolle.py | 53 +++++++++++++++++
|
||||
- backend/app/schemas/kontrolle.py | 47 +++++++++++++++
|
||||
- backend/app/services/kontrolle.py | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/tests/conftest.py | 52 +++++++++++++++++
|
||||
- backend/tests/test_kontrolle.py | 238 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-03 23:40 – 23:40 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- DEVLOG.md | 39 +++++++++++++
|
||||
- backend/app/api/v1/api.py | 3 +-
|
||||
- backend/app/api/v1/endpoints/kontrollen.py | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/models/__init__.py | 9 +++
|
||||
- backend/app/models/fehlbestand.py | 47 +++++++++++++++
|
||||
- backend/app/models/historie.py | 27 +++++++++
|
||||
- backend/app/models/kontrolle.py | 53 +++++++++++++++++
|
||||
- backend/app/schemas/kontrolle.py | 47 +++++++++++++++
|
||||
- backend/app/services/kontrolle.py | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/tests/conftest.py | 52 +++++++++++++++++
|
||||
- backend/tests/test_kontrolle.py | 238 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1.endpoints import auth, benutzer, health, kontrollen, objekte, stammdaten, vorlagen, zustaendigkeit
|
||||
from app.api.v1.endpoints import (
|
||||
auth,
|
||||
benutzer,
|
||||
fehlbestaende,
|
||||
health,
|
||||
kontrollen,
|
||||
objekte,
|
||||
stammdaten,
|
||||
vorlagen,
|
||||
zustaendigkeit,
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(health.router, tags=["health"])
|
||||
@@ -11,3 +21,4 @@ api_router.include_router(zustaendigkeit.router, tags=["zustaendigkeit"])
|
||||
api_router.include_router(vorlagen.router, tags=["vorlagen"])
|
||||
api_router.include_router(objekte.router, tags=["objekte"])
|
||||
api_router.include_router(kontrollen.router, tags=["kontrollen"])
|
||||
api_router.include_router(fehlbestaende.router, tags=["fehlbestaende"])
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import require_roles
|
||||
from app.db.session import get_db
|
||||
from app.models.auth import RolleTyp
|
||||
from app.models.fehlbestand import Fehlbestand
|
||||
from app.schemas.fehlbestand import (
|
||||
FehlbestandRead,
|
||||
NachfuellungCreate,
|
||||
NachfuellungRead,
|
||||
NachfuellungResponse,
|
||||
)
|
||||
from app.services.fehlbestand import FehlbestandBereitsErledigtError, nachfuellen
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_mitarbeiter_plus = require_roles(
|
||||
RolleTyp.mitarbeiter,
|
||||
RolleTyp.materialverantwortlicher,
|
||||
RolleTyp.leitungsverantwortlicher,
|
||||
RolleTyp.administration,
|
||||
)
|
||||
_verantwortliche = require_roles(
|
||||
RolleTyp.administration, RolleTyp.materialverantwortlicher, RolleTyp.leitungsverantwortlicher
|
||||
)
|
||||
|
||||
|
||||
@router.get("/fehlbestaende", response_model=list[FehlbestandRead])
|
||||
async def liste_fehlbestaende(
|
||||
status_filter: str | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_=Depends(_verantwortliche),
|
||||
) -> list[Fehlbestand]:
|
||||
"""Vollständige Filterung nach Zuständigkeit/Standort folgt Sprint 6 (Dashboard)."""
|
||||
stmt = select(Fehlbestand)
|
||||
if status_filter is not None:
|
||||
stmt = stmt.where(Fehlbestand.status == status_filter)
|
||||
result = await db.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.get("/fehlbestaende/{fehlbestand_id}", response_model=FehlbestandRead)
|
||||
async def hole_fehlbestand(
|
||||
fehlbestand_id: uuid.UUID, db: AsyncSession = Depends(get_db), _=Depends(_mitarbeiter_plus)
|
||||
) -> Fehlbestand:
|
||||
fehlbestand = await db.get(Fehlbestand, fehlbestand_id)
|
||||
if fehlbestand is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fehlbestand nicht gefunden")
|
||||
return fehlbestand
|
||||
|
||||
|
||||
@router.post(
|
||||
"/fehlbestaende/{fehlbestand_id}/nachfuellungen",
|
||||
response_model=NachfuellungResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def erfasse_nachfuellung(
|
||||
fehlbestand_id: uuid.UUID,
|
||||
payload: NachfuellungCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user=Depends(_mitarbeiter_plus),
|
||||
) -> NachfuellungResponse:
|
||||
"""Prompt 02.3/02.5: deckt Sofort-Nachfüllung während der Kontrolle (Karte 07)
|
||||
und spätere/externe Nachfüllung gleichermaßen ab."""
|
||||
fehlbestand = await db.get(Fehlbestand, fehlbestand_id)
|
||||
if fehlbestand is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fehlbestand nicht gefunden")
|
||||
|
||||
try:
|
||||
nachfuellung, ueberbestand = await nachfuellen(
|
||||
db, fehlbestand=fehlbestand, menge=payload.menge, benutzer_id=current_user.id
|
||||
)
|
||||
except FehlbestandBereitsErledigtError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Fehlbestand ist bereits erledigt"
|
||||
) from exc
|
||||
|
||||
return NachfuellungResponse(
|
||||
nachfuellung=NachfuellungRead.model_validate(nachfuellung),
|
||||
fehlbestand=FehlbestandRead.model_validate(fehlbestand),
|
||||
ueberbestand=ueberbestand,
|
||||
)
|
||||
@@ -121,13 +121,13 @@ async def erfasse_kontrollposition(
|
||||
payload: KontrollpositionInput,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Benutzer = Depends(_mitarbeiter_plus),
|
||||
) -> Kontrollposition:
|
||||
) -> KontrollpositionRead:
|
||||
kontrolle = await _hole_kontrolle_oder_404(db, kontrolle_id)
|
||||
_pruefe_besitz(kontrolle, current_user)
|
||||
|
||||
objekt = await db.get(Objekt, kontrolle.objekt_id)
|
||||
zustaendiger_server_id = await _hauptserver_id(db)
|
||||
return await erfasse_position(
|
||||
kontrollposition, fehlbestand_id = await erfasse_position(
|
||||
db,
|
||||
kontrolle=kontrolle,
|
||||
objekt=objekt,
|
||||
@@ -135,6 +135,15 @@ async def erfasse_kontrollposition(
|
||||
istmenge=payload.istmenge,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
)
|
||||
return KontrollpositionRead(
|
||||
id=kontrollposition.id,
|
||||
kontrolle_id=kontrollposition.kontrolle_id,
|
||||
material_id=kontrollposition.material_id,
|
||||
sollmenge_snapshot=kontrollposition.sollmenge_snapshot,
|
||||
istmenge_erfasst=kontrollposition.istmenge_erfasst,
|
||||
abweichung=kontrollposition.abweichung,
|
||||
fehlbestand_id=fehlbestand_id,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/kontrollen/{kontrolle_id}/abschliessen", response_model=KontrolleRead)
|
||||
|
||||
@@ -2,6 +2,7 @@ from app.models.auth import Benutzer, BenutzerRolle, RolleTyp, Systemknoten, Kno
|
||||
from app.models.fehlbestand import Fehlbestand, FehlbestandStatus
|
||||
from app.models.historie import Historie
|
||||
from app.models.kontrolle import Kontrolle, KontrollStatus, Kontrollposition
|
||||
from app.models.nachfuellung import Nachfuellung
|
||||
from app.models.objekt import Objekt, ObjektStatus
|
||||
from app.models.objektposition import Objektposition, ObjektpositionStatus
|
||||
from app.models.stammdaten import Bereich, Kategorie, Material, MaterialTyp, Objekttyp, Standort
|
||||
@@ -20,6 +21,7 @@ __all__ = [
|
||||
"Kontrolle",
|
||||
"KontrollStatus",
|
||||
"Kontrollposition",
|
||||
"Nachfuellung",
|
||||
"Objekt",
|
||||
"ObjektStatus",
|
||||
"Objektposition",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import ForeignKey, Numeric
|
||||
from sqlalchemy.dialects.postgresql import TIMESTAMP, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class Nachfuellung(Base):
|
||||
"""Vier-Kernbegriffe (Prompt 02.9): einzige Aktion, die Objektposition.istmenge
|
||||
tatsächlich ändert. fehlbestand_id nullable: Schema erlaubt Nachfüllung ohne
|
||||
Fehlbestand-Bezug, V1 nutzt sie aber ausschließlich fehlbestand-gebunden
|
||||
(Prompt 21 Abschnitt 7)."""
|
||||
|
||||
__tablename__ = "nachfuellung"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
fehlbestand_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("fehlbestand.id"))
|
||||
objekt_id: Mapped[int] = mapped_column(ForeignKey("objekt.id"), nullable=False)
|
||||
material_id: Mapped[int] = mapped_column(ForeignKey("material.id"), nullable=False)
|
||||
menge: Mapped[Decimal] = mapped_column(Numeric, nullable=False)
|
||||
benutzer_id: Mapped[int] = mapped_column(ForeignKey("benutzer.id"), nullable=False)
|
||||
zeitpunkt: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False)
|
||||
@@ -0,0 +1,46 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.models.fehlbestand import FehlbestandStatus
|
||||
|
||||
|
||||
class FehlbestandRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: uuid.UUID
|
||||
objekt_id: int
|
||||
material_id: int
|
||||
standort_id: int
|
||||
sollmenge: Decimal
|
||||
istmenge: Decimal
|
||||
fehlmenge: Decimal
|
||||
entstanden_am: datetime
|
||||
festgestellt_von: int
|
||||
kontrolle_id: uuid.UUID | None
|
||||
ursache: str | None
|
||||
verantwortlicher_id: int | None
|
||||
status: FehlbestandStatus
|
||||
erledigt_am: datetime | None
|
||||
|
||||
|
||||
class NachfuellungCreate(BaseModel):
|
||||
menge: Decimal
|
||||
|
||||
|
||||
class NachfuellungRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: uuid.UUID
|
||||
fehlbestand_id: uuid.UUID | None
|
||||
objekt_id: int
|
||||
material_id: int
|
||||
menge: Decimal
|
||||
benutzer_id: int
|
||||
zeitpunkt: datetime
|
||||
|
||||
|
||||
class NachfuellungResponse(BaseModel):
|
||||
nachfuellung: NachfuellungRead
|
||||
fehlbestand: FehlbestandRead
|
||||
ueberbestand: Decimal
|
||||
@@ -34,6 +34,7 @@ class KontrollpositionRead(BaseModel):
|
||||
sollmenge_snapshot: Decimal
|
||||
istmenge_erfasst: Decimal
|
||||
abweichung: bool
|
||||
fehlbestand_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
class KontrolleAbbrechenRequest(BaseModel):
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.fehlbestand import Fehlbestand, FehlbestandStatus
|
||||
from app.models.nachfuellung import Nachfuellung
|
||||
from app.models.objektposition import Objektposition
|
||||
|
||||
|
||||
class FehlbestandBereitsErledigtError(Exception):
|
||||
"""Ein bereits erledigter Fehlbestand kann nicht erneut nachgefüllt werden."""
|
||||
|
||||
|
||||
async def nachfuellen(
|
||||
db: AsyncSession, *, fehlbestand: Fehlbestand, menge: Decimal, benutzer_id: int
|
||||
) -> tuple[Nachfuellung, Decimal]:
|
||||
"""Vier-Kernbegriffe (Prompt 02.9): Nachfüllung ist die einzige Aktion, die
|
||||
Objektposition.istmenge tatsächlich ändert. Deckt sowohl Sofort-Nachfüllung
|
||||
während der Kontrolle (Karte 07) als auch spätere/externe Nachfüllung
|
||||
(Prompt 02.5) ab - technisch derselbe Vorgang, nur anderer Zeitpunkt.
|
||||
|
||||
Überbestand durch Nachfüllung ist erlaubt (Prompt 21.9/E4): kein Fehler, nur
|
||||
als Info im Rückgabewert `ueberbestand` gekennzeichnet.
|
||||
|
||||
Wichtig: `fehlbestand.istmenge` ist die zuletzt GEZÄHLTE Wahrheit (aus der
|
||||
Kontrolle, die den Fehlbestand ausgelöst hat) - Objektposition.istmenge wird
|
||||
hier auf den neuen Wert GESETZT, nicht unabhängig davon inkrementiert. Sonst
|
||||
bliebe Objektposition.istmenge bei ihrem alten (ggf. 0/"nicht kontrolliert")
|
||||
Stand stehen, obwohl die Kontrolle bereits einen realen Zählwert kennt -
|
||||
"Ist-Menge wird auf Soll korrigiert" (Prompt 02.3) meint genau dieses Setzen.
|
||||
"""
|
||||
if fehlbestand.status == FehlbestandStatus.erledigt:
|
||||
raise FehlbestandBereitsErledigtError()
|
||||
|
||||
neue_istmenge = fehlbestand.istmenge + menge
|
||||
ueberbestand = max(Decimal(0), neue_istmenge - fehlbestand.sollmenge)
|
||||
neue_fehlmenge = max(Decimal(0), fehlbestand.sollmenge - neue_istmenge)
|
||||
|
||||
result = await db.execute(
|
||||
select(Objektposition).where(
|
||||
Objektposition.objekt_id == fehlbestand.objekt_id,
|
||||
Objektposition.material_id == fehlbestand.material_id,
|
||||
)
|
||||
)
|
||||
objektposition = result.scalar_one_or_none()
|
||||
if objektposition is not None:
|
||||
objektposition.istmenge = neue_istmenge
|
||||
|
||||
fehlbestand.istmenge = neue_istmenge
|
||||
fehlbestand.fehlmenge = neue_fehlmenge
|
||||
if neue_fehlmenge <= 0:
|
||||
fehlbestand.status = FehlbestandStatus.erledigt
|
||||
fehlbestand.erledigt_am = datetime.now(timezone.utc)
|
||||
else:
|
||||
fehlbestand.status = FehlbestandStatus.nachgefuellt_teilweise
|
||||
|
||||
nachfuellung = Nachfuellung(
|
||||
fehlbestand_id=fehlbestand.id,
|
||||
objekt_id=fehlbestand.objekt_id,
|
||||
material_id=fehlbestand.material_id,
|
||||
menge=menge,
|
||||
benutzer_id=benutzer_id,
|
||||
zeitpunkt=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(nachfuellung)
|
||||
await db.flush()
|
||||
return nachfuellung, ueberbestand
|
||||
@@ -1,3 +1,4 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -102,10 +103,12 @@ async def erfasse_position(
|
||||
material_id: int,
|
||||
istmenge: Decimal,
|
||||
zustaendiger_server_id: int,
|
||||
) -> Kontrollposition:
|
||||
) -> tuple[Kontrollposition, uuid.UUID | None]:
|
||||
"""Prompt 02.3/09 Vier-Kernbegriffe: Kontrolle erfasst nur, ändert nie die
|
||||
persistente Objektposition.istmenge. Bei Ist < Soll entsteht automatisch ein
|
||||
Fehlbestand (nie bei Überbestand, Prompt 02.4)."""
|
||||
Fehlbestand (nie bei Überbestand, Prompt 02.4). Gibt zusätzlich die
|
||||
Fehlbestand-ID zurück (falls einer aktiv ist), damit die UI direkt eine
|
||||
Sofort-Nachfüllung (Karte 07) anbieten kann, ohne separat nachzufragen."""
|
||||
vorlagen_sollmengen = await sollmenge_map(db, objekt.vorlage_id)
|
||||
result = await db.execute(
|
||||
select(Objektposition).where(
|
||||
@@ -138,7 +141,7 @@ async def erfasse_position(
|
||||
kontrollposition.abweichung = abweichung
|
||||
await db.flush()
|
||||
|
||||
await _synchronisiere_fehlbestand(
|
||||
fehlbestand_id = await _synchronisiere_fehlbestand(
|
||||
db,
|
||||
kontrolle=kontrolle,
|
||||
objekt=objekt,
|
||||
@@ -147,7 +150,7 @@ async def erfasse_position(
|
||||
istmenge=istmenge,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
)
|
||||
return kontrollposition
|
||||
return kontrollposition, fehlbestand_id
|
||||
|
||||
|
||||
async def _synchronisiere_fehlbestand(
|
||||
@@ -159,7 +162,7 @@ async def _synchronisiere_fehlbestand(
|
||||
sollmenge: Decimal,
|
||||
istmenge: Decimal,
|
||||
zustaendiger_server_id: int,
|
||||
) -> None:
|
||||
) -> uuid.UUID | None:
|
||||
result = await db.execute(
|
||||
select(Fehlbestand).where(
|
||||
Fehlbestand.kontrolle_id == kontrolle.id,
|
||||
@@ -174,8 +177,7 @@ async def _synchronisiere_fehlbestand(
|
||||
# Ist < Soll (Prompt 02.3) - Überbestand (Ist > Soll) erzeugt laut Prompt 02.4
|
||||
# ausdrücklich KEINEN Fehlbestand.
|
||||
if bestehender is None:
|
||||
db.add(
|
||||
Fehlbestand(
|
||||
bestehender = Fehlbestand(
|
||||
erzeugt_von_server_id=zustaendiger_server_id,
|
||||
objekt_id=objekt.id,
|
||||
material_id=material_id,
|
||||
@@ -188,7 +190,7 @@ async def _synchronisiere_fehlbestand(
|
||||
kontrolle_id=kontrolle.id,
|
||||
status=FehlbestandStatus.offen,
|
||||
)
|
||||
)
|
||||
db.add(bestehender)
|
||||
else:
|
||||
# Korrektur vor Abschluss (z. B. Tippfehler nochmal erfasst) - solange die
|
||||
# Kontrolle noch läuft, ist der von IHR erzeugte Fehlbestand noch nicht
|
||||
@@ -200,7 +202,9 @@ async def _synchronisiere_fehlbestand(
|
||||
# Abweichung wurde vor Abschluss korrigiert - der von dieser Kontrolle
|
||||
# erzeugte Fehlbestand hat sich erledigt, bevor er je "real" wurde.
|
||||
await db.delete(bestehender)
|
||||
bestehender = None
|
||||
await db.flush()
|
||||
return bestehender.id if bestehender is not None else None
|
||||
|
||||
|
||||
async def fehlende_material_ids(db: AsyncSession, kontrolle: Kontrolle) -> list[int]:
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.models.objektposition import Objektposition
|
||||
from tests.conftest import auth_header, login
|
||||
|
||||
|
||||
async def _fehlbestand_erzeugen(client, token, objekt, material, istmenge: str):
|
||||
start = await client.post(
|
||||
f"/api/v1/objekte/{objekt.id}/kontrollen", json={"uebernehmen": False}, headers=auth_header(token)
|
||||
)
|
||||
kontrolle_id = start.json()["id"]
|
||||
put_response = await client.put(
|
||||
f"/api/v1/kontrollen/{kontrolle_id}/positionen/{material.id}",
|
||||
json={"istmenge": istmenge},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
return put_response.json()["fehlbestand_id"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sofort_nachfuellung_erledigt_ohne_zwischenstatus(
|
||||
client, db_session, objekt_mit_position, mitarbeiter_user
|
||||
):
|
||||
"""U4: Sofort-Nachfüllung während der Kontrolle - Fehlbestand entsteht und
|
||||
wird im selben Moment erledigt, Ist-Menge wird auf Soll korrigiert."""
|
||||
objekt, material = objekt_mit_position
|
||||
token = await login(client, "mitarbeiter1")
|
||||
fehlbestand_id = await _fehlbestand_erzeugen(client, token, objekt, material, "7")
|
||||
|
||||
response = await client.post(
|
||||
f"/api/v1/fehlbestaende/{fehlbestand_id}/nachfuellungen",
|
||||
json={"menge": "3"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert response.status_code == 201
|
||||
body = response.json()
|
||||
assert body["fehlbestand"]["status"] == "erledigt"
|
||||
assert float(body["fehlbestand"]["fehlmenge"]) == 0
|
||||
assert float(body["ueberbestand"]) == 0
|
||||
|
||||
objektposition = (
|
||||
await db_session.execute(
|
||||
select(Objektposition).where(
|
||||
Objektposition.objekt_id == objekt.id, Objektposition.material_id == material.id
|
||||
)
|
||||
)
|
||||
).scalar_one()
|
||||
assert float(objektposition.istmenge) == 10
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teilnachfuellung_bleibt_offen_mit_reduzierter_fehlmenge(
|
||||
client, db_session, objekt_mit_position, mitarbeiter_user
|
||||
):
|
||||
"""U5: Teilnachfüllung reduziert Fehlmenge, Status bleibt (fachlich weiterhin) offen."""
|
||||
objekt, material = objekt_mit_position
|
||||
token = await login(client, "mitarbeiter1")
|
||||
fehlbestand_id = await _fehlbestand_erzeugen(client, token, objekt, material, "6")
|
||||
|
||||
response = await client.post(
|
||||
f"/api/v1/fehlbestaende/{fehlbestand_id}/nachfuellungen",
|
||||
json={"menge": "2"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert response.status_code == 201
|
||||
body = response.json()["fehlbestand"]
|
||||
assert body["status"] == "nachgefuellt_teilweise"
|
||||
assert float(body["fehlmenge"]) == 2
|
||||
assert float(body["istmenge"]) == 8
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_erledigt_ausschliesslich_bei_ist_gleich_soll(
|
||||
client, db_session, objekt_mit_position, mitarbeiter_user
|
||||
):
|
||||
"""U3: Fehlbestand wird ausschließlich automatisch bei Ist=Soll erledigt."""
|
||||
objekt, material = objekt_mit_position
|
||||
token = await login(client, "mitarbeiter1")
|
||||
fehlbestand_id = await _fehlbestand_erzeugen(client, token, objekt, material, "4")
|
||||
|
||||
teil = await client.post(
|
||||
f"/api/v1/fehlbestaende/{fehlbestand_id}/nachfuellungen",
|
||||
json={"menge": "3"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert teil.json()["fehlbestand"]["status"] == "nachgefuellt_teilweise"
|
||||
|
||||
rest = await client.post(
|
||||
f"/api/v1/fehlbestaende/{fehlbestand_id}/nachfuellungen",
|
||||
json={"menge": "3"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert rest.json()["fehlbestand"]["status"] == "erledigt"
|
||||
assert float(rest.json()["fehlbestand"]["fehlmenge"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ueberbestand_durch_nachfuellung_kein_fehler(
|
||||
client, db_session, objekt_mit_position, mitarbeiter_user
|
||||
):
|
||||
"""E4/Prompt 21.9: Nachfüllung über Soll hinaus erlaubt, als Überbestand markiert."""
|
||||
objekt, material = objekt_mit_position
|
||||
token = await login(client, "mitarbeiter1")
|
||||
fehlbestand_id = await _fehlbestand_erzeugen(client, token, objekt, material, "7")
|
||||
|
||||
response = await client.post(
|
||||
f"/api/v1/fehlbestaende/{fehlbestand_id}/nachfuellungen",
|
||||
json={"menge": "5"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert response.status_code == 201
|
||||
body = response.json()
|
||||
assert body["fehlbestand"]["status"] == "erledigt"
|
||||
assert float(body["ueberbestand"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nachfuellung_auf_erledigten_fehlbestand_ist_409(
|
||||
client, objekt_mit_position, mitarbeiter_user
|
||||
):
|
||||
objekt, material = objekt_mit_position
|
||||
token = await login(client, "mitarbeiter1")
|
||||
fehlbestand_id = await _fehlbestand_erzeugen(client, token, objekt, material, "7")
|
||||
|
||||
erste = await client.post(
|
||||
f"/api/v1/fehlbestaende/{fehlbestand_id}/nachfuellungen",
|
||||
json={"menge": "3"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert erste.json()["fehlbestand"]["status"] == "erledigt"
|
||||
|
||||
zweite = await client.post(
|
||||
f"/api/v1/fehlbestaende/{fehlbestand_id}/nachfuellungen",
|
||||
json={"menge": "1"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert zweite.status_code == 409
|
||||
Reference in New Issue
Block a user