from typing import List, Optional from fastapi.encoders import jsonable_encoder from sqlalchemy import desc from sqlalchemy.orm import Session from app.models.inventory_movement import InventoryMovement, MovementType from app.models.product import Product from app.schemas.inventory import InventoryMovementCreate from app.utils.uuid import generate_uuid def get(db: Session, movement_id: str) -> Optional[InventoryMovement]: """ Get an inventory movement by ID. """ return db.query(InventoryMovement).filter(InventoryMovement.id == movement_id).first() def get_multi( db: Session, *, skip: int = 0, limit: int = 100, product_id: Optional[str] = None, movement_type: Optional[MovementType] = None, ) -> List[InventoryMovement]: """ Get multiple inventory movements with optional filtering. """ query = db.query(InventoryMovement) if product_id: query = query.filter(InventoryMovement.product_id == product_id) if movement_type: query = query.filter(InventoryMovement.type == movement_type) # Order by most recent first query = query.order_by(desc(InventoryMovement.created_at)) return query.offset(skip).limit(limit).all() def create( db: Session, *, obj_in: InventoryMovementCreate, created_by: Optional[str] = None ) -> InventoryMovement: """ Create a new inventory movement and update product stock. """ obj_in_data = jsonable_encoder(obj_in) db_obj = InventoryMovement(**obj_in_data, id=generate_uuid(), created_by=created_by) # Start transaction try: # Add movement record db.add(db_obj) # Update product stock based on movement type product = ( db.query(Product).filter(Product.id == obj_in.product_id).with_for_update().first() ) if not product: raise ValueError(f"Product with ID {obj_in.product_id} not found") if obj_in.type in [MovementType.STOCK_IN, MovementType.RETURN]: product.current_stock += obj_in.quantity elif obj_in.type == MovementType.STOCK_OUT: if product.current_stock < obj_in.quantity: raise ValueError( f"Insufficient stock for product {product.name}. Available: {product.current_stock}, Requested: {obj_in.quantity}" ) product.current_stock -= obj_in.quantity elif obj_in.type == MovementType.ADJUSTMENT: # For adjustments, the quantity value is the delta (can be positive or negative) product.current_stock += obj_in.quantity db.add(product) db.commit() db.refresh(db_obj) return db_obj except Exception as e: db.rollback() raise e def remove(db: Session, *, movement_id: str) -> Optional[InventoryMovement]: """ Delete an inventory movement and revert the stock change. This is generally not recommended for production use as it alters inventory history. """ movement = db.query(InventoryMovement).filter(InventoryMovement.id == movement_id).first() if not movement: return None # Start transaction try: # Get the product and prepare to revert the stock change product = ( db.query(Product).filter(Product.id == movement.product_id).with_for_update().first() ) if not product: raise ValueError(f"Product with ID {movement.product_id} not found") # Revert the stock change based on movement type if movement.type in [MovementType.STOCK_IN, MovementType.RETURN]: product.current_stock -= movement.quantity elif movement.type == MovementType.STOCK_OUT: product.current_stock += movement.quantity elif movement.type == MovementType.ADJUSTMENT: # For adjustments, reverse the quantity effect product.current_stock -= movement.quantity # Delete the movement db.delete(movement) db.add(product) db.commit() return movement except Exception as e: db.rollback() raise e