Files
delphi-database/app/api/files.py
2025-08-08 15:55:15 -05:00

493 lines
14 KiB
Python

"""
File Management API endpoints
"""
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import or_, func, and_, desc
from datetime import date, datetime
from app.database.base import get_db
from app.models.files import File
from app.models.rolodex import Rolodex
from app.models.ledger import Ledger
from app.models.lookups import Employee, FileType, FileStatus
from app.models.user import User
from app.auth.security import get_current_user
router = APIRouter()
# Pydantic schemas
from pydantic import BaseModel
class FileBase(BaseModel):
file_no: str
id: str # Rolodex ID (file owner)
regarding: Optional[str] = None
empl_num: str
file_type: str
opened: date
closed: Optional[date] = None
status: str
footer_code: Optional[str] = None
opposing: Optional[str] = None
rate_per_hour: float
memo: Optional[str] = None
class FileCreate(FileBase):
pass
class FileUpdate(BaseModel):
id: Optional[str] = None
regarding: Optional[str] = None
empl_num: Optional[str] = None
file_type: Optional[str] = None
opened: Optional[date] = None
closed: Optional[date] = None
status: Optional[str] = None
footer_code: Optional[str] = None
opposing: Optional[str] = None
rate_per_hour: Optional[float] = None
memo: Optional[str] = None
class FileResponse(FileBase):
# Financial balances
trust_bal: float = 0.0
hours: float = 0.0
hourly_fees: float = 0.0
flat_fees: float = 0.0
disbursements: float = 0.0
credit_bal: float = 0.0
total_charges: float = 0.0
amount_owing: float = 0.0
transferable: float = 0.0
class Config:
from_attributes = True
@router.get("/", response_model=List[FileResponse])
async def list_files(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
search: Optional[str] = Query(None),
status_filter: Optional[str] = Query(None),
employee_filter: Optional[str] = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""List files with pagination and filtering"""
query = db.query(File)
if search:
query = query.filter(
or_(
File.file_no.contains(search),
File.id.contains(search),
File.regarding.contains(search),
File.file_type.contains(search)
)
)
if status_filter:
query = query.filter(File.status == status_filter)
if employee_filter:
query = query.filter(File.empl_num == employee_filter)
files = query.offset(skip).limit(limit).all()
return files
@router.get("/{file_no}", response_model=FileResponse)
async def get_file(
file_no: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get specific file by file number"""
file_obj = db.query(File).filter(File.file_no == file_no).first()
if not file_obj:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found"
)
return file_obj
@router.post("/", response_model=FileResponse)
async def create_file(
file_data: FileCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Create new file"""
# Check if file number already exists
existing = db.query(File).filter(File.file_no == file_data.file_no).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="File number already exists"
)
file_obj = File(**file_data.model_dump())
db.add(file_obj)
db.commit()
db.refresh(file_obj)
return file_obj
@router.put("/{file_no}", response_model=FileResponse)
async def update_file(
file_no: str,
file_data: FileUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Update file"""
file_obj = db.query(File).filter(File.file_no == file_no).first()
if not file_obj:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found"
)
# Update fields
for field, value in file_data.model_dump(exclude_unset=True).items():
setattr(file_obj, field, value)
db.commit()
db.refresh(file_obj)
return file_obj
@router.delete("/{file_no}")
async def delete_file(
file_no: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Delete file"""
file_obj = db.query(File).filter(File.file_no == file_no).first()
if not file_obj:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found"
)
db.delete(file_obj)
db.commit()
return {"message": "File deleted successfully"}
@router.get("/stats/summary")
async def get_file_stats(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get file statistics and summary"""
total_files = db.query(File).count()
active_files = db.query(File).filter(File.status == "ACTIVE").count()
# Get status breakdown
status_stats = db.query(
File.status,
func.count(File.file_no).label('count')
).group_by(File.status).all()
# Get file type breakdown
type_stats = db.query(
File.file_type,
func.count(File.file_no).label('count')
).group_by(File.file_type).all()
# Get employee breakdown
employee_stats = db.query(
File.empl_num,
func.count(File.file_no).label('count')
).group_by(File.empl_num).all()
# Financial summary
financial_summary = db.query(
func.sum(File.total_charges).label('total_charges'),
func.sum(File.amount_owing).label('total_owing'),
func.sum(File.trust_bal).label('total_trust'),
func.sum(File.hours).label('total_hours')
).first()
return {
"total_files": total_files,
"active_files": active_files,
"status_breakdown": [{"status": s[0], "count": s[1]} for s in status_stats],
"type_breakdown": [{"type": t[0], "count": t[1]} for t in type_stats],
"employee_breakdown": [{"employee": e[0], "count": e[1]} for e in employee_stats],
"financial_summary": {
"total_charges": float(financial_summary.total_charges or 0),
"total_owing": float(financial_summary.total_owing or 0),
"total_trust": float(financial_summary.total_trust or 0),
"total_hours": float(financial_summary.total_hours or 0)
}
}
@router.get("/lookups/file-types")
async def get_file_types(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get available file types"""
file_types = db.query(FileType).filter(FileType.active == True).all()
return [{"code": ft.type_code, "description": ft.description} for ft in file_types]
@router.get("/lookups/file-statuses")
async def get_file_statuses(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get available file statuses"""
statuses = db.query(FileStatus).filter(FileStatus.active == True).order_by(FileStatus.sort_order).all()
return [{"code": s.status_code, "description": s.description} for s in statuses]
@router.get("/lookups/employees")
async def get_employees(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get active employees"""
employees = db.query(Employee).filter(Employee.active == True).all()
return [{"code": e.empl_num, "name": f"{e.first_name} {e.last_name}"} for e in employees]
@router.get("/{file_no}/financial-summary")
async def get_file_financial_summary(
file_no: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get detailed financial summary for a file"""
file_obj = db.query(File).filter(File.file_no == file_no).first()
if not file_obj:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found"
)
# Get recent ledger entries
recent_entries = db.query(Ledger)\
.filter(Ledger.file_no == file_no)\
.order_by(desc(Ledger.date))\
.limit(10)\
.all()
# Get unbilled entries
unbilled_entries = db.query(Ledger)\
.filter(and_(Ledger.file_no == file_no, Ledger.billed == "N"))\
.all()
unbilled_total = sum(entry.amount for entry in unbilled_entries)
return {
"file_no": file_no,
"financial_data": {
"trust_balance": file_obj.trust_bal,
"hours_total": file_obj.hours,
"hourly_fees": file_obj.hourly_fees,
"flat_fees": file_obj.flat_fees,
"disbursements": file_obj.disbursements,
"total_charges": file_obj.total_charges,
"amount_owing": file_obj.amount_owing,
"credit_balance": file_obj.credit_bal,
"transferable": file_obj.transferable,
"unbilled_amount": unbilled_total
},
"recent_entries": [
{
"date": entry.date.isoformat() if entry.date else None,
"amount": entry.amount,
"description": entry.note,
"billed": entry.billed == "Y"
} for entry in recent_entries
],
"unbilled_count": len(unbilled_entries)
}
@router.get("/{file_no}/client-info")
async def get_file_client_info(
file_no: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get client information for a file"""
file_obj = db.query(File)\
.options(joinedload(File.owner))\
.filter(File.file_no == file_no)\
.first()
if not file_obj:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found"
)
client = file_obj.owner
if not client:
return {"file_no": file_no, "client": None}
return {
"file_no": file_no,
"client": {
"id": client.id,
"name": f"{client.first or ''} {client.last}".strip(),
"email": client.email,
"city": client.city,
"state": client.abrev,
"group": client.group
}
}
@router.post("/{file_no}/close")
async def close_file(
file_no: str,
close_date: Optional[date] = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Close a file"""
file_obj = db.query(File).filter(File.file_no == file_no).first()
if not file_obj:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found"
)
file_obj.closed = close_date or date.today()
file_obj.status = "CLOSED"
db.commit()
db.refresh(file_obj)
return {"message": f"File {file_no} closed successfully", "closed_date": file_obj.closed}
@router.post("/{file_no}/reopen")
async def reopen_file(
file_no: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Reopen a closed file"""
file_obj = db.query(File).filter(File.file_no == file_no).first()
if not file_obj:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found"
)
file_obj.closed = None
file_obj.status = "ACTIVE"
db.commit()
db.refresh(file_obj)
return {"message": f"File {file_no} reopened successfully"}
@router.get("/search/advanced")
async def advanced_file_search(
file_no: Optional[str] = Query(None),
client_name: Optional[str] = Query(None),
regarding: Optional[str] = Query(None),
file_type: Optional[str] = Query(None),
status: Optional[str] = Query(None),
employee: Optional[str] = Query(None),
opened_after: Optional[date] = Query(None),
opened_before: Optional[date] = Query(None),
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Advanced file search with multiple criteria"""
query = db.query(File).options(joinedload(File.owner))
if file_no:
query = query.filter(File.file_no.contains(file_no))
if client_name:
query = query.join(Rolodex).filter(
or_(
Rolodex.first.contains(client_name),
Rolodex.last.contains(client_name),
func.concat(Rolodex.first, ' ', Rolodex.last).contains(client_name)
)
)
if regarding:
query = query.filter(File.regarding.contains(regarding))
if file_type:
query = query.filter(File.file_type == file_type)
if status:
query = query.filter(File.status == status)
if employee:
query = query.filter(File.empl_num == employee)
if opened_after:
query = query.filter(File.opened >= opened_after)
if opened_before:
query = query.filter(File.opened <= opened_before)
# Get total count for pagination
total = query.count()
# Apply pagination
files = query.offset(skip).limit(limit).all()
# Format results with client names
results = []
for file_obj in files:
client = file_obj.owner
client_name = f"{client.first or ''} {client.last}".strip() if client else "Unknown"
results.append({
"file_no": file_obj.file_no,
"client_id": file_obj.id,
"client_name": client_name,
"regarding": file_obj.regarding,
"file_type": file_obj.file_type,
"status": file_obj.status,
"employee": file_obj.empl_num,
"opened": file_obj.opened.isoformat() if file_obj.opened else None,
"closed": file_obj.closed.isoformat() if file_obj.closed else None,
"amount_owing": file_obj.amount_owing,
"total_charges": file_obj.total_charges
})
return {
"files": results,
"total": total,
"skip": skip,
"limit": limit
}