Refactoring code for modular approach
This commit is contained in:
315
app/api/v1/stock.py
Normal file
315
app/api/v1/stock.py
Normal file
@@ -0,0 +1,315 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.tasks.background_tasks import process_marketplace_import
|
||||
from middleware.decorators import rate_limit
|
||||
from models.api_models import MarketplaceImportJobResponse, MarketplaceImportRequest, StockResponse, \
|
||||
StockSummaryResponse
|
||||
from models.database_models import User, MarketplaceImportJob, Shop
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Stock Management Routes (Protected)
|
||||
|
||||
@router.post("/stock", response_model=StockResponse)
|
||||
def set_stock(stock: StockCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""Set exact stock quantity for a GTIN at a specific location (replaces existing quantity)"""
|
||||
|
||||
# Normalize GTIN
|
||||
def normalize_gtin(gtin_value):
|
||||
if not gtin_value:
|
||||
return None
|
||||
gtin_str = str(gtin_value).strip()
|
||||
if '.' in gtin_str:
|
||||
gtin_str = gtin_str.split('.')[0]
|
||||
gtin_clean = ''.join(filter(str.isdigit, gtin_str))
|
||||
if len(gtin_clean) in [8, 12, 13, 14]:
|
||||
return gtin_clean.zfill(13) if len(gtin_clean) == 13 else gtin_clean.zfill(12)
|
||||
return gtin_clean if gtin_clean else None
|
||||
|
||||
normalized_gtin = normalize_gtin(stock.gtin)
|
||||
if not normalized_gtin:
|
||||
raise HTTPException(status_code=400, detail="Invalid GTIN format")
|
||||
|
||||
# Check if stock entry already exists for this GTIN and location
|
||||
existing_stock = db.query(Stock).filter(
|
||||
Stock.gtin == normalized_gtin,
|
||||
Stock.location == stock.location.strip().upper()
|
||||
).first()
|
||||
|
||||
if existing_stock:
|
||||
# Update existing stock (SET to exact quantity)
|
||||
old_quantity = existing_stock.quantity
|
||||
existing_stock.quantity = stock.quantity
|
||||
existing_stock.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(existing_stock)
|
||||
logger.info(f"Updated stock for GTIN {normalized_gtin} at {stock.location}: {old_quantity} → {stock.quantity}")
|
||||
return existing_stock
|
||||
else:
|
||||
# Create new stock entry
|
||||
new_stock = Stock(
|
||||
gtin=normalized_gtin,
|
||||
location=stock.location.strip().upper(),
|
||||
quantity=stock.quantity
|
||||
)
|
||||
db.add(new_stock)
|
||||
db.commit()
|
||||
db.refresh(new_stock)
|
||||
logger.info(f"Created new stock for GTIN {normalized_gtin} at {stock.location}: {stock.quantity}")
|
||||
return new_stock
|
||||
|
||||
|
||||
@router.post("/stock/add", response_model=StockResponse)
|
||||
def add_stock(stock: StockAdd, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""Add quantity to existing stock for a GTIN at a specific location (adds to existing quantity)"""
|
||||
|
||||
# Normalize GTIN
|
||||
def normalize_gtin(gtin_value):
|
||||
if not gtin_value:
|
||||
return None
|
||||
gtin_str = str(gtin_value).strip()
|
||||
if '.' in gtin_str:
|
||||
gtin_str = gtin_str.split('.')[0]
|
||||
gtin_clean = ''.join(filter(str.isdigit, gtin_str))
|
||||
if len(gtin_clean) in [8, 12, 13, 14]:
|
||||
return gtin_clean.zfill(13) if len(gtin_clean) == 13 else gtin_clean.zfill(12)
|
||||
return gtin_clean if gtin_clean else None
|
||||
|
||||
normalized_gtin = normalize_gtin(stock.gtin)
|
||||
if not normalized_gtin:
|
||||
raise HTTPException(status_code=400, detail="Invalid GTIN format")
|
||||
|
||||
# Check if stock entry already exists for this GTIN and location
|
||||
existing_stock = db.query(Stock).filter(
|
||||
Stock.gtin == normalized_gtin,
|
||||
Stock.location == stock.location.strip().upper()
|
||||
).first()
|
||||
|
||||
if existing_stock:
|
||||
# Add to existing stock
|
||||
old_quantity = existing_stock.quantity
|
||||
existing_stock.quantity += stock.quantity
|
||||
existing_stock.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(existing_stock)
|
||||
logger.info(
|
||||
f"Added stock for GTIN {normalized_gtin} at {stock.location}: {old_quantity} + {stock.quantity} = {existing_stock.quantity}")
|
||||
return existing_stock
|
||||
else:
|
||||
# Create new stock entry with the quantity
|
||||
new_stock = Stock(
|
||||
gtin=normalized_gtin,
|
||||
location=stock.location.strip().upper(),
|
||||
quantity=stock.quantity
|
||||
)
|
||||
db.add(new_stock)
|
||||
db.commit()
|
||||
db.refresh(new_stock)
|
||||
logger.info(f"Created new stock for GTIN {normalized_gtin} at {stock.location}: {stock.quantity}")
|
||||
return new_stock
|
||||
|
||||
|
||||
@router.post("/stock/remove", response_model=StockResponse)
|
||||
def remove_stock(stock: StockAdd, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""Remove quantity from existing stock for a GTIN at a specific location"""
|
||||
|
||||
# Normalize GTIN
|
||||
def normalize_gtin(gtin_value):
|
||||
if not gtin_value:
|
||||
return None
|
||||
gtin_str = str(gtin_value).strip()
|
||||
if '.' in gtin_str:
|
||||
gtin_str = gtin_str.split('.')[0]
|
||||
gtin_clean = ''.join(filter(str.isdigit, gtin_str))
|
||||
if len(gtin_clean) in [8, 12, 13, 14]:
|
||||
return gtin_clean.zfill(13) if len(gtin_clean) == 13 else gtin_clean.zfill(12)
|
||||
return gtin_clean if gtin_clean else None
|
||||
|
||||
normalized_gtin = normalize_gtin(stock.gtin)
|
||||
if not normalized_gtin:
|
||||
raise HTTPException(status_code=400, detail="Invalid GTIN format")
|
||||
|
||||
# Find existing stock entry
|
||||
existing_stock = db.query(Stock).filter(
|
||||
Stock.gtin == normalized_gtin,
|
||||
Stock.location == stock.location.strip().upper()
|
||||
).first()
|
||||
|
||||
if not existing_stock:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No stock found for GTIN {normalized_gtin} at location {stock.location}"
|
||||
)
|
||||
|
||||
# Check if we have enough stock to remove
|
||||
if existing_stock.quantity < stock.quantity:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Insufficient stock. Available: {existing_stock.quantity}, Requested to remove: {stock.quantity}"
|
||||
)
|
||||
|
||||
# Remove from existing stock
|
||||
old_quantity = existing_stock.quantity
|
||||
existing_stock.quantity -= stock.quantity
|
||||
existing_stock.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(existing_stock)
|
||||
logger.info(
|
||||
f"Removed stock for GTIN {normalized_gtin} at {stock.location}: {old_quantity} - {stock.quantity} = {existing_stock.quantity}")
|
||||
return existing_stock
|
||||
|
||||
|
||||
@router.get("/stock/{gtin}", response_model=StockSummaryResponse)
|
||||
def get_stock_by_gtin(gtin: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""Get all stock locations and total quantity for a specific GTIN"""
|
||||
|
||||
# Normalize GTIN
|
||||
def normalize_gtin(gtin_value):
|
||||
if not gtin_value:
|
||||
return None
|
||||
gtin_str = str(gtin_value).strip()
|
||||
if '.' in gtin_str:
|
||||
gtin_str = gtin_str.split('.')[0]
|
||||
gtin_clean = ''.join(filter(str.isdigit, gtin_str))
|
||||
if len(gtin_clean) in [8, 12, 13, 14]:
|
||||
return gtin_clean.zfill(13) if len(gtin_clean) == 13 else gtin_clean.zfill(12)
|
||||
return gtin_clean if gtin_clean else None
|
||||
|
||||
normalized_gtin = normalize_gtin(gtin)
|
||||
if not normalized_gtin:
|
||||
raise HTTPException(status_code=400, detail="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 HTTPException(status_code=404, detail=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
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stock/{gtin}/total")
|
||||
def get_total_stock(gtin: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""Get total quantity in stock for a specific GTIN"""
|
||||
|
||||
# Normalize GTIN
|
||||
def normalize_gtin(gtin_value):
|
||||
if not gtin_value:
|
||||
return None
|
||||
gtin_str = str(gtin_value).strip()
|
||||
if '.' in gtin_str:
|
||||
gtin_str = gtin_str.split('.')[0]
|
||||
gtin_clean = ''.join(filter(str.isdigit, gtin_str))
|
||||
if len(gtin_clean) in [8, 12, 13, 14]:
|
||||
return gtin_clean.zfill(13) if len(gtin_clean) == 13 else gtin_clean.zfill(12)
|
||||
return gtin_clean if gtin_clean else None
|
||||
|
||||
normalized_gtin = normalize_gtin(gtin)
|
||||
if not normalized_gtin:
|
||||
raise HTTPException(status_code=400, detail="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)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/stock", response_model=List[StockResponse])
|
||||
def get_all_stock(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
location: Optional[str] = Query(None, description="Filter by location"),
|
||||
gtin: Optional[str] = Query(None, description="Filter by GTIN"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get all stock entries with optional filtering"""
|
||||
query = db.query(Stock)
|
||||
|
||||
if location:
|
||||
query = query.filter(Stock.location.ilike(f"%{location}%"))
|
||||
|
||||
if gtin:
|
||||
# Normalize GTIN for search
|
||||
def normalize_gtin(gtin_value):
|
||||
if not gtin_value:
|
||||
return None
|
||||
gtin_str = str(gtin_value).strip()
|
||||
if '.' in gtin_str:
|
||||
gtin_str = gtin_str.split('.')[0]
|
||||
gtin_clean = ''.join(filter(str.isdigit, gtin_str))
|
||||
if len(gtin_clean) in [8, 12, 13, 14]:
|
||||
return gtin_clean.zfill(13) if len(gtin_clean) == 13 else gtin_clean.zfill(12)
|
||||
return gtin_clean if gtin_clean else None
|
||||
|
||||
normalized_gtin = normalize_gtin(gtin)
|
||||
if normalized_gtin:
|
||||
query = query.filter(Stock.gtin == normalized_gtin)
|
||||
|
||||
stock_entries = query.offset(skip).limit(limit).all()
|
||||
return stock_entries
|
||||
|
||||
|
||||
@router.put("/stock/{stock_id}", response_model=StockResponse)
|
||||
def update_stock(stock_id: int, stock_update: StockUpdate, db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)):
|
||||
"""Update stock quantity for a specific stock entry"""
|
||||
stock_entry = db.query(Stock).filter(Stock.id == stock_id).first()
|
||||
if not stock_entry:
|
||||
raise HTTPException(status_code=404, detail="Stock entry not found")
|
||||
|
||||
stock_entry.quantity = stock_update.quantity
|
||||
stock_entry.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(stock_entry)
|
||||
return stock_entry
|
||||
|
||||
|
||||
@router.delete("/stock/{stock_id}")
|
||||
def delete_stock(stock_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""Delete a stock entry"""
|
||||
stock_entry = db.query(Stock).filter(Stock.id == stock_id).first()
|
||||
if not stock_entry:
|
||||
raise HTTPException(status_code=404, detail="Stock entry not found")
|
||||
|
||||
db.delete(stock_entry)
|
||||
db.commit()
|
||||
return {"message": "Stock entry deleted successfully"}
|
||||
|
||||
Reference in New Issue
Block a user