feat(estimates): integrate inventory items
This commit is contained in:
parent
fb5c2cc26a
commit
663bdc41b1
15 changed files with 629 additions and 28 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import re
|
||||
import unicodedata
|
||||
from decimal import Decimal
|
||||
from typing import TypeVar
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
|
@ -8,6 +9,7 @@ from sqlalchemy.orm import Session
|
|||
from starlette.requests import Request
|
||||
|
||||
from app.models.inventory import InventoryCategory, InventoryItem, InventoryLocation, InventorySupplier
|
||||
from app.models.repair_estimate import RepairEstimate
|
||||
from app.models.user import User
|
||||
from app.repositories.inventory_repository import InventoryRepository
|
||||
from app.schemas.inventory import (
|
||||
|
|
@ -194,6 +196,83 @@ class InventoryService:
|
|||
InventoryService._add_movement(db, item, "consumption", payload, actor_user_id=actor.id)
|
||||
return InventoryService._commit_stock_action(db, item, before_data, "inventory.stock.consume", actor, request)
|
||||
|
||||
@staticmethod
|
||||
def reserve_for_estimate(db: Session, estimate: RepairEstimate, *, actor_user_id: int | None) -> None:
|
||||
for estimate_item in estimate.items:
|
||||
if estimate_item.inventory_item_id is None:
|
||||
continue
|
||||
item = InventoryRepository.get_item(db, estimate_item.inventory_item_id)
|
||||
if item is None or not item.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"Lagerartikel für Position {estimate_item.position} ist nicht mehr aktiv")
|
||||
quantity = InventoryService._estimate_quantity_to_int(estimate_item.quantity, estimate_item.position)
|
||||
if quantity > item.quantity_available:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Nicht genügend verfügbarer Bestand für {item.sku} · {item.name}",
|
||||
)
|
||||
item.quantity_reserved += quantity
|
||||
InventoryService._recalculate_available(item)
|
||||
InventoryRepository.create_movement(
|
||||
db,
|
||||
item_id=item.id,
|
||||
movement_type="reservation",
|
||||
quantity=quantity,
|
||||
reason="estimate_reserved",
|
||||
reference_type="repair_estimate",
|
||||
reference_id=estimate.id,
|
||||
note=f"Kostenvoranschlag {estimate.estimate_number}",
|
||||
actor_user_id=actor_user_id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def release_estimate_reservation(db: Session, estimate: RepairEstimate, *, actor_user_id: int | None) -> None:
|
||||
for estimate_item in estimate.items:
|
||||
if estimate_item.inventory_item_id is None:
|
||||
continue
|
||||
item = InventoryRepository.get_item(db, estimate_item.inventory_item_id)
|
||||
if item is None:
|
||||
continue
|
||||
quantity = InventoryService._estimate_quantity_to_int(estimate_item.quantity, estimate_item.position)
|
||||
item.quantity_reserved = max(0, item.quantity_reserved - quantity)
|
||||
InventoryService._recalculate_available(item)
|
||||
InventoryRepository.create_movement(
|
||||
db,
|
||||
item_id=item.id,
|
||||
movement_type="release",
|
||||
quantity=quantity,
|
||||
reason="estimate_released",
|
||||
reference_type="repair_estimate",
|
||||
reference_id=estimate.id,
|
||||
note=f"Kostenvoranschlag {estimate.estimate_number}",
|
||||
actor_user_id=actor_user_id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def consume_reserved_stock(db: Session, estimate: RepairEstimate, *, actor_user_id: int | None) -> None:
|
||||
for estimate_item in estimate.items:
|
||||
if estimate_item.inventory_item_id is None:
|
||||
continue
|
||||
item = InventoryRepository.get_item(db, estimate_item.inventory_item_id)
|
||||
if item is None:
|
||||
continue
|
||||
quantity = InventoryService._estimate_quantity_to_int(estimate_item.quantity, estimate_item.position)
|
||||
if quantity > item.quantity_reserved or quantity > item.quantity_on_hand:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"Reservierter Bestand für {item.sku} reicht nicht aus")
|
||||
item.quantity_reserved -= quantity
|
||||
item.quantity_on_hand -= quantity
|
||||
InventoryService._recalculate_available(item)
|
||||
InventoryRepository.create_movement(
|
||||
db,
|
||||
item_id=item.id,
|
||||
movement_type="consumption",
|
||||
quantity=quantity,
|
||||
reason="estimate_consumed",
|
||||
reference_type="repair_estimate",
|
||||
reference_id=estimate.id,
|
||||
note=f"Kostenvoranschlag {estimate.estimate_number}",
|
||||
actor_user_id=actor_user_id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_category(db: Session, payload: InventoryCategoryPayload, *, actor: User, request: Request) -> InventoryCategory:
|
||||
category = InventoryCategory(name=payload.name, slug=_slugify(payload.name), description=payload.description)
|
||||
|
|
@ -260,6 +339,18 @@ class InventoryService:
|
|||
if payload.quantity <= 0:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Menge muss größer 0 sein")
|
||||
|
||||
@staticmethod
|
||||
def _estimate_quantity_to_int(quantity: Decimal, position: int) -> int:
|
||||
if quantity != quantity.to_integral_value():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"Lagerposition {position} muss eine ganze Menge verwenden",
|
||||
)
|
||||
normalized = int(quantity)
|
||||
if normalized <= 0:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Lagerposition {position} benötigt eine Menge größer 0")
|
||||
return normalized
|
||||
|
||||
@staticmethod
|
||||
def _add_movement(db: Session, item: InventoryItem, movement_type: str, payload: InventoryStockAction, *, actor_user_id: int) -> None:
|
||||
InventoryService._recalculate_available(item)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue