Files
MABEA/backend/app/services/kontrolle/erfassung.py
T
patrickandClaude Sonnet 5 a18ec5bbce
CI / backend-tests (push) Failing after 2m1s
CI / frontend-build (push) Successful in 18s
fix(kontrolle): Kontrolle bucht Ist-Menge jetzt immer, nicht nur bei Abweichung
Bisher änderte eine Kontrolle Objektposition.istmenge nur indirekt
über eine anschließende Nachfüllung bei Abweichung. Zählte eine
Kontrolle Ist=Soll (kein Fehlbestand, keine Nachfüllung), blieb der
Wert dauerhaft auf 0 stehen - ein frisch angelegtes, aber vollständig
bestücktes Objekt zeigte in der Beladung nie den echten Bestand.
Kontrolle setzt istmenge jetzt immer auf den gezählten Wert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
2026-09-06 23:38:17 +02:00

207 lines
8.2 KiB
Python

import uuid
from datetime import date, 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.kontrolle import Kontrolle, Kontrollposition
from app.models.mindermenge import MindermengeStatus, MindermengenGenehmigung
from app.models.objekt import Objekt
from app.models.objektposition import Objektposition
from app.services import benachrichtigung as benachrichtigung_service
from app.services import historie as historie_service
from app.services.objekte import sollmenge_effektiv
async def erfasse_position(
db: AsyncSession,
*,
kontrolle: Kontrolle,
objekt: Objekt,
material_id: int,
istmenge: Decimal,
zustaendiger_server_id: int,
ablaufdatum: date | None = None,
chargennummer: str | None = None,
seriennummer: str | None = None,
) -> tuple[Kontrollposition, uuid.UUID | None]:
"""Prompt 02.3/09: Kontrolle bucht die gezählte Menge als neuen Ist-Bestand
der Objektposition - vorher blieb dieser bei Ist=Soll unverändert (nur
Nachfüllung schrieb ihn), wodurch ein frisch angelegtes, aber vollständig
bestücktes Objekt dauerhaft auf 0 stehen blieb. Bei Ist < Soll entsteht
automatisch ein 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.
Ablaufdatum/Chargennummer/Seriennummer sind KEINE Kernbegriffe-Verletzung:
sie beschreiben das physische Exemplar, nicht die Ist-Menge, und werden
daher hier - auf Wunsch als Teil der normalen Kontroll-Erfassung, nicht nur
als separate Admin-Pflege - direkt mitgepflegt."""
result = await db.execute(
select(Objektposition).where(
Objektposition.objekt_id == objekt.id, Objektposition.material_id == material_id
)
)
objektposition = result.scalar_one_or_none()
sollmenge = sollmenge_effektiv(objektposition) if objektposition else Decimal(0)
if objektposition is not None:
objektposition.istmenge = istmenge
if ablaufdatum is not None:
objektposition.ablaufdatum = ablaufdatum
if chargennummer is not None:
objektposition.chargennummer = chargennummer
if seriennummer is not None:
objektposition.seriennummer = seriennummer
abweichung = istmenge != sollmenge
result = await db.execute(
select(Kontrollposition).where(
Kontrollposition.kontrolle_id == kontrolle.id, Kontrollposition.material_id == material_id
)
)
kontrollposition = result.scalar_one_or_none()
if kontrollposition is None:
kontrollposition = Kontrollposition(
kontrolle_id=kontrolle.id,
material_id=material_id,
sollmenge_snapshot=sollmenge,
istmenge_erfasst=istmenge,
abweichung=abweichung,
)
db.add(kontrollposition)
else:
kontrollposition.sollmenge_snapshot = sollmenge
kontrollposition.istmenge_erfasst = istmenge
kontrollposition.abweichung = abweichung
await db.flush()
await historie_service.log(
db,
zustaendiger_server_id=zustaendiger_server_id,
benutzer_id=kontrolle.benutzer_id,
ereignistyp="istmenge_erfasst",
entitaet_typ="kontrollposition",
entitaet_id=kontrollposition.id,
neuer_wert={"material_id": material_id, "sollmenge": str(sollmenge), "istmenge": str(istmenge)},
)
fehlbestand_id = await _synchronisiere_fehlbestand(
db,
kontrolle=kontrolle,
objekt=objekt,
material_id=material_id,
sollmenge=sollmenge,
istmenge=istmenge,
zustaendiger_server_id=zustaendiger_server_id,
)
return kontrollposition, fehlbestand_id
async def _synchronisiere_fehlbestand(
db: AsyncSession,
*,
kontrolle: Kontrolle,
objekt: Objekt,
material_id: int,
sollmenge: Decimal,
istmenge: Decimal,
zustaendiger_server_id: int,
) -> uuid.UUID | None:
result = await db.execute(
select(Fehlbestand).where(
Fehlbestand.objekt_id == objekt.id,
Fehlbestand.material_id == material_id,
Fehlbestand.status == FehlbestandStatus.offen,
)
)
bestehender = result.scalar_one_or_none()
von_dieser_kontrolle = bestehender is not None and bestehender.kontrolle_id == kontrolle.id
unterdeckung = sollmenge - istmenge
if unterdeckung > 0:
# Ist < Soll (Prompt 02.3) - Überbestand (Ist > Soll) erzeugt laut Prompt 02.4
# ausdrücklich KEINEN Fehlbestand.
if bestehender is None:
bestehender = Fehlbestand(
erzeugt_von_server_id=zustaendiger_server_id,
objekt_id=objekt.id,
material_id=material_id,
standort_id=objekt.standort_id,
sollmenge=sollmenge,
istmenge=istmenge,
fehlmenge=unterdeckung,
entstanden_am=datetime.now(timezone.utc),
festgestellt_von=kontrolle.benutzer_id,
kontrolle_id=kontrolle.id,
status=FehlbestandStatus.offen,
)
db.add(bestehender)
await db.flush()
await historie_service.log(
db,
zustaendiger_server_id=zustaendiger_server_id,
benutzer_id=kontrolle.benutzer_id,
ereignistyp="fehlbestand_entstanden",
entitaet_typ="fehlbestand",
entitaet_id=bestehender.id,
neuer_wert={
"objekt_id": objekt.id,
"material_id": material_id,
"sollmenge": str(sollmenge),
"istmenge": str(istmenge),
"fehlmenge": str(unterdeckung),
},
)
await benachrichtigung_service.benachrichtige_neuer_fehlbestand(db, fehlbestand=bestehender)
else:
# kontrolle_id bleibt unangetastet, sonst würde ein Abbruch dieser
# Kontrolle einen fremden Fehlbestand mitlöschen (_verwirf_kontrolldaten).
bestehender.sollmenge = sollmenge
bestehender.istmenge = istmenge
bestehender.fehlmenge = unterdeckung
elif bestehender is not None:
if von_dieser_kontrolle:
await db.delete(bestehender)
bestehender = None
else:
bestehender.istmenge = istmenge
bestehender.fehlmenge = Decimal(0)
bestehender.status = FehlbestandStatus.erledigt
bestehender.erledigt_am = datetime.now(timezone.utc)
await historie_service.log(
db,
zustaendiger_server_id=zustaendiger_server_id,
benutzer_id=kontrolle.benutzer_id,
ereignistyp="fehlbestand_erledigt",
entitaet_typ="fehlbestand",
entitaet_id=bestehender.id,
neuer_wert={"status": "erledigt"},
)
genehmigung_result = await db.execute(
select(MindermengenGenehmigung).where(
MindermengenGenehmigung.fehlbestand_id == bestehender.id,
MindermengenGenehmigung.status == MindermengeStatus.aktiv,
)
)
genehmigung = genehmigung_result.scalar_one_or_none()
if genehmigung is not None:
genehmigung.status = MindermengeStatus.beendet_durch_erledigung
genehmigung.beendet_am = datetime.now(timezone.utc)
await historie_service.log(
db,
zustaendiger_server_id=zustaendiger_server_id,
benutzer_id=kontrolle.benutzer_id,
ereignistyp="mindermenge_beendet_durch_erledigung",
entitaet_typ="mindermengen_genehmigung",
entitaet_id=genehmigung.id,
neuer_wert={"status": "beendet_durch_erledigung"},
)
bestehender = None
await db.flush()
return bestehender.id if bestehender is not None else None