reports: add PDF generation infra (fpdf2); Phone Book CSV/PDF export; Payments - Detailed report with preview and PDF grouped by deposit date; update Dockerfile for deps; smoke-tested in Docker
This commit is contained in:
102
app/main.py
102
app/main.py
@@ -31,6 +31,7 @@ from structlog import contextvars as structlog_contextvars
|
||||
from .database import create_tables, get_db, get_database_url
|
||||
from .models import User, Case, Client, Phone, Transaction, Document, Payment, ImportLog
|
||||
from .auth import authenticate_user, get_current_user_from_session
|
||||
from .reporting import build_phone_book_pdf, build_payments_detailed_pdf
|
||||
from .logging_config import setup_logging
|
||||
from .schemas import (
|
||||
ClientOut,
|
||||
@@ -2271,7 +2272,7 @@ async def phone_book_report(
|
||||
request: Request,
|
||||
client_ids: List[int] | None = Query(None),
|
||||
q: str | None = Query(None, description="Filter by name/company"),
|
||||
format: str | None = Query(None, description="csv for CSV output"),
|
||||
format: str | None = Query(None, description="csv or pdf for export"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
user = get_current_user_from_session(request.session)
|
||||
@@ -2313,6 +2314,14 @@ async def phone_book_report(
|
||||
headers={"Content-Disposition": "attachment; filename=phone_book.csv"},
|
||||
)
|
||||
|
||||
if format == "pdf":
|
||||
pdf_bytes = build_phone_book_pdf(clients)
|
||||
return Response(
|
||||
content=pdf_bytes,
|
||||
media_type="application/pdf",
|
||||
headers={"Content-Disposition": "attachment; filename=phone_book.pdf"},
|
||||
)
|
||||
|
||||
logger.info("phone_book_render", count=len(clients))
|
||||
return templates.TemplateResponse(
|
||||
"report_phone_book.html",
|
||||
@@ -2320,6 +2329,97 @@ async def phone_book_report(
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------
|
||||
# Reports: Payments - Detailed
|
||||
# ------------------------------
|
||||
|
||||
@app.get("/reports/payments-detailed")
|
||||
async def payments_detailed_report(
|
||||
request: Request,
|
||||
from_date: str | None = Query(None, description="YYYY-MM-DD"),
|
||||
to_date: str | None = Query(None, description="YYYY-MM-DD"),
|
||||
file_no: str | None = Query(None, description="Case file number"),
|
||||
format: str | None = Query(None, description="pdf for PDF output"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
user = get_current_user_from_session(request.session)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=302)
|
||||
|
||||
query = (
|
||||
db.query(Payment)
|
||||
.join(Case, Payment.case_id == Case.id)
|
||||
.join(Client, Case.client_id == Client.id)
|
||||
)
|
||||
|
||||
filters = []
|
||||
if from_date:
|
||||
try:
|
||||
dt = datetime.strptime(from_date, "%Y-%m-%d")
|
||||
filters.append(Payment.payment_date >= dt)
|
||||
except ValueError:
|
||||
pass
|
||||
if to_date:
|
||||
try:
|
||||
dt = datetime.strptime(to_date, "%Y-%m-%d")
|
||||
filters.append(Payment.payment_date <= dt)
|
||||
except ValueError:
|
||||
pass
|
||||
if file_no:
|
||||
filters.append(Case.file_no.ilike(f"%{file_no}%"))
|
||||
|
||||
if filters:
|
||||
query = query.filter(and_(*filters))
|
||||
|
||||
# For grouping by deposit date, order by date then id
|
||||
payments = (
|
||||
query.options(joinedload(Payment.case).joinedload(Case.client))
|
||||
.order_by(Payment.payment_date.asc().nulls_last(), Payment.id.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
if format == "pdf":
|
||||
pdf_bytes = build_payments_detailed_pdf(payments)
|
||||
return Response(
|
||||
content=pdf_bytes,
|
||||
media_type="application/pdf",
|
||||
headers={"Content-Disposition": "attachment; filename=payments_detailed.pdf"},
|
||||
)
|
||||
|
||||
# Build preview groups for template: [{date, total, items}]
|
||||
groups: list[dict[str, Any]] = []
|
||||
from collections import defaultdict
|
||||
grouped: dict[str, list[Payment]] = defaultdict(list)
|
||||
for p in payments:
|
||||
key = p.payment_date.date().isoformat() if p.payment_date else "(No Date)"
|
||||
grouped[key].append(p)
|
||||
overall_total = sum((p.amount or 0.0) for p in payments)
|
||||
for key in sorted(grouped.keys()):
|
||||
items = grouped[key]
|
||||
total_amt = sum((p.amount or 0.0) for p in items)
|
||||
groups.append({"date": key, "total": total_amt, "items": items})
|
||||
|
||||
logger.info(
|
||||
"payments_detailed_render",
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
file_no=file_no,
|
||||
count=len(payments),
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"payments_detailed.html",
|
||||
{
|
||||
"request": request,
|
||||
"user": user,
|
||||
"groups": groups,
|
||||
"overall_total": overall_total,
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"file_no": file_no,
|
||||
},
|
||||
)
|
||||
|
||||
# ------------------------------
|
||||
# JSON API: list/filter endpoints
|
||||
# ------------------------------
|
||||
|
||||
Reference in New Issue
Block a user