
- Set up project structure and FastAPI application - Create database models with SQLAlchemy - Implement authentication with JWT - Add CRUD operations for products, inventory, categories - Implement purchase order and sales functionality - Create reporting endpoints - Set up Alembic for database migrations - Add comprehensive documentation in README.md
168 lines
4.9 KiB
Python
168 lines
4.9 KiB
Python
from typing import Any, List, Optional
|
|
from decimal import Decimal
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.deps import get_db, get_current_active_user, get_current_admin_user
|
|
from app.crud.crud_sale import sale
|
|
from app.crud.crud_product import product
|
|
from app.crud.crud_inventory import inventory
|
|
from app.models.user import User as UserModel
|
|
from app.schemas.sale import Sale, SaleCreate, SaleUpdate
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/", response_model=List[Sale])
|
|
def read_sales(
|
|
db: Session = Depends(get_db),
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
status: Optional[str] = None,
|
|
current_user: UserModel = Depends(get_current_active_user),
|
|
) -> Any:
|
|
"""
|
|
Retrieve sales with their items.
|
|
Filter by status if provided.
|
|
"""
|
|
sales = sale.get_multi_with_items(db, skip=skip, limit=limit)
|
|
|
|
# Apply status filter
|
|
if status:
|
|
sales = [s for s in sales if s.status == status]
|
|
|
|
# Calculate total amount for each sale
|
|
for s in sales:
|
|
s.total_amount = sale.get_total_amount(db, id=s.id)
|
|
|
|
return sales
|
|
|
|
|
|
@router.post("/", response_model=Sale)
|
|
def create_sale(
|
|
*,
|
|
db: Session = Depends(get_db),
|
|
sale_in: SaleCreate,
|
|
current_user: UserModel = Depends(get_current_active_user),
|
|
) -> Any:
|
|
"""
|
|
Create new sale with items and update inventory.
|
|
"""
|
|
# Verify all products exist and have enough inventory
|
|
for item in sale_in.items:
|
|
# Check if product exists
|
|
prod = product.get(db, id=item.product_id)
|
|
if not prod:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Product with id {item.product_id} not found"
|
|
)
|
|
|
|
# Check if enough inventory is available
|
|
available = inventory.get_total_product_quantity(db, product_id=item.product_id)
|
|
if available < item.quantity:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Not enough inventory for product {prod.name}. Available: {available}, Requested: {item.quantity}"
|
|
)
|
|
|
|
# Create sale with items
|
|
new_sale = sale.create_with_items(
|
|
db, obj_in=sale_in, user_id=current_user.id
|
|
)
|
|
|
|
if not new_sale:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Failed to create sale, likely due to insufficient inventory"
|
|
)
|
|
|
|
# Calculate total amount
|
|
new_sale.total_amount = sale.get_total_amount(db, id=new_sale.id)
|
|
|
|
return new_sale
|
|
|
|
|
|
@router.get("/{id}", response_model=Sale)
|
|
def read_sale(
|
|
*,
|
|
db: Session = Depends(get_db),
|
|
id: int,
|
|
current_user: UserModel = Depends(get_current_active_user),
|
|
) -> Any:
|
|
"""
|
|
Get sale by ID with its items.
|
|
"""
|
|
sale_item = sale.get_with_items(db, id=id)
|
|
if not sale_item:
|
|
raise HTTPException(status_code=404, detail="Sale not found")
|
|
|
|
# Calculate total amount
|
|
sale_item.total_amount = sale.get_total_amount(db, id=sale_item.id)
|
|
|
|
return sale_item
|
|
|
|
|
|
@router.put("/{id}", response_model=Sale)
|
|
def update_sale(
|
|
*,
|
|
db: Session = Depends(get_db),
|
|
id: int,
|
|
sale_in: SaleUpdate,
|
|
current_user: UserModel = Depends(get_current_active_user),
|
|
) -> Any:
|
|
"""
|
|
Update a sale (but not its items).
|
|
Can only update completed sales that haven't been cancelled or returned.
|
|
"""
|
|
sale_item = sale.get(db, id=id)
|
|
if not sale_item:
|
|
raise HTTPException(status_code=404, detail="Sale not found")
|
|
|
|
# Only allow updates to completed sales
|
|
if sale_item.status != "completed":
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Cannot update sale with status {sale_item.status}. Only completed sales can be updated."
|
|
)
|
|
|
|
updated_sale = sale.update(db, db_obj=sale_item, obj_in=sale_in)
|
|
|
|
# Get full sale with items for response
|
|
result = sale.get_with_items(db, id=updated_sale.id)
|
|
|
|
# Calculate total amount
|
|
result.total_amount = sale.get_total_amount(db, id=result.id)
|
|
|
|
return result
|
|
|
|
|
|
@router.post("/{id}/cancel", response_model=Sale)
|
|
def cancel_sale(
|
|
*,
|
|
db: Session = Depends(get_db),
|
|
id: int,
|
|
current_user: UserModel = Depends(get_current_active_user),
|
|
) -> Any:
|
|
"""
|
|
Cancel a sale and return items to inventory.
|
|
"""
|
|
sale_item = sale.get(db, id=id)
|
|
if not sale_item:
|
|
raise HTTPException(status_code=404, detail="Sale not found")
|
|
|
|
# Only allow cancelling completed sales
|
|
if sale_item.status != "completed":
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Cannot cancel sale with status {sale_item.status}. Only completed sales can be cancelled."
|
|
)
|
|
|
|
# Update status and return to inventory
|
|
cancelled_sale = sale.cancel_sale(db, id=id)
|
|
|
|
# Calculate total amount
|
|
cancelled_sale.total_amount = sale.get_total_amount(db, id=cancelled_sale.id)
|
|
|
|
return cancelled_sale |