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
207 lines
8.2 KiB
Python
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
|