maybe good
This commit is contained in:
493
app/api/files.py
Normal file
493
app/api/files.py
Normal file
@@ -0,0 +1,493 @@
|
||||
"""
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user