Automated Action 5935f302dc Create Small Business Inventory Management System with FastAPI and SQLite
- 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
2025-05-16 08:53:15 +00:00

177 lines
5.9 KiB
Python

from typing import List, Optional, Dict, Any
from sqlalchemy import func
from sqlalchemy.orm import Session, joinedload
from decimal import Decimal
from app.crud.base import CRUDBase
from app.models.sale import Sale, SaleItem
from app.models.inventory import Inventory, InventoryTransaction
from app.schemas.sale import SaleCreate, SaleUpdate
class CRUDSale(CRUDBase[Sale, SaleCreate, SaleUpdate]):
def create_with_items(
self, db: Session, *, obj_in: SaleCreate, user_id: int
) -> Optional[Sale]:
"""Create sale with items and update inventory"""
# First check if we have enough inventory
for item in obj_in.items:
inventory_qty = db.query(func.sum(Inventory.quantity)).filter(
Inventory.product_id == item.product_id
).scalar() or 0
if inventory_qty < item.quantity:
# Not enough inventory
return None
# Create sale
db_obj = Sale(
customer_name=obj_in.customer_name,
notes=obj_in.notes,
status=obj_in.status,
created_by=user_id
)
db.add(db_obj)
db.flush()
# Create items and update inventory
for item in obj_in.items:
db_item = SaleItem(
sale_id=db_obj.id,
product_id=item.product_id,
quantity=item.quantity,
unit_price=item.unit_price
)
db.add(db_item)
# Update inventory - reduce quantities
self._reduce_inventory(
db,
product_id=item.product_id,
quantity=item.quantity,
sale_id=db_obj.id
)
db.commit()
db.refresh(db_obj)
return db_obj
def _reduce_inventory(
self, db: Session, *, product_id: int, quantity: int, sale_id: int
) -> None:
"""Reduce inventory for a product, starting with oldest inventory first"""
remaining = quantity
# Get all inventory for this product, ordered by id (assuming oldest first)
inventories = db.query(Inventory).filter(
Inventory.product_id == product_id,
Inventory.quantity > 0
).order_by(Inventory.id).all()
for inv in inventories:
if remaining <= 0:
break
if inv.quantity >= remaining:
# This inventory is enough to cover remaining quantity
inv.quantity -= remaining
# Record transaction
transaction = InventoryTransaction(
product_id=product_id,
quantity=-remaining, # Negative for reduction
transaction_type="sale",
reference_id=sale_id,
location=inv.location
)
db.add(transaction)
remaining = 0
else:
# Use all of this inventory and continue to next
remaining -= inv.quantity
# Record transaction
transaction = InventoryTransaction(
product_id=product_id,
quantity=-inv.quantity, # Negative for reduction
transaction_type="sale",
reference_id=sale_id,
location=inv.location
)
db.add(transaction)
inv.quantity = 0
def get_with_items(self, db: Session, id: int) -> Optional[Sale]:
"""Get sale with its items"""
return db.query(Sale).options(
joinedload(Sale.items).joinedload(SaleItem.product)
).filter(Sale.id == id).first()
def get_multi_with_items(
self, db: Session, *, skip: int = 0, limit: int = 100
) -> List[Sale]:
"""Get multiple sales with their items"""
return db.query(Sale).options(
joinedload(Sale.items).joinedload(SaleItem.product)
).offset(skip).limit(limit).all()
def cancel_sale(self, db: Session, *, id: int) -> Optional[Sale]:
"""Cancel a sale and return items to inventory"""
sale = self.get_with_items(db, id)
if not sale:
return None
if sale.status != "completed":
return sale # Already cancelled or returned
sale.status = "cancelled"
# Return items to inventory
for item in sale.items:
# Find or create inventory at default location
inventory = db.query(Inventory).filter(
Inventory.product_id == item.product_id,
Inventory.location == None # Default location
).first()
if not inventory:
inventory = Inventory(
product_id=item.product_id,
quantity=0,
location=None
)
db.add(inventory)
db.flush()
# Update quantity
inventory.quantity += item.quantity
# Record transaction
transaction = InventoryTransaction(
product_id=item.product_id,
quantity=item.quantity,
transaction_type="return",
reference_id=sale.id,
reason="Sale cancelled"
)
db.add(transaction)
db.commit()
db.refresh(sale)
return sale
def get_total_amount(self, db: Session, *, id: int) -> Decimal:
"""Calculate total amount for a sale"""
result = db.query(
func.sum(SaleItem.quantity * SaleItem.unit_price).label("total")
).filter(
SaleItem.sale_id == id
).scalar()
return result or Decimal("0.00")
sale = CRUDSale(Sale)