Automated Action c8aed27755 Create SaaS invoicing application with FastAPI and SQLite
- Set up project structure with modular organization
- Implement database models for users, organizations, clients, invoices
- Create Alembic migration scripts for database setup
- Implement JWT-based authentication and authorization
- Create API endpoints for users, organizations, clients, invoices
- Add PDF generation for invoices using ReportLab
- Add comprehensive documentation in README
2025-06-06 11:21:11 +00:00

201 lines
6.1 KiB
Python

from typing import Any, List
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Response
from sqlalchemy.orm import Session
from app import crud, models, schemas
from app.api import deps
from app.services.pdf_generator import generate_invoice_pdf
router = APIRouter()
@router.get("/", response_model=List[schemas.Invoice])
def read_invoices(
db: Session = Depends(deps.get_db),
skip: int = 0,
limit: int = 100,
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Retrieve invoices.
"""
if crud.user.is_superuser(current_user):
invoices = crud.invoice.get_multi(db, skip=skip, limit=limit)
elif current_user.organization_id:
# Filter invoices by the user's organization
invoices = (
db.query(models.Invoice)
.filter(models.Invoice.organization_id == current_user.organization_id)
.offset(skip)
.limit(limit)
.all()
)
else:
invoices = []
return invoices
@router.post("/", response_model=schemas.Invoice)
def create_invoice(
*,
db: Session = Depends(deps.get_db),
invoice_in: schemas.InvoiceCreate,
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Create new invoice.
"""
# Verify organization access
if not crud.user.is_superuser(current_user) and (
current_user.organization_id != invoice_in.organization_id
):
raise HTTPException(status_code=400, detail="Not enough permissions")
# Verify client belongs to the organization
client = crud.client.get(db, id=invoice_in.client_id)
if not client or client.organization_id != invoice_in.organization_id:
raise HTTPException(status_code=400, detail="Invalid client for this organization")
# Create invoice
invoice_data = invoice_in.dict(exclude={"items"})
invoice = models.Invoice(**invoice_data, created_by_id=current_user.id)
db.add(invoice)
db.commit()
db.refresh(invoice)
# Create invoice items
for item_data in invoice_in.items:
item = models.InvoiceItem(**item_data.dict(), invoice_id=invoice.id)
db.add(item)
db.commit()
db.refresh(invoice)
return invoice
@router.get("/{id}", response_model=schemas.Invoice)
def read_invoice(
*,
db: Session = Depends(deps.get_db),
id: int,
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Get invoice by ID.
"""
invoice = crud.invoice.get(db, id=id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
if not crud.user.is_superuser(current_user) and (
current_user.organization_id != invoice.organization_id
):
raise HTTPException(status_code=400, detail="Not enough permissions")
return invoice
@router.put("/{id}", response_model=schemas.Invoice)
def update_invoice(
*,
db: Session = Depends(deps.get_db),
id: int,
invoice_in: schemas.InvoiceUpdate,
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Update an invoice.
"""
invoice = crud.invoice.get(db, id=id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
if not crud.user.is_superuser(current_user) and (
current_user.organization_id != invoice.organization_id
):
raise HTTPException(status_code=400, detail="Not enough permissions")
# Prevent changing organization_id if not a superuser
if (
invoice_in.organization_id is not None
and invoice_in.organization_id != invoice.organization_id
and not crud.user.is_superuser(current_user)
):
raise HTTPException(
status_code=400, detail="Cannot change invoice's organization"
)
# Update basic invoice data
update_data = invoice_in.dict(exclude={"items"}, exclude_unset=True)
for field, value in update_data.items():
setattr(invoice, field, value)
# Update invoice items if provided
if invoice_in.items:
# Delete existing items
db.query(models.InvoiceItem).filter(models.InvoiceItem.invoice_id == id).delete()
# Create new items
for item_data in invoice_in.items:
item = models.InvoiceItem(**item_data.dict(), invoice_id=id)
db.add(item)
db.commit()
db.refresh(invoice)
return invoice
@router.delete("/{id}", response_model=schemas.Invoice)
def delete_invoice(
*,
db: Session = Depends(deps.get_db),
id: int,
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Delete an invoice.
"""
invoice = crud.invoice.get(db, id=id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
if not crud.user.is_superuser(current_user) and (
current_user.organization_id != invoice.organization_id
):
raise HTTPException(status_code=400, detail="Not enough permissions")
invoice = crud.invoice.remove(db, id=id)
return invoice
@router.get("/{id}/pdf", response_class=Response)
def generate_pdf(
*,
db: Session = Depends(deps.get_db),
id: int,
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Generate a PDF for the invoice.
"""
invoice = crud.invoice.get(db, id=id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
if not crud.user.is_superuser(current_user) and (
current_user.organization_id != invoice.organization_id
):
raise HTTPException(status_code=400, detail="Not enough permissions")
# Generate PDF
pdf_content = generate_invoice_pdf(db, invoice)
# Return PDF as response
filename = f"invoice_{invoice.invoice_number}_{datetime.now().strftime('%Y%m%d')}.pdf"
return Response(
content=pdf_content,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)