Automated Action 0b5aa51985 Add web frontend for invoice generation service
- Add Jinja2 templates and static files for web UI
- Create frontend routes for invoice management
- Implement home page with recent invoices list
- Add invoice creation form with dynamic items
- Create invoice search functionality
- Implement invoice details view with status update
- Add JavaScript for form validation and dynamic UI
- Update main.py to serve static files
- Update documentation
2025-05-18 21:43:44 +00:00

206 lines
6.7 KiB
Python

from datetime import datetime, timedelta
from typing import Optional
from fastapi import APIRouter, Depends, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models.invoice import Invoice, InvoiceItem
from app.schemas.invoice import InvoiceCreate, InvoiceItemCreate, InvoiceStatusUpdate
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
@router.get("/", response_class=HTMLResponse)
async def home(request: Request, db: Session = Depends(get_db)):
"""Render the home page with recent invoices."""
invoices = db.query(Invoice).order_by(Invoice.date_created.desc()).limit(10).all()
return templates.TemplateResponse(
"home.html", {"request": request, "invoices": invoices}
)
@router.get("/create", response_class=HTMLResponse)
async def create_invoice_page(request: Request):
"""Render the create invoice form."""
# Set default due date to 30 days from now
due_date = (datetime.now() + timedelta(days=30)).strftime("%Y-%m-%d")
return templates.TemplateResponse(
"create_invoice.html", {"request": request, "due_date": due_date}
)
@router.post("/create", response_class=HTMLResponse)
async def create_invoice(
request: Request,
customer_name: str = Form(...),
customer_email: Optional[str] = Form(None),
customer_address: Optional[str] = Form(None),
due_date: datetime = Form(...),
notes: Optional[str] = Form(None),
db: Session = Depends(get_db),
):
"""Process the invoice creation form and create a new invoice."""
form_data = await request.form()
# Extract invoice items from form data
items = []
item_index = 0
while True:
description_key = f"items[{item_index}][description]"
quantity_key = f"items[{item_index}][quantity]"
unit_price_key = f"items[{item_index}][unit_price]"
if description_key not in form_data:
break
description = form_data.get(description_key)
quantity = form_data.get(quantity_key)
unit_price = form_data.get(unit_price_key)
if description and quantity and unit_price:
items.append(
InvoiceItemCreate(
description=description,
quantity=float(quantity),
unit_price=float(unit_price),
)
)
item_index += 1
# Create the invoice
if not items:
# If no valid items, return to form with error
return templates.TemplateResponse(
"create_invoice.html",
{
"request": request,
"error": "At least one invoice item is required",
"customer_name": customer_name,
"customer_email": customer_email,
"customer_address": customer_address,
"due_date": due_date.strftime("%Y-%m-%d"),
"notes": notes,
},
)
invoice_data = InvoiceCreate(
customer_name=customer_name,
customer_email=customer_email,
customer_address=customer_address,
due_date=due_date,
notes=notes,
items=items,
)
# Create new invoice in database
from app.core.utils import generate_invoice_number
db_invoice = Invoice(
invoice_number=generate_invoice_number(),
date_created=datetime.utcnow(),
due_date=invoice_data.due_date,
customer_name=invoice_data.customer_name,
customer_email=invoice_data.customer_email,
customer_address=invoice_data.customer_address,
notes=invoice_data.notes,
status="PENDING",
total_amount=0, # Will be calculated from items
)
db.add(db_invoice)
db.flush() # Flush to get the ID
# Create invoice items
total_amount = 0
for item_data in invoice_data.items:
item_amount = item_data.quantity * item_data.unit_price
total_amount += item_amount
db_item = InvoiceItem(
invoice_id=db_invoice.id,
description=item_data.description,
quantity=item_data.quantity,
unit_price=item_data.unit_price,
amount=item_amount,
)
db.add(db_item)
# Update total amount
db_invoice.total_amount = total_amount
# Commit the transaction
db.commit()
db.refresh(db_invoice)
# Redirect to invoice details page
return RedirectResponse(
url=f"/invoice/{db_invoice.id}", status_code=303
) # 303 See Other
@router.get("/search", response_class=HTMLResponse)
async def search_invoice_page(request: Request):
"""Render the search invoice form."""
return templates.TemplateResponse("search_invoice.html", {"request": request})
@router.post("/search", response_class=HTMLResponse)
async def search_invoice(
request: Request, invoice_number: str = Form(...), db: Session = Depends(get_db)
):
"""Process the search form and find an invoice by number."""
invoice = (
db.query(Invoice).filter(Invoice.invoice_number == invoice_number).first()
)
if not invoice:
return templates.TemplateResponse(
"search_invoice.html",
{
"request": request,
"error": f"Invoice with number {invoice_number} not found",
"invoice_number": invoice_number,
},
)
# Redirect to invoice details page
return RedirectResponse(url=f"/invoice/{invoice.id}", status_code=303)
@router.get("/invoice/{invoice_id}", response_class=HTMLResponse)
async def invoice_details(request: Request, invoice_id: int, db: Session = Depends(get_db)):
"""Render the invoice details page."""
invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first()
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
return templates.TemplateResponse(
"invoice_details.html", {"request": request, "invoice": invoice}
)
@router.post("/invoice/{invoice_id}/status", response_class=HTMLResponse)
async def update_status(
request: Request,
invoice_id: int,
status: str = Form(...),
db: Session = Depends(get_db),
):
"""Update the status of an invoice."""
invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first()
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
status_update = InvoiceStatusUpdate(status=status)
invoice.status = status_update.status
db.commit()
return RedirectResponse(
url=f"/invoice/{invoice.id}", status_code=303
)