Exception handling enhancement
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
# app/services/stock_service.py
|
||||
"""Summary description ....
|
||||
"""
|
||||
Stock service for managing inventory operations.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- ....
|
||||
- ....
|
||||
- ....
|
||||
- Stock quantity management (set, add, remove)
|
||||
- Stock information retrieval and validation
|
||||
- Location-based inventory tracking
|
||||
- GTIN normalization and validation
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -13,6 +15,15 @@ from typing import List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
StockNotFoundException,
|
||||
InsufficientStockException,
|
||||
InvalidStockOperationException,
|
||||
StockValidationException,
|
||||
NegativeStockException,
|
||||
InvalidQuantityException,
|
||||
ValidationException,
|
||||
)
|
||||
from models.schemas.stock import (StockAdd, StockCreate, StockLocationResponse,
|
||||
StockSummaryResponse, StockUpdate)
|
||||
from models.database.product import Product
|
||||
@@ -29,236 +40,544 @@ class StockService:
|
||||
"""Class constructor."""
|
||||
self.gtin_processor = GTINProcessor()
|
||||
|
||||
def normalize_gtin(self, gtin_value) -> Optional[str]:
|
||||
"""Normalize GTIN format using the GTINProcessor."""
|
||||
return self.gtin_processor.normalize(gtin_value)
|
||||
|
||||
def set_stock(self, db: Session, stock_data: StockCreate) -> Stock:
|
||||
"""Set exact stock quantity for a GTIN at a specific location (replaces existing quantity)."""
|
||||
normalized_gtin = self.normalize_gtin(stock_data.gtin)
|
||||
if not normalized_gtin:
|
||||
raise ValueError("Invalid GTIN format")
|
||||
"""
|
||||
Set exact stock quantity for a GTIN at a specific location (replaces existing quantity).
|
||||
|
||||
location = stock_data.location.strip().upper()
|
||||
Args:
|
||||
db: Database session
|
||||
stock_data: Stock creation data
|
||||
|
||||
# Check if stock entry already exists for this GTIN and location
|
||||
existing_stock = (
|
||||
db.query(Stock)
|
||||
.filter(Stock.gtin == normalized_gtin, Stock.location == location)
|
||||
.first()
|
||||
)
|
||||
Returns:
|
||||
Stock object with updated quantity
|
||||
|
||||
if existing_stock:
|
||||
# Update existing stock (SET to exact quantity)
|
||||
old_quantity = existing_stock.quantity
|
||||
existing_stock.quantity = stock_data.quantity
|
||||
existing_stock.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(existing_stock)
|
||||
logger.info(
|
||||
f"Updated stock for GTIN {normalized_gtin} at {location}: {old_quantity} → {stock_data.quantity}"
|
||||
)
|
||||
return existing_stock
|
||||
else:
|
||||
# Create new stock entry
|
||||
new_stock = Stock(
|
||||
gtin=normalized_gtin, location=location, quantity=stock_data.quantity
|
||||
)
|
||||
db.add(new_stock)
|
||||
db.commit()
|
||||
db.refresh(new_stock)
|
||||
logger.info(
|
||||
f"Created new stock for GTIN {normalized_gtin} at {location}: {stock_data.quantity}"
|
||||
)
|
||||
return new_stock
|
||||
Raises:
|
||||
InvalidQuantityException: If quantity is negative
|
||||
StockValidationException: If GTIN or location is invalid
|
||||
"""
|
||||
try:
|
||||
# Validate and normalize input
|
||||
normalized_gtin = self._validate_and_normalize_gtin(stock_data.gtin)
|
||||
location = self._validate_and_normalize_location(stock_data.location)
|
||||
self._validate_quantity(stock_data.quantity, allow_zero=True)
|
||||
|
||||
# Check if stock entry already exists for this GTIN and location
|
||||
existing_stock = self._get_stock_entry(db, normalized_gtin, location)
|
||||
|
||||
if existing_stock:
|
||||
# Update existing stock (SET to exact quantity)
|
||||
old_quantity = existing_stock.quantity
|
||||
existing_stock.quantity = stock_data.quantity
|
||||
existing_stock.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(existing_stock)
|
||||
|
||||
logger.info(
|
||||
f"Updated stock for GTIN {normalized_gtin} at {location}: {old_quantity} → {stock_data.quantity}"
|
||||
)
|
||||
return existing_stock
|
||||
else:
|
||||
# Create new stock entry
|
||||
new_stock = Stock(
|
||||
gtin=normalized_gtin,
|
||||
location=location,
|
||||
quantity=stock_data.quantity
|
||||
)
|
||||
db.add(new_stock)
|
||||
db.commit()
|
||||
db.refresh(new_stock)
|
||||
|
||||
logger.info(
|
||||
f"Created new stock for GTIN {normalized_gtin} at {location}: {stock_data.quantity}"
|
||||
)
|
||||
return new_stock
|
||||
|
||||
except (InvalidQuantityException, StockValidationException):
|
||||
db.rollback()
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error setting stock: {str(e)}")
|
||||
raise ValidationException("Failed to set stock")
|
||||
|
||||
def add_stock(self, db: Session, stock_data: StockAdd) -> Stock:
|
||||
"""Add quantity to existing stock for a GTIN at a specific location (adds to existing quantity)."""
|
||||
normalized_gtin = self.normalize_gtin(stock_data.gtin)
|
||||
if not normalized_gtin:
|
||||
raise ValueError("Invalid GTIN format")
|
||||
"""
|
||||
Add quantity to existing stock for a GTIN at a specific location (adds to existing quantity).
|
||||
|
||||
location = stock_data.location.strip().upper()
|
||||
Args:
|
||||
db: Database session
|
||||
stock_data: Stock addition data
|
||||
|
||||
# Check if stock entry already exists for this GTIN and location
|
||||
existing_stock = (
|
||||
db.query(Stock)
|
||||
.filter(Stock.gtin == normalized_gtin, Stock.location == location)
|
||||
.first()
|
||||
)
|
||||
Returns:
|
||||
Stock object with updated quantity
|
||||
|
||||
if existing_stock:
|
||||
# Add to existing stock
|
||||
Raises:
|
||||
InvalidQuantityException: If quantity is not positive
|
||||
StockValidationException: If GTIN or location is invalid
|
||||
"""
|
||||
try:
|
||||
# Validate and normalize input
|
||||
normalized_gtin = self._validate_and_normalize_gtin(stock_data.gtin)
|
||||
location = self._validate_and_normalize_location(stock_data.location)
|
||||
self._validate_quantity(stock_data.quantity, allow_zero=False)
|
||||
|
||||
# Check if stock entry already exists for this GTIN and location
|
||||
existing_stock = self._get_stock_entry(db, normalized_gtin, location)
|
||||
|
||||
if existing_stock:
|
||||
# Add to existing stock
|
||||
old_quantity = existing_stock.quantity
|
||||
existing_stock.quantity += stock_data.quantity
|
||||
existing_stock.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(existing_stock)
|
||||
|
||||
logger.info(
|
||||
f"Added stock for GTIN {normalized_gtin} at {location}: "
|
||||
f"{old_quantity} + {stock_data.quantity} = {existing_stock.quantity}"
|
||||
)
|
||||
return existing_stock
|
||||
else:
|
||||
# Create new stock entry with the quantity
|
||||
new_stock = Stock(
|
||||
gtin=normalized_gtin,
|
||||
location=location,
|
||||
quantity=stock_data.quantity
|
||||
)
|
||||
db.add(new_stock)
|
||||
db.commit()
|
||||
db.refresh(new_stock)
|
||||
|
||||
logger.info(
|
||||
f"Created new stock for GTIN {normalized_gtin} at {location}: {stock_data.quantity}"
|
||||
)
|
||||
return new_stock
|
||||
|
||||
except (InvalidQuantityException, StockValidationException):
|
||||
db.rollback()
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error adding stock: {str(e)}")
|
||||
raise ValidationException("Failed to add stock")
|
||||
|
||||
def remove_stock(self, db: Session, stock_data: StockAdd) -> Stock:
|
||||
"""
|
||||
Remove quantity from existing stock for a GTIN at a specific location.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
stock_data: Stock removal data
|
||||
|
||||
Returns:
|
||||
Stock object with updated quantity
|
||||
|
||||
Raises:
|
||||
StockNotFoundException: If no stock found for GTIN/location
|
||||
InsufficientStockException: If not enough stock available
|
||||
InvalidQuantityException: If quantity is not positive
|
||||
NegativeStockException: If operation would result in negative stock
|
||||
"""
|
||||
try:
|
||||
# Validate and normalize input
|
||||
normalized_gtin = self._validate_and_normalize_gtin(stock_data.gtin)
|
||||
location = self._validate_and_normalize_location(stock_data.location)
|
||||
self._validate_quantity(stock_data.quantity, allow_zero=False)
|
||||
|
||||
# Find existing stock entry
|
||||
existing_stock = self._get_stock_entry(db, normalized_gtin, location)
|
||||
if not existing_stock:
|
||||
raise StockNotFoundException(normalized_gtin, identifier_type="gtin")
|
||||
|
||||
# Check if we have enough stock to remove
|
||||
if existing_stock.quantity < stock_data.quantity:
|
||||
raise InsufficientStockException(
|
||||
gtin=normalized_gtin,
|
||||
location=location,
|
||||
requested=stock_data.quantity,
|
||||
available=existing_stock.quantity
|
||||
)
|
||||
|
||||
# Remove from existing stock
|
||||
old_quantity = existing_stock.quantity
|
||||
existing_stock.quantity += stock_data.quantity
|
||||
new_quantity = existing_stock.quantity - stock_data.quantity
|
||||
|
||||
# Validate resulting quantity
|
||||
if new_quantity < 0:
|
||||
raise NegativeStockException(normalized_gtin, location, new_quantity)
|
||||
|
||||
existing_stock.quantity = new_quantity
|
||||
existing_stock.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(existing_stock)
|
||||
|
||||
logger.info(
|
||||
f"Added stock for GTIN {normalized_gtin} at {location}: "
|
||||
f"{old_quantity} + {stock_data.quantity} = {existing_stock.quantity}"
|
||||
f"Removed stock for GTIN {normalized_gtin} at {location}: "
|
||||
f"{old_quantity} - {stock_data.quantity} = {existing_stock.quantity}"
|
||||
)
|
||||
return existing_stock
|
||||
else:
|
||||
# Create new stock entry with the quantity
|
||||
new_stock = Stock(
|
||||
gtin=normalized_gtin, location=location, quantity=stock_data.quantity
|
||||
|
||||
except (StockNotFoundException, InsufficientStockException, InvalidQuantityException, NegativeStockException):
|
||||
db.rollback()
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error removing stock: {str(e)}")
|
||||
raise ValidationException("Failed to remove stock")
|
||||
|
||||
def get_stock_by_gtin(self, db: Session, gtin: str) -> StockSummaryResponse:
|
||||
"""
|
||||
Get all stock locations and total quantity for a specific GTIN.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
gtin: GTIN to look up
|
||||
|
||||
Returns:
|
||||
StockSummaryResponse with locations and totals
|
||||
|
||||
Raises:
|
||||
StockNotFoundException: If no stock found for GTIN
|
||||
StockValidationException: If GTIN is invalid
|
||||
"""
|
||||
try:
|
||||
normalized_gtin = self._validate_and_normalize_gtin(gtin)
|
||||
|
||||
# Get all stock entries for this GTIN
|
||||
stock_entries = db.query(Stock).filter(Stock.gtin == normalized_gtin).all()
|
||||
|
||||
if not stock_entries:
|
||||
raise StockNotFoundException(normalized_gtin, identifier_type="gtin")
|
||||
|
||||
# Calculate total quantity and build locations list
|
||||
total_quantity = 0
|
||||
locations = []
|
||||
|
||||
for entry in stock_entries:
|
||||
total_quantity += entry.quantity
|
||||
locations.append(
|
||||
StockLocationResponse(location=entry.location, quantity=entry.quantity)
|
||||
)
|
||||
|
||||
# Try to get product title for reference
|
||||
product = db.query(Product).filter(Product.gtin == normalized_gtin).first()
|
||||
product_title = product.title if product else None
|
||||
|
||||
return StockSummaryResponse(
|
||||
gtin=normalized_gtin,
|
||||
total_quantity=total_quantity,
|
||||
locations=locations,
|
||||
product_title=product_title,
|
||||
)
|
||||
db.add(new_stock)
|
||||
|
||||
except (StockNotFoundException, StockValidationException):
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting stock by GTIN {gtin}: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve stock information")
|
||||
|
||||
def get_total_stock(self, db: Session, gtin: str) -> dict:
|
||||
"""
|
||||
Get total quantity in stock for a specific GTIN.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
gtin: GTIN to look up
|
||||
|
||||
Returns:
|
||||
Dictionary with total stock information
|
||||
|
||||
Raises:
|
||||
StockNotFoundException: If no stock found for GTIN
|
||||
StockValidationException: If GTIN is invalid
|
||||
"""
|
||||
try:
|
||||
normalized_gtin = self._validate_and_normalize_gtin(gtin)
|
||||
|
||||
# Calculate total stock
|
||||
total_stock = db.query(Stock).filter(Stock.gtin == normalized_gtin).all()
|
||||
|
||||
if not total_stock:
|
||||
raise StockNotFoundException(normalized_gtin, identifier_type="gtin")
|
||||
|
||||
total_quantity = sum(entry.quantity for entry in total_stock)
|
||||
|
||||
# Get product info for context
|
||||
product = db.query(Product).filter(Product.gtin == normalized_gtin).first()
|
||||
|
||||
return {
|
||||
"gtin": normalized_gtin,
|
||||
"total_quantity": total_quantity,
|
||||
"product_title": product.title if product else None,
|
||||
"locations_count": len(total_stock),
|
||||
}
|
||||
|
||||
except (StockNotFoundException, StockValidationException):
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting total stock for GTIN {gtin}: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve total stock")
|
||||
|
||||
def get_all_stock(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
location: Optional[str] = None,
|
||||
gtin: Optional[str] = None,
|
||||
) -> List[Stock]:
|
||||
"""
|
||||
Get all stock entries with optional filtering.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
skip: Number of records to skip
|
||||
limit: Maximum records to return
|
||||
location: Optional location filter
|
||||
gtin: Optional GTIN filter
|
||||
|
||||
Returns:
|
||||
List of Stock objects
|
||||
"""
|
||||
try:
|
||||
query = db.query(Stock)
|
||||
|
||||
if location:
|
||||
query = query.filter(Stock.location.ilike(f"%{location}%"))
|
||||
|
||||
if gtin:
|
||||
normalized_gtin = self._normalize_gtin(gtin)
|
||||
if normalized_gtin:
|
||||
query = query.filter(Stock.gtin == normalized_gtin)
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting all stock: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve stock entries")
|
||||
|
||||
def update_stock(
|
||||
self, db: Session, stock_id: int, stock_update: StockUpdate
|
||||
) -> Stock:
|
||||
"""
|
||||
Update stock quantity for a specific stock entry.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
stock_id: Stock entry ID
|
||||
stock_update: Update data
|
||||
|
||||
Returns:
|
||||
Updated Stock object
|
||||
|
||||
Raises:
|
||||
StockNotFoundException: If stock entry not found
|
||||
InvalidQuantityException: If quantity is invalid
|
||||
"""
|
||||
try:
|
||||
stock_entry = self._get_stock_by_id_or_raise(db, stock_id)
|
||||
|
||||
# Validate new quantity
|
||||
self._validate_quantity(stock_update.quantity, allow_zero=True)
|
||||
|
||||
stock_entry.quantity = stock_update.quantity
|
||||
stock_entry.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(new_stock)
|
||||
db.refresh(stock_entry)
|
||||
|
||||
logger.info(
|
||||
f"Created new stock for GTIN {normalized_gtin} at {location}: {stock_data.quantity}"
|
||||
f"Updated stock entry {stock_id} to quantity {stock_update.quantity}"
|
||||
)
|
||||
return new_stock
|
||||
return stock_entry
|
||||
|
||||
def remove_stock(self, db: Session, stock_data: StockAdd) -> Stock:
|
||||
"""Remove quantity from existing stock for a GTIN at a specific location."""
|
||||
normalized_gtin = self.normalize_gtin(stock_data.gtin)
|
||||
except (StockNotFoundException, InvalidQuantityException):
|
||||
db.rollback()
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error updating stock {stock_id}: {str(e)}")
|
||||
raise ValidationException("Failed to update stock")
|
||||
|
||||
def delete_stock(self, db: Session, stock_id: int) -> bool:
|
||||
"""
|
||||
Delete a stock entry.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
stock_id: Stock entry ID
|
||||
|
||||
Returns:
|
||||
True if deletion successful
|
||||
|
||||
Raises:
|
||||
StockNotFoundException: If stock entry not found
|
||||
"""
|
||||
try:
|
||||
stock_entry = self._get_stock_by_id_or_raise(db, stock_id)
|
||||
|
||||
gtin = stock_entry.gtin
|
||||
location = stock_entry.location
|
||||
db.delete(stock_entry)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Deleted stock entry {stock_id} for GTIN {gtin} at {location}")
|
||||
return True
|
||||
|
||||
except StockNotFoundException:
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting stock {stock_id}: {str(e)}")
|
||||
raise ValidationException("Failed to delete stock entry")
|
||||
|
||||
def get_stock_summary_by_location(self, db: Session, location: str) -> dict:
|
||||
"""
|
||||
Get stock summary for a specific location.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
location: Location to summarize
|
||||
|
||||
Returns:
|
||||
Dictionary with location stock summary
|
||||
"""
|
||||
try:
|
||||
normalized_location = self._validate_and_normalize_location(location)
|
||||
|
||||
stock_entries = db.query(Stock).filter(Stock.location == normalized_location).all()
|
||||
|
||||
if not stock_entries:
|
||||
return {
|
||||
"location": normalized_location,
|
||||
"total_items": 0,
|
||||
"total_quantity": 0,
|
||||
"unique_gtins": 0,
|
||||
}
|
||||
|
||||
total_quantity = sum(entry.quantity for entry in stock_entries)
|
||||
unique_gtins = len(set(entry.gtin for entry in stock_entries))
|
||||
|
||||
return {
|
||||
"location": normalized_location,
|
||||
"total_items": len(stock_entries),
|
||||
"total_quantity": total_quantity,
|
||||
"unique_gtins": unique_gtins,
|
||||
}
|
||||
|
||||
except StockValidationException:
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting stock summary for location {location}: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve location stock summary")
|
||||
|
||||
def get_low_stock_items(self, db: Session, threshold: int = 10) -> List[dict]:
|
||||
"""
|
||||
Get items with stock below threshold.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
threshold: Stock threshold to consider "low"
|
||||
|
||||
Returns:
|
||||
List of low stock items with details
|
||||
"""
|
||||
try:
|
||||
if threshold < 0:
|
||||
raise InvalidQuantityException(threshold, "Threshold must be non-negative")
|
||||
|
||||
low_stock_entries = db.query(Stock).filter(Stock.quantity <= threshold).all()
|
||||
|
||||
low_stock_items = []
|
||||
for entry in low_stock_entries:
|
||||
# Get product info if available
|
||||
product = db.query(Product).filter(Product.gtin == entry.gtin).first()
|
||||
|
||||
low_stock_items.append({
|
||||
"gtin": entry.gtin,
|
||||
"location": entry.location,
|
||||
"current_quantity": entry.quantity,
|
||||
"product_title": product.title if product else None,
|
||||
"product_id": product.product_id if product else None,
|
||||
})
|
||||
|
||||
return low_stock_items
|
||||
|
||||
except InvalidQuantityException:
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting low stock items: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve low stock items")
|
||||
|
||||
# Private helper methods
|
||||
def _validate_and_normalize_gtin(self, gtin: str) -> str:
|
||||
"""Validate and normalize GTIN format."""
|
||||
if not gtin or not gtin.strip():
|
||||
raise StockValidationException("GTIN is required", field="gtin")
|
||||
|
||||
normalized_gtin = self._normalize_gtin(gtin)
|
||||
if not normalized_gtin:
|
||||
raise ValueError("Invalid GTIN format")
|
||||
raise StockValidationException("Invalid GTIN format", field="gtin")
|
||||
|
||||
location = stock_data.location.strip().upper()
|
||||
return normalized_gtin
|
||||
|
||||
# Find existing stock entry
|
||||
existing_stock = (
|
||||
def _validate_and_normalize_location(self, location: str) -> str:
|
||||
"""Validate and normalize location."""
|
||||
if not location or not location.strip():
|
||||
raise StockValidationException("Location is required", field="location")
|
||||
|
||||
return location.strip().upper()
|
||||
|
||||
def _validate_quantity(self, quantity: int, allow_zero: bool = True) -> None:
|
||||
"""Validate quantity value."""
|
||||
if quantity is None:
|
||||
raise InvalidQuantityException(quantity, "Quantity is required")
|
||||
|
||||
if not isinstance(quantity, int):
|
||||
raise InvalidQuantityException(quantity, "Quantity must be an integer")
|
||||
|
||||
if quantity < 0:
|
||||
raise InvalidQuantityException(quantity, "Quantity cannot be negative")
|
||||
|
||||
if not allow_zero and quantity == 0:
|
||||
raise InvalidQuantityException(quantity, "Quantity must be positive")
|
||||
|
||||
def _normalize_gtin(self, gtin_value) -> Optional[str]:
|
||||
"""Normalize GTIN format using the GTINProcessor."""
|
||||
try:
|
||||
return self.gtin_processor.normalize(gtin_value)
|
||||
except Exception as e:
|
||||
logger.error(f"Error normalizing GTIN {gtin_value}: {str(e)}")
|
||||
return None
|
||||
|
||||
def _get_stock_entry(self, db: Session, gtin: str, location: str) -> Optional[Stock]:
|
||||
"""Get stock entry by GTIN and location."""
|
||||
return (
|
||||
db.query(Stock)
|
||||
.filter(Stock.gtin == normalized_gtin, Stock.location == location)
|
||||
.filter(Stock.gtin == gtin, Stock.location == location)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not existing_stock:
|
||||
raise ValueError(
|
||||
f"No stock found for GTIN {normalized_gtin} at location {location}"
|
||||
)
|
||||
|
||||
# Check if we have enough stock to remove
|
||||
if existing_stock.quantity < stock_data.quantity:
|
||||
raise ValueError(
|
||||
f"Insufficient stock. Available: {existing_stock.quantity}, Requested to remove: {stock_data.quantity}"
|
||||
)
|
||||
|
||||
# Remove from existing stock
|
||||
old_quantity = existing_stock.quantity
|
||||
existing_stock.quantity -= stock_data.quantity
|
||||
existing_stock.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(existing_stock)
|
||||
logger.info(
|
||||
f"Removed stock for GTIN {normalized_gtin} at {location}: "
|
||||
f"{old_quantity} - {stock_data.quantity} = {existing_stock.quantity}"
|
||||
)
|
||||
return existing_stock
|
||||
|
||||
def get_stock_by_gtin(self, db: Session, gtin: str) -> StockSummaryResponse:
|
||||
"""Get all stock locations and total quantity for a specific GTIN."""
|
||||
normalized_gtin = self.normalize_gtin(gtin)
|
||||
if not normalized_gtin:
|
||||
raise ValueError("Invalid GTIN format")
|
||||
|
||||
# Get all stock entries for this GTIN
|
||||
stock_entries = db.query(Stock).filter(Stock.gtin == normalized_gtin).all()
|
||||
|
||||
if not stock_entries:
|
||||
raise ValueError(f"No stock found for GTIN: {gtin}")
|
||||
|
||||
# Calculate total quantity and build locations list
|
||||
total_quantity = 0
|
||||
locations = []
|
||||
|
||||
for entry in stock_entries:
|
||||
total_quantity += entry.quantity
|
||||
locations.append(
|
||||
StockLocationResponse(location=entry.location, quantity=entry.quantity)
|
||||
)
|
||||
|
||||
# Try to get product title for reference
|
||||
product = db.query(Product).filter(Product.gtin == normalized_gtin).first()
|
||||
product_title = product.title if product else None
|
||||
|
||||
return StockSummaryResponse(
|
||||
gtin=normalized_gtin,
|
||||
total_quantity=total_quantity,
|
||||
locations=locations,
|
||||
product_title=product_title,
|
||||
)
|
||||
|
||||
def get_total_stock(self, db: Session, gtin: str) -> dict:
|
||||
"""Get total quantity in stock for a specific GTIN."""
|
||||
normalized_gtin = self.normalize_gtin(gtin)
|
||||
if not normalized_gtin:
|
||||
raise ValueError("Invalid GTIN format")
|
||||
|
||||
# Calculate total stock
|
||||
total_stock = db.query(Stock).filter(Stock.gtin == normalized_gtin).all()
|
||||
total_quantity = sum(entry.quantity for entry in total_stock)
|
||||
|
||||
# Get product info for context
|
||||
product = db.query(Product).filter(Product.gtin == normalized_gtin).first()
|
||||
|
||||
return {
|
||||
"gtin": normalized_gtin,
|
||||
"total_quantity": total_quantity,
|
||||
"product_title": product.title if product else None,
|
||||
"locations_count": len(total_stock),
|
||||
}
|
||||
|
||||
def get_all_stock(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
location: Optional[str] = None,
|
||||
gtin: Optional[str] = None,
|
||||
) -> List[Stock]:
|
||||
"""Get all stock entries with optional filtering."""
|
||||
query = db.query(Stock)
|
||||
|
||||
if location:
|
||||
query = query.filter(Stock.location.ilike(f"%{location}%"))
|
||||
|
||||
if gtin:
|
||||
normalized_gtin = self.normalize_gtin(gtin)
|
||||
if normalized_gtin:
|
||||
query = query.filter(Stock.gtin == normalized_gtin)
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
def update_stock(
|
||||
self, db: Session, stock_id: int, stock_update: StockUpdate
|
||||
) -> Stock:
|
||||
"""Update stock quantity for a specific stock entry."""
|
||||
def _get_stock_by_id_or_raise(self, db: Session, stock_id: int) -> Stock:
|
||||
"""Get stock by ID or raise exception."""
|
||||
stock_entry = db.query(Stock).filter(Stock.id == stock_id).first()
|
||||
if not stock_entry:
|
||||
raise ValueError("Stock entry not found")
|
||||
|
||||
stock_entry.quantity = stock_update.quantity
|
||||
stock_entry.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(stock_entry)
|
||||
|
||||
logger.info(
|
||||
f"Updated stock entry {stock_id} to quantity {stock_update.quantity}"
|
||||
)
|
||||
raise StockNotFoundException(str(stock_id))
|
||||
return stock_entry
|
||||
|
||||
def delete_stock(self, db: Session, stock_id: int) -> bool:
|
||||
"""Delete a stock entry."""
|
||||
stock_entry = db.query(Stock).filter(Stock.id == stock_id).first()
|
||||
if not stock_entry:
|
||||
raise ValueError("Stock entry not found")
|
||||
|
||||
gtin = stock_entry.gtin
|
||||
location = stock_entry.location
|
||||
db.delete(stock_entry)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Deleted stock entry {stock_id} for GTIN {gtin} at {location}")
|
||||
return True
|
||||
# Legacy methods for backward compatibility (deprecated)
|
||||
def normalize_gtin(self, gtin_value) -> Optional[str]:
|
||||
"""Normalize GTIN format. DEPRECATED: Use _normalize_gtin."""
|
||||
logger.warning("normalize_gtin is deprecated, use _normalize_gtin")
|
||||
return self._normalize_gtin(gtin_value)
|
||||
|
||||
def get_stock_by_id(self, db: Session, stock_id: int) -> Optional[Stock]:
|
||||
"""Get a stock entry by its ID."""
|
||||
return db.query(Stock).filter(Stock.id == stock_id).first()
|
||||
"""Get stock by ID. DEPRECATED: Use proper exception handling."""
|
||||
logger.warning("get_stock_by_id is deprecated, use proper exception handling")
|
||||
try:
|
||||
return db.query(Stock).filter(Stock.id == stock_id).first()
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting stock by ID: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
# Create service instance
|
||||
|
||||
Reference in New Issue
Block a user