151 lines
4.3 KiB
Python
151 lines
4.3 KiB
Python
"""
|
|
OPC Projects and Invoicing Router
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, Response, Request
|
|
from typing import Optional
|
|
from pydantic import BaseModel
|
|
from db import opc_db
|
|
from rbac import User
|
|
from services import pdf_service
|
|
from datetime import datetime
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class ProjectCreate(BaseModel):
|
|
name: str
|
|
description: Optional[str] = None
|
|
client_id: Optional[int] = None
|
|
|
|
|
|
class InvoiceGenerate(BaseModel):
|
|
project_id: int
|
|
client_id: int
|
|
invoice_number: Optional[str] = None
|
|
|
|
|
|
@router.get("/projects")
|
|
async def list_projects(
|
|
request: Request,
|
|
):
|
|
"""List all projects"""
|
|
user: User = request.state.user
|
|
pool = await opc_db.get_pool()
|
|
async with pool.acquire() as conn:
|
|
rows = await conn.fetch("SELECT * FROM projects ORDER BY created_at DESC")
|
|
return {"items": [dict(row) for row in rows]}
|
|
|
|
|
|
@router.post("/projects")
|
|
async def create_project(
|
|
project: ProjectCreate,
|
|
request: Request,
|
|
):
|
|
"""Create a new project"""
|
|
user: User = request.state.user
|
|
pool = await opc_db.get_pool()
|
|
async with pool.acquire() as conn:
|
|
row = await conn.fetchrow("""
|
|
INSERT INTO projects (name, description, client_id)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING *
|
|
""", project.name, project.description, project.client_id)
|
|
return dict(row)
|
|
|
|
|
|
@router.get("/clients")
|
|
async def list_clients(
|
|
request: Request,
|
|
):
|
|
"""List all clients"""
|
|
user: User = request.state.user
|
|
pool = await opc_db.get_pool()
|
|
async with pool.acquire() as conn:
|
|
rows = await conn.fetch("SELECT * FROM clients ORDER BY name")
|
|
return {"items": [dict(row) for row in rows]}
|
|
|
|
|
|
@router.post("/clients")
|
|
async def create_client(
|
|
request: Request,
|
|
name: str,
|
|
email: Optional[str] = None,
|
|
company: Optional[str] = None,
|
|
):
|
|
"""Create a new client"""
|
|
user: User = request.state.user
|
|
pool = await opc_db.get_pool()
|
|
async with pool.acquire() as conn:
|
|
row = await conn.fetchrow("""
|
|
INSERT INTO clients (name, email, company)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING *
|
|
""", name, email, company)
|
|
return dict(row)
|
|
|
|
|
|
@router.post("/invoices/generate")
|
|
async def generate_invoice(
|
|
invoice: InvoiceGenerate,
|
|
request: Request,
|
|
):
|
|
"""Generate PDF invoice for a project"""
|
|
user: User = request.state.user
|
|
pool = await opc_db.get_pool()
|
|
async with pool.acquire() as conn:
|
|
# Get client
|
|
client_row = await conn.fetchrow("SELECT * FROM clients WHERE id = $1", invoice.client_id)
|
|
if not client_row:
|
|
raise HTTPException(status_code=404, detail="Client not found")
|
|
client = dict(client_row)
|
|
|
|
# Get time entries for project
|
|
time_rows = await conn.fetch("""
|
|
SELECT * FROM time_entries
|
|
WHERE project_id = $1 AND billable = TRUE
|
|
ORDER BY started_at
|
|
""", invoice.project_id)
|
|
|
|
if not time_rows:
|
|
raise HTTPException(status_code=400, detail="No billable time entries found")
|
|
|
|
time_entries = [dict(row) for row in time_rows]
|
|
|
|
# Generate PDF
|
|
pdf_bytes = pdf_service.generate_invoice_from_project(
|
|
project_id=invoice.project_id,
|
|
client=client,
|
|
time_entries=time_entries,
|
|
invoice_number=invoice.invoice_number
|
|
)
|
|
|
|
# Return PDF
|
|
filename = f"invoice-{invoice.invoice_number or datetime.now().strftime('%Y%m%d')}.pdf"
|
|
return Response(
|
|
content=pdf_bytes,
|
|
media_type="application/pdf",
|
|
headers={
|
|
"Content-Disposition": f"attachment; filename={filename}"
|
|
}
|
|
)
|
|
|
|
|
|
@router.get("/projects/{project_id}/time")
|
|
async def get_project_time(
|
|
project_id: int,
|
|
request: Request,
|
|
):
|
|
"""Get time entries for a project"""
|
|
user: User = request.state.user
|
|
entries = await opc_db.get_time_entries(project_id=project_id)
|
|
total_minutes = sum(e["duration_minutes"] for e in entries)
|
|
billable_minutes = sum(e["duration_minutes"] for e in entries if e.get("billable"))
|
|
|
|
return {
|
|
"entries": entries,
|
|
"total_minutes": total_minutes,
|
|
"total_hours": round(total_minutes / 60, 2),
|
|
"billable_minutes": billable_minutes,
|
|
"billable_hours": round(billable_minutes / 60, 2)
|
|
}
|