| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- 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", "text", "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}
- f.options = filters[f.name]
- 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))
- f.current_value = defaults[f.name]
- 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,
- "filter_config": forderung_filter_config,
- },
- )
- @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",
- }
- fileserver = "C:\\Projekte\\Reisacher-Fileserver"
- files = [f.relative_to(fileserver) for f in Path(f"{fileserver}\\{client_db}\\{document_no}").glob("*")]
- template_context["files"] = files
- 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)
- @router.get("/files/")
- def get_files(request: Request):
- name = request.query_params.get("name")
- filename = Path(f"C:\\Projekte\\Reisacher-Fileserver\\{unquote(name)}")
- return FileResponse(filename, media_type="application/octet-stream", filename=filename.name)
|