
- Fix unused imports in API endpoints - Add proper __all__ exports in model and schema modules - Add proper TYPE_CHECKING imports in models to prevent circular imports - Fix import order in migrations - Fix long lines in migration scripts - All ruff checks passing
250 lines
8.5 KiB
Python
250 lines
8.5 KiB
Python
from datetime import datetime, timedelta
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlalchemy import func
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app import crud, models
|
|
from app.api import deps
|
|
from app.models.inventory_transaction import TransactionType
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/inventory-summary")
|
|
def get_inventory_summary(
|
|
db: Session = Depends(deps.get_db),
|
|
current_user: models.User = Depends(deps.get_current_active_user),
|
|
) -> Any:
|
|
"""
|
|
Get summary of inventory status.
|
|
"""
|
|
# Get all products belonging to the user
|
|
products = crud.product.get_multi_by_owner(db, owner_id=current_user.id)
|
|
|
|
# Calculate summary statistics
|
|
total_products = len(products)
|
|
total_inventory_value = sum(p.quantity * p.cost for p in products)
|
|
total_retail_value = sum(p.quantity * p.price for p in products)
|
|
potential_profit = total_retail_value - total_inventory_value
|
|
|
|
low_stock_count = len([p for p in products if 0 < p.quantity <= p.reorder_level])
|
|
out_of_stock_count = len([p for p in products if p.quantity == 0])
|
|
in_stock_count = total_products - low_stock_count - out_of_stock_count
|
|
|
|
return {
|
|
"total_products": total_products,
|
|
"total_inventory_value": round(total_inventory_value, 2),
|
|
"total_retail_value": round(total_retail_value, 2),
|
|
"potential_profit": round(potential_profit, 2),
|
|
"inventory_status": {
|
|
"in_stock": in_stock_count,
|
|
"low_stock": low_stock_count,
|
|
"out_of_stock": out_of_stock_count
|
|
}
|
|
}
|
|
|
|
|
|
@router.get("/transaction-history")
|
|
def get_transaction_history(
|
|
db: Session = Depends(deps.get_db),
|
|
days: int = 30,
|
|
current_user: models.User = Depends(deps.get_current_active_user),
|
|
) -> Any:
|
|
"""
|
|
Get transaction history for the specified number of days.
|
|
"""
|
|
# Get all products belonging to the user
|
|
products = crud.product.get_multi_by_owner(db, owner_id=current_user.id)
|
|
product_ids = [p.id for p in products]
|
|
|
|
# Get date range
|
|
end_date = datetime.utcnow()
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
# Get transactions for those products within the date range
|
|
transactions = (
|
|
db.query(
|
|
models.InventoryTransaction.transaction_type,
|
|
func.count().label("count"),
|
|
func.sum(models.InventoryTransaction.quantity).label("total_quantity"),
|
|
)
|
|
.filter(
|
|
models.InventoryTransaction.product_id.in_(product_ids),
|
|
models.InventoryTransaction.transaction_date >= start_date,
|
|
models.InventoryTransaction.transaction_date <= end_date,
|
|
)
|
|
.group_by(models.InventoryTransaction.transaction_type)
|
|
.all()
|
|
)
|
|
|
|
# Format results
|
|
result = {}
|
|
for t_type in TransactionType:
|
|
result[t_type.value] = {"count": 0, "total_quantity": 0}
|
|
|
|
for t_type, count, total_quantity in transactions:
|
|
result[t_type.value] = {
|
|
"count": count,
|
|
"total_quantity": total_quantity or 0
|
|
}
|
|
|
|
return {
|
|
"period_days": days,
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"transactions_by_type": result
|
|
}
|
|
|
|
|
|
@router.get("/product-performance")
|
|
def get_product_performance(
|
|
db: Session = Depends(deps.get_db),
|
|
days: int = 30,
|
|
limit: int = 10,
|
|
current_user: models.User = Depends(deps.get_current_active_user),
|
|
) -> Any:
|
|
"""
|
|
Get top-selling products for the specified number of days.
|
|
"""
|
|
# Get all products belonging to the user
|
|
products = crud.product.get_multi_by_owner(db, owner_id=current_user.id)
|
|
product_dict = {p.id: p for p in products}
|
|
|
|
# Get date range
|
|
end_date = datetime.utcnow()
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
# Get sales transactions grouped by product
|
|
sales = (
|
|
db.query(
|
|
models.InventoryTransaction.product_id,
|
|
func.sum(models.InventoryTransaction.quantity).label("quantity_sold"),
|
|
)
|
|
.filter(
|
|
models.InventoryTransaction.product_id.in_(product_dict.keys()),
|
|
models.InventoryTransaction.transaction_type == TransactionType.SALE,
|
|
models.InventoryTransaction.transaction_date >= start_date,
|
|
models.InventoryTransaction.transaction_date <= end_date,
|
|
)
|
|
.group_by(models.InventoryTransaction.product_id)
|
|
.order_by(func.sum(models.InventoryTransaction.quantity).desc())
|
|
.limit(limit)
|
|
.all()
|
|
)
|
|
|
|
# Format results
|
|
top_selling = []
|
|
for product_id, quantity_sold in sales:
|
|
product = product_dict.get(product_id)
|
|
if product:
|
|
top_selling.append({
|
|
"id": product.id,
|
|
"name": product.name,
|
|
"sku": product.sku,
|
|
"quantity_sold": quantity_sold,
|
|
"revenue": round(quantity_sold * product.price, 2),
|
|
"profit": round(quantity_sold * (product.price - product.cost), 2),
|
|
})
|
|
|
|
return {
|
|
"period_days": days,
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"top_selling_products": top_selling
|
|
}
|
|
|
|
|
|
@router.get("/category-performance")
|
|
def get_category_performance(
|
|
db: Session = Depends(deps.get_db),
|
|
days: int = 30,
|
|
current_user: models.User = Depends(deps.get_current_active_user),
|
|
) -> Any:
|
|
"""
|
|
Get sales performance by category for the specified number of days.
|
|
"""
|
|
# Get all products belonging to the user
|
|
products = crud.product.get_multi_by_owner(db, owner_id=current_user.id)
|
|
|
|
# Collect category IDs
|
|
category_ids = set()
|
|
for p in products:
|
|
if p.category_id:
|
|
category_ids.add(p.category_id)
|
|
|
|
# Get categories
|
|
categories = crud.category.get_multi_by_ids(db, ids=list(category_ids))
|
|
category_dict = {c.id: c for c in categories}
|
|
|
|
# Get date range
|
|
end_date = datetime.utcnow()
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
# Get sales transactions for these products
|
|
sales = (
|
|
db.query(
|
|
models.Product.category_id,
|
|
func.sum(models.InventoryTransaction.quantity).label("quantity_sold"),
|
|
)
|
|
.join(
|
|
models.InventoryTransaction,
|
|
models.InventoryTransaction.product_id == models.Product.id
|
|
)
|
|
.filter(
|
|
models.Product.owner_id == current_user.id,
|
|
models.InventoryTransaction.transaction_type == TransactionType.SALE,
|
|
models.InventoryTransaction.transaction_date >= start_date,
|
|
models.InventoryTransaction.transaction_date <= end_date,
|
|
)
|
|
.group_by(models.Product.category_id)
|
|
.all()
|
|
)
|
|
|
|
# Format results
|
|
category_performance = []
|
|
for category_id, quantity_sold in sales:
|
|
# Skip None category
|
|
if not category_id:
|
|
continue
|
|
|
|
category = category_dict.get(category_id)
|
|
if category:
|
|
# Calculate revenue and profit for this category
|
|
revenue = 0
|
|
profit = 0
|
|
for p in products:
|
|
if p.category_id == category_id:
|
|
# Get sales for this specific product
|
|
product_sales = (
|
|
db.query(func.sum(models.InventoryTransaction.quantity))
|
|
.filter(
|
|
models.InventoryTransaction.product_id == p.id,
|
|
models.InventoryTransaction.transaction_type == TransactionType.SALE,
|
|
models.InventoryTransaction.transaction_date >= start_date,
|
|
models.InventoryTransaction.transaction_date <= end_date,
|
|
)
|
|
.scalar() or 0
|
|
)
|
|
|
|
revenue += product_sales * p.price
|
|
profit += product_sales * (p.price - p.cost)
|
|
|
|
category_performance.append({
|
|
"id": category.id,
|
|
"name": category.name,
|
|
"quantity_sold": quantity_sold,
|
|
"revenue": round(revenue, 2),
|
|
"profit": round(profit, 2),
|
|
})
|
|
|
|
# Sort by revenue
|
|
category_performance.sort(key=lambda x: x["revenue"], reverse=True)
|
|
|
|
return {
|
|
"period_days": days,
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"category_performance": category_performance
|
|
} |