
- 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
206 lines
6.7 KiB
Python
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
|
|
) |