import io from dataclasses import dataclass from datetime import datetime from pathlib import Path from urllib.parse import unquote, urlencode import pandas as pd from docx import Document from docxtpl import DocxTemplate from fastapi import APIRouter, Depends, Form, HTTPException, Request, Response from fastapi.responses import ( FileResponse, HTMLResponse, RedirectResponse, StreamingResponse, ) from fastapi.templating import Jinja2Templates from sqlalchemy import text from sqlalchemy.orm import Session from .auth import ldap_authenticate from .db import get_session from .models import Bemerkung, Forderung from .schemas import BemerkungIn router = APIRouter() templates = Jinja2Templates(directory="templates") @router.get("/", response_class=HTMLResponse) def index(request: Request): return RedirectResponse(url="/login") @router.get("/login", response_class=HTMLResponse) def login_get(request: Request): return templates.TemplateResponse(request, "base/login.html", {"request": request}) @router.get("/select", response_class=HTMLResponse) def select_get(request: Request): return templates.TemplateResponse(request, "base/select.html", {"request": request}) @router.post("/login") def login_post(username: str = Form(...), password: str = Form(...)): user = ldap_authenticate(username, password) if not user: raise HTTPException(status_code=401, detail="Invalid credentials") # For a simple demo we set a cookie (not secure) -- replace with real session in production response = RedirectResponse(url="/forderungen", status_code=302) response.set_cookie("user", user["username"]) return response @router.get("/forderungen", response_class=HTMLResponse) def forderungsliste(request: Request, db: Session = Depends(get_session), limit: int = 100): q = db.query(Forderung).order_by(Forderung.faelligkeit.asc()).limit(limit).all() return templates.TemplateResponse(request, "base/list.html", {"request": request, "forderungen": q}) def number_format(input: float) -> str: return format(input, "0,.2f").replace(".", ":").replace(",", ".").replace(":", ",") def date_format(input: datetime) -> str: if input is None: return "" return input.strftime("%d.%m.%Y") templates.env.filters["number_format"] = number_format templates.env.filters["date_format"] = date_format def single_quote(text: str): return "'" + unquote(text) + "'" @dataclass class FilterConfig: name: str text: str filter_type: str width: str key_column: str value_column: str visible: bool = True default_value: str = "" options: list[dict[str, str]] | None = None current_value: str | None = None forderung_filter_config = [ FilterConfig("Hauptbetrieb", "Hauptbetrieb", "select", "2", "Client_DB", "Hauptbetrieb_Name"), FilterConfig("Standort", "Standort", "select", "3", "Standort_ID", "Standort_Name"), FilterConfig("Bereich", "Bereich", "select", "3", "Bereich", "Bereich"), FilterConfig("Verursacher", "Verursacher", "select", "4", "Verursacher", "Verursacher"), FilterConfig("Rechnungsnummer", "Rechnungsnummer", "text", "3", "Document_No", "Document_No"), FilterConfig( "RechnungsdatumVon", "Rechnungsdatum von", "date", "2", "Invoice_Date", "Invoice_Date", default_value="2000-01-01T00:00:00", ), FilterConfig( "RechnungsdatumBis", "Rechnungsdatum bis", "date", "2", "Invoice_Date", "Invoice_Date", default_value="2027-01-01T00:00:00", ), FilterConfig("Kunde", "Kunde", "select", "5", "Kunde", "Kunde"), FilterConfig( "WiedervorlageVon", "Wiedervorlage von", "date", "2", "Wiedervorlage", "Wiedervorlage", default_value="2000-01-01T00:00:00", ), FilterConfig( "WiedervorlageBis", "Wiedervorlage bis", "date", "2", "Wiedervorlage", "Wiedervorlage", default_value="2027-01-01T00:00:00", ), FilterConfig("Fahrzeug", "Fahrzeug", "select", "4", "VIN", "VIN"), FilterConfig("Staffel", "Staffel", "select", "2", "Staffel", "Staffel"), FilterConfig("Mahnstufe", "Mahnstufe", "select", "2", "Mahnstufe", "Mahnstufe"), FilterConfig("BenutzerSelect", "Benutzer", "hidden", "2", "", "", visible=False, default_value="winter"), ] filter_type_option = { "text": "text", "select": "selectedOptionValue", "date": "selectedDate", "hidden": "selectedOptionValue", } @router.get("/app/forderungen/liste", response_class=HTMLResponse) def forderungen_liste(request: Request, db: Session = Depends(get_session), limit: int = 100, page: int = 0): params = { f.name: request.query_params[f.name] for f in forderung_filter_config if request.query_params.get(f.name, "") != "" } if page == 0 and len(params) < len(request.query_params): return RedirectResponse(url="/app/forderungen/liste?" + urlencode(params)) context = {} for f in forderung_filter_config: context[f.name] = {filter_type_option[f.filter_type]: single_quote(params.get(f.name, f.default_value))} query = templates.TemplateResponse(request, "forderungen/queries/forderungen_liste.sql", context).body.decode( "utf-8" ) # print(query) # q = db.execute(text("SELECT * FROM [dbo].[Forderungen]")) q = db.execute(text(query)).fetchall() col_names = list(q[0]._asdict().keys()) filters = {} for f in forderung_filter_config: if f.key_column == "": continue filters[f.name] = {row[col_names.index(f.key_column)]: row[col_names.index(f.value_column)] for row in q} summary = { "offen": sum([r[col_names.index("offen")] for r in q]), "Anzahl": len(q), } q_limit = q[page * limit : (page + 1) * limit] defaults = {} for f in forderung_filter_config: defaults[f.name] = unquote(request.query_params.get(f.name, f.default_value)) if len(q_limit) == 0: return None template = "forderungen/liste.html" if page > 0: template = "forderungen/liste_tabelle.html" return templates.TemplateResponse( request, template, { "request": request, "forderungen_liste": q_limit, "filter": filters, "defaults": defaults, "summary": summary, "page": page, }, ) @router.get("/app/forderungen/details/{client_db}_{document_no}", response_class=HTMLResponse) def forderungen_details( request: Request, client_db: str, document_no: str, db: Session = Depends(get_session), limit: int = 100 ): context = { "appsmith": { "URL": { "queryParams": { "Client_DB": "'" + client_db + "'", "Document_No": "'" + document_no + "'", } } }, "BelegeFilter": {"selectedOptionValue": "D"}, "BenutzerSelect": {"selectedOptionValue": "winter"}, } template_context = {} for filename in [ "auftrag_positionen", "forderung_belege", "forderung_kommentar", "forderung_kopf", "forderung_mahnung", ]: query = templates.TemplateResponse(request, f"forderungen/queries/{filename}.sql", context).body.decode("utf-8") template_context[filename] = db.execute(text(query)).fetchall() template_context["forderung"] = { "id": "12345", "rechnungsnummer": "WERE123445", "kunde": "Burghard", "betrag": "120,00", "faelligkeit": "23.12.1983", } template_context["files"] = list(Path(f"C:\\Projekte\\Reisacher-Fileserver\\{client_db}\\{document_no}").glob("*")) return templates.TemplateResponse(request, "forderungen/details.html", template_context) @router.get("/app/forderungen/dashboard", response_class=HTMLResponse) def forderungen_dashboard(request: Request, db: Session = Depends(get_session)): context = {"summary": False} return templates.TemplateResponse(request, "forderungen/dashboard.html", context) @router.get("/chat", response_class=HTMLResponse) def chat(request: Request): # q = db.query(Forderung).order_by(Forderung.faelligkeit.asc()).limit(limit).all() return templates.TemplateResponse(request, "base/chat.html") @router.get("/export/csv") def export_csv(db: Session = Depends(get_session)): q = db.query(Forderung).all() rows = [] for f in q: rows.append( { "id": f.id, "rechnungsnummer": f.rechnungsnummer, "kunde": f.kunde.name if f.kunde else "", "betrag": float(f.betrag), "faelligkeit": f.faelligkeit.isoformat() if f.faelligkeit else "", } ) df = pd.DataFrame(rows) buf = io.StringIO() df.to_csv(buf, index=False) buf.seek(0) return Response( content=buf.getvalue(), media_type="text/csv", headers={"Content-Disposition": "attachment; filename=forderungen.csv"}, ) @router.get("/export/xlsx") def export_xlsx(db: Session = Depends(get_session)): q = db.query(Forderung).all() rows = [] for f in q: rows.append( { "id": f.id, "rechnungsnummer": f.rechnungsnummer, "kunde": f.kunde.name if f.kunde else "", "betrag": float(f.betrag), "faelligkeit": f.faelligkeit.isoformat() if f.faelligkeit else "", } ) df = pd.DataFrame(rows) buf = io.BytesIO() with pd.ExcelWriter(buf, engine="openpyxl") as writer: df.to_excel(writer, index=False, sheet_name="Forderungen") buf.seek(0) return StreamingResponse( buf, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={"Content-Disposition": "attachment; filename=forderungen.xlsx"}, ) @router.get("/detail/{id}", response_class=HTMLResponse) def detail(request: Request, id: int, db: Session = Depends(get_session)): f = db.query(Forderung).filter(Forderung.id == id).first() if not f: raise HTTPException(status_code=404) return templates.TemplateResponse(request, "base/detail.html", {"request": request, "forderung": f}) @router.post("/detail/{id}/bemerkung") def save_bemerkung(id: int, data: BemerkungIn, request: Request, db: Session = Depends(get_session)): user = request.cookies.get("user") or "anonymous" b = Bemerkung( forderung_id=id, benutzer=user, bemerkung=data.bemerkung, wiedervorlage_datum=data.wiedervorlage_datum ) db.add(b) db.commit() return RedirectResponse(url=f"/detail/{id}", status_code=302) @router.get("/detail/{id}/export/docx") def export_docx(id: int, db: Session = Depends(get_session)): f = db.query(Forderung).filter(Forderung.id == id).first() if not f: raise HTTPException(status_code=404) doc = Document() doc.add_heading(f"Rechnung {f.rechnungsnummer}", level=1) doc.add_paragraph(f'Kunde: {f.kunde.name if f.kunde else ""}') doc.add_paragraph(f"Betrag: {float(f.betrag)}") doc.add_paragraph(f"Fälligkeit: {f.faelligkeit}") buf = io.BytesIO() doc.save(buf) buf.seek(0) return StreamingResponse( buf, media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", headers={"Content-Disposition": f"attachment; filename=rechnung_{f.rechnungsnummer}.docx"}, ) @router.get("/app/forderungen/export/{client_db}_{document_no}/") def export_docx2(request: Request, client_db: str, document_no: str, db: Session = Depends(get_session)): context = { "appsmith": { "URL": { "queryParams": { "Client_DB": "'" + client_db + "'", "Document_No": "'" + document_no + "'", } } }, } query = templates.TemplateResponse(request, "forderungen/queries/forderung_kopf.sql", context).body.decode("utf-8") q = db.execute(text(query)).fetchone() doc = DocxTemplate("templates\\forderungen\\docs\\Mahnung_AHR.docx") filename = Path( f"C:\\Projekte\\Reisacher-Fileserver\\{client_db}\\{document_no}\\Mahnung_{client_db}_{document_no}.docx" ) filename.parent.mkdir(parents=True, exist_ok=True) context = { "Kunde_Name": q.Kunde, "Kunde_Adresse": "Im Waldhof 14a", "Kunde_PLZ": "61476", "Kunde_Ort": "Kronberg", "Datum_heute": datetime.now().strftime("%d.%m.%Y"), "Kunde_Nr": q.Kunde.split("-")[-1].strip(), "Fahrzeug_Kennzeichen": "OF-RB 512", "Rechnung_Nr": q.Document_No, "Betrag_SB": "250,00", "Betrag_USt": "380,00", } doc.render(context) doc.save(filename) return FileResponse(filename, media_type="application/octet-stream", filename=filename.name)