
Features implemented: - Product management with CRUD operations - Category and supplier management - Stock movement tracking with automatic updates - Inventory reports and analytics - SQLite database with Alembic migrations - Health monitoring endpoints - CORS configuration for API access - Comprehensive API documentation - Code quality with Ruff linting and formatting The system provides a complete backend solution for small business inventory management with proper database relationships, stock tracking, and reporting capabilities.
154 lines
4.7 KiB
Python
154 lines
4.7 KiB
Python
from typing import List, Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.orm import Session
|
|
from app.db.session import get_db
|
|
from app.models.product import Product
|
|
from app.models.stock_movement import StockMovement, MovementType
|
|
from app.schemas.product import Product as ProductSchema, ProductCreate, ProductUpdate
|
|
|
|
router = APIRouter(prefix="/products", tags=["products"])
|
|
|
|
|
|
@router.get("/", response_model=List[ProductSchema])
|
|
def get_products(
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(100, ge=1, le=1000),
|
|
category_id: Optional[int] = None,
|
|
supplier_id: Optional[int] = None,
|
|
low_stock: bool = False,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
query = db.query(Product)
|
|
|
|
if category_id:
|
|
query = query.filter(Product.category_id == category_id)
|
|
if supplier_id:
|
|
query = query.filter(Product.supplier_id == supplier_id)
|
|
if low_stock:
|
|
query = query.filter(Product.quantity_in_stock <= Product.minimum_stock_level)
|
|
|
|
return query.offset(skip).limit(limit).all()
|
|
|
|
|
|
@router.get("/{product_id}", response_model=ProductSchema)
|
|
def get_product(product_id: int, db: Session = Depends(get_db)):
|
|
product = db.query(Product).filter(Product.id == product_id).first()
|
|
if not product:
|
|
raise HTTPException(status_code=404, detail="Product not found")
|
|
return product
|
|
|
|
|
|
@router.post("/", response_model=ProductSchema)
|
|
def create_product(product: ProductCreate, db: Session = Depends(get_db)):
|
|
existing_product = db.query(Product).filter(Product.sku == product.sku).first()
|
|
if existing_product:
|
|
raise HTTPException(status_code=400, detail="SKU already exists")
|
|
|
|
db_product = Product(**product.dict())
|
|
db.add(db_product)
|
|
db.commit()
|
|
db.refresh(db_product)
|
|
return db_product
|
|
|
|
|
|
@router.put("/{product_id}", response_model=ProductSchema)
|
|
def update_product(
|
|
product_id: int, product: ProductUpdate, db: Session = Depends(get_db)
|
|
):
|
|
db_product = db.query(Product).filter(Product.id == product_id).first()
|
|
if not db_product:
|
|
raise HTTPException(status_code=404, detail="Product not found")
|
|
|
|
update_data = product.dict(exclude_unset=True)
|
|
if "sku" in update_data:
|
|
existing_product = (
|
|
db.query(Product)
|
|
.filter(Product.sku == update_data["sku"], Product.id != product_id)
|
|
.first()
|
|
)
|
|
if existing_product:
|
|
raise HTTPException(status_code=400, detail="SKU already exists")
|
|
|
|
for field, value in update_data.items():
|
|
setattr(db_product, field, value)
|
|
|
|
db.commit()
|
|
db.refresh(db_product)
|
|
return db_product
|
|
|
|
|
|
@router.delete("/{product_id}")
|
|
def delete_product(product_id: int, db: Session = Depends(get_db)):
|
|
db_product = db.query(Product).filter(Product.id == product_id).first()
|
|
if not db_product:
|
|
raise HTTPException(status_code=404, detail="Product not found")
|
|
|
|
db.delete(db_product)
|
|
db.commit()
|
|
return {"message": "Product deleted successfully"}
|
|
|
|
|
|
@router.post("/{product_id}/stock/add")
|
|
def add_stock(
|
|
product_id: int,
|
|
quantity: int,
|
|
reference: Optional[str] = None,
|
|
notes: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
db_product = db.query(Product).filter(Product.id == product_id).first()
|
|
if not db_product:
|
|
raise HTTPException(status_code=404, detail="Product not found")
|
|
|
|
db_product.quantity_in_stock += quantity
|
|
|
|
movement = StockMovement(
|
|
product_id=product_id,
|
|
movement_type=MovementType.IN,
|
|
quantity=quantity,
|
|
reference=reference,
|
|
notes=notes,
|
|
)
|
|
db.add(movement)
|
|
db.commit()
|
|
db.refresh(db_product)
|
|
|
|
return {
|
|
"message": f"Added {quantity} units to stock",
|
|
"new_stock_level": db_product.quantity_in_stock,
|
|
}
|
|
|
|
|
|
@router.post("/{product_id}/stock/remove")
|
|
def remove_stock(
|
|
product_id: int,
|
|
quantity: int,
|
|
reference: Optional[str] = None,
|
|
notes: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
db_product = db.query(Product).filter(Product.id == product_id).first()
|
|
if not db_product:
|
|
raise HTTPException(status_code=404, detail="Product not found")
|
|
|
|
if db_product.quantity_in_stock < quantity:
|
|
raise HTTPException(status_code=400, detail="Insufficient stock")
|
|
|
|
db_product.quantity_in_stock -= quantity
|
|
|
|
movement = StockMovement(
|
|
product_id=product_id,
|
|
movement_type=MovementType.OUT,
|
|
quantity=quantity,
|
|
reference=reference,
|
|
notes=notes,
|
|
)
|
|
db.add(movement)
|
|
db.commit()
|
|
db.refresh(db_product)
|
|
|
|
return {
|
|
"message": f"Removed {quantity} units from stock",
|
|
"new_stock_level": db_product.quantity_in_stock,
|
|
}
|