document-processor/app/main.py

188 lines
6.6 KiB
Python

from decimal import Decimal
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sqlalchemy import create_engine, func
from sqlalchemy.orm import sessionmaker
from app.core.config import DATABASE_URL
from app.models.document import Document
from app.models.extracted_field import ExtractedField
from app.models.receipt_line_item import ReceiptLineItem
from app.routes.documents import router as documents_router
from app.routes.health import router as health_router
from app.routes.ingest import router as ingest_router
from app.routes.line_items import router as line_items_router
from app.routes.queue import router as queue_router
from app.routes.trash import router as trash_router
app = FastAPI(title="document-processor")
app.mount("/static", StaticFiles(directory="app/static"), name="static")
app.mount("/files", StaticFiles(directory="/mnt/storage/document-processor"), name="files")
app.include_router(health_router)
app.include_router(documents_router)
app.include_router(ingest_router)
app.include_router(line_items_router)
app.include_router(queue_router)
app.include_router(trash_router)
templates = Jinja2Templates(directory="app/templates")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)
def _to_str(value) -> str:
if value is None:
return ""
return str(value)
@app.get("/")
def root_dashboard(request: Request):
db = SessionLocal()
try:
total_documents = db.query(func.count(Document.id)).scalar() or 0
active_documents = (
db.query(func.count(Document.id))
.filter(Document.is_trashed.is_(False))
.scalar()
or 0
)
reviewed_documents = (
db.query(func.count(Document.id))
.filter(Document.is_trashed.is_(False))
.filter(Document.review_status == "reviewed")
.scalar()
or 0
)
pending_review_documents = (
db.query(func.count(Document.id))
.filter(Document.is_trashed.is_(False))
.filter(Document.review_status != "reviewed")
.scalar()
or 0
)
trashed_documents = (
db.query(func.count(Document.id))
.filter(Document.is_trashed.is_(True))
.scalar()
or 0
)
extracted_documents = db.query(func.count(ExtractedField.id)).scalar() or 0
total_line_items = db.query(func.count(ReceiptLineItem.id)).scalar() or 0
cocktail_count = (
db.query(func.count(ReceiptLineItem.id))
.filter(func.lower(ReceiptLineItem.item_category) == "cocktail")
.scalar()
or 0
)
quality_candidates = (
db.query(ReceiptLineItem.extra_json)
.filter(func.lower(ReceiptLineItem.item_category) == "cocktail")
.all()
)
rated_cocktails = 0
pending_cocktail_reviews = 0
na_cocktails = 0
rating_sum = Decimal("0")
for (extra_json,) in quality_candidates:
extra = extra_json or {}
status = str(extra.get("quality_status") or "").strip().lower()
rating_raw = str(extra.get("quality_rating") or "").strip()
rating_dec = None
if rating_raw:
try:
rating_dec = Decimal(rating_raw)
except Exception:
rating_dec = None
if status == "na":
na_cocktails += 1
elif rating_dec is not None:
rated_cocktails += 1
rating_sum += rating_dec
else:
pending_cocktail_reviews += 1
avg_cocktail_rating = ""
if rated_cocktails > 0:
avg_cocktail_rating = str((rating_sum / Decimal(rated_cocktails)).quantize(Decimal("0.01")))
recent_documents = (
db.query(Document)
.filter(Document.is_trashed.is_(False))
.order_by(Document.updated_at.desc())
.limit(10)
.all()
)
recent_line_items = (
db.query(ReceiptLineItem)
.join(Document, ReceiptLineItem.document_id == Document.id)
.filter(Document.is_trashed.is_(False))
.order_by(ReceiptLineItem.updated_at.desc())
.limit(10)
.all()
)
recent_line_item_rows = []
for item in recent_line_items:
doc = item.document
merchant = ""
transaction_date = ""
if doc and doc.extracted_fields:
extracted = sorted(
doc.extracted_fields,
key=lambda x: x.updated_at or x.created_at,
reverse=True,
)[0]
merchant = extracted.merchant_normalized or extracted.merchant_raw or ""
if extracted.transaction_date:
transaction_date = extracted.transaction_date.isoformat()
extra = item.extra_json or {}
recent_line_item_rows.append(
{
"document_id": doc.document_id if doc else "",
"merchant": merchant,
"transaction_date": transaction_date,
"description": item.normalized_description or item.raw_description or "",
"category": item.item_category or "",
"line_total": _to_str(item.line_total),
"quality_rating": _to_str(extra.get("quality_rating")),
"quality_status": _to_str(extra.get("quality_status")),
}
)
return templates.TemplateResponse(
request=request,
name="dashboard.html",
context={
"request": request,
"active_page": "",
"total_documents": total_documents,
"active_documents": active_documents,
"reviewed_documents": reviewed_documents,
"pending_review_documents": pending_review_documents,
"trashed_documents": trashed_documents,
"extracted_documents": extracted_documents,
"total_line_items": total_line_items,
"cocktail_count": cocktail_count,
"rated_cocktails": rated_cocktails,
"pending_cocktail_reviews": pending_cocktail_reviews,
"na_cocktails": na_cocktails,
"avg_cocktail_rating": avg_cocktail_rating,
"recent_documents": recent_documents,
"recent_line_items": recent_line_item_rows,
},
)
finally:
db.close()