from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException, status from fastapi.responses import JSONResponse from sqlalchemy.orm import Session from app.core.database import get_db from app.core.utils import generate_invoice_number from app.models.invoice import Invoice, InvoiceItem from app.schemas.invoice import ( InvoiceCreate, InvoiceDB, InvoiceStatusUpdate, InvoiceUpdate, invoice_db_filterable, ) router = APIRouter() @router.post("/", response_model=InvoiceDB, status_code=status.HTTP_201_CREATED) def create_invoice(invoice_data: InvoiceCreate, db: Session = Depends(get_db)): """ Create a new invoice. """ # Generate unique invoice number invoice_number = generate_invoice_number() # Create new invoice db_invoice = Invoice( invoice_number=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 ) # Add to DB 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) return db_invoice @router.get("/") def get_invoices( skip: int = 0, limit: int = 100, fields: Optional[str] = None, status: Optional[str] = None, sort_order: Optional[str] = "desc", db: Session = Depends(get_db) ): """ Retrieve a list of invoices. Parameters: - skip: Number of records to skip - limit: Maximum number of records to return - fields: Comma-separated list of fields to include in the response (e.g., "id,invoice_number,total_amount") If not provided, all fields will be returned. - status: Filter invoices by status (e.g., "PENDING", "PAID", "CANCELLED") If not provided, all invoices regardless of status will be returned. - sort_order: Sort invoices by date_created field, either "asc" for ascending or "desc" for descending order. Default is "desc" (newest first). """ # Build the query with filters query = db.query(Invoice) # Apply status filter if provided if status: # Validate the status value allowed_statuses = ["PENDING", "PAID", "CANCELLED"] if status not in allowed_statuses: raise HTTPException( status_code=400, detail=f"Status must be one of {', '.join(allowed_statuses)}" ) # Apply the filter query = query.filter(Invoice.status == status) # Apply sorting if sort_order: # Validate sort_order value allowed_sort_orders = ["asc", "desc"] if sort_order.lower() not in allowed_sort_orders: raise HTTPException( status_code=400, detail=f"Sort order must be one of {', '.join(allowed_sort_orders)}" ) # Apply sorting based on sort_order if sort_order.lower() == "asc": query = query.order_by(Invoice.date_created.asc()) else: # Default to 'desc' query = query.order_by(Invoice.date_created.desc()) else: # Default sorting (descending - newest first) query = query.order_by(Invoice.date_created.desc()) # Apply pagination invoices = query.offset(skip).limit(limit).all() # If fields parameter is provided, filter the response if fields: # Process the response to include only the specified fields filtered_response = invoice_db_filterable.process_response(invoices, fields) return JSONResponse(content=filtered_response) # Otherwise, return all fields return invoices @router.get("/{invoice_id}") def get_invoice( invoice_id: int, fields: Optional[str] = None, db: Session = Depends(get_db) ): """ Retrieve a specific invoice by ID. Parameters: - invoice_id: ID of the invoice to retrieve - fields: Comma-separated list of fields to include in the response (e.g., "id,invoice_number,total_amount") If not provided, all fields will be returned. """ invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first() if invoice is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Invoice with ID {invoice_id} not found", ) # If fields parameter is provided, filter the response if fields: # Process the response to include only the specified fields filtered_response = invoice_db_filterable.process_response(invoice, fields) return JSONResponse(content=filtered_response) # Otherwise, return all fields return invoice @router.get("/find", response_model=InvoiceDB) def find_invoice_by_number( invoice_number: str, fields: Optional[str] = None, db: Session = Depends(get_db) ): """ Find an invoice by its invoice number. Parameters: - invoice_number: The invoice number to search for - fields: Comma-separated list of fields to include in the response (e.g., "id,invoice_number,total_amount") If not provided, all fields will be returned. """ invoice = db.query(Invoice).filter(Invoice.invoice_number == invoice_number).first() if invoice is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Invoice with number {invoice_number} not found", ) # If fields parameter is provided, filter the response if fields: # Process the response to include only the specified fields filtered_response = invoice_db_filterable.process_response(invoice, fields) return JSONResponse(content=filtered_response) # Otherwise, return all fields return invoice @router.patch("/{invoice_id}", response_model=InvoiceDB) def update_invoice( invoice_id: int, invoice_update: InvoiceUpdate, db: Session = Depends(get_db) ): """ Update an existing invoice. """ # Get the invoice invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first() if invoice is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Invoice with ID {invoice_id} not found", ) # Update invoice fields update_data = invoice_update.dict(exclude_unset=True) for field, value in update_data.items(): setattr(invoice, field, value) # Commit changes db.commit() db.refresh(invoice) return invoice @router.patch("/{invoice_id}/status", response_model=InvoiceDB) def update_invoice_status( invoice_id: int, status_update: InvoiceStatusUpdate, db: Session = Depends(get_db) ): """ Update the status of an invoice. """ # Get the invoice invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first() if invoice is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Invoice with ID {invoice_id} not found", ) # Update status invoice.status = status_update.status # Commit changes db.commit() db.refresh(invoice) return invoice @router.delete("/{invoice_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None) def delete_invoice(invoice_id: int, db: Session = Depends(get_db)): """ Delete an invoice. """ # Get the invoice invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first() if invoice is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Invoice with ID {invoice_id} not found", ) # Delete the invoice db.delete(invoice) db.commit() return None