|
|
@@ -1,15 +1,15 @@
|
|
|
import io
|
|
|
+import logging
|
|
|
import re
|
|
|
+import secrets
|
|
|
from datetime import datetime, timedelta
|
|
|
from pathlib import Path
|
|
|
from typing import Annotated
|
|
|
from urllib.parse import unquote, urlencode
|
|
|
|
|
|
import pandas as pd
|
|
|
-import requests
|
|
|
-from docx import Document
|
|
|
from docxtpl import DocxTemplate
|
|
|
-from fastapi import APIRouter, Depends, Form, HTTPException, Request, Response
|
|
|
+from fastapi import APIRouter, Depends, Form, HTTPException, Request, Response, status
|
|
|
from fastapi.responses import (
|
|
|
FileResponse,
|
|
|
HTMLResponse,
|
|
|
@@ -20,25 +20,50 @@ from sqlalchemy import text
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
from app.file_storage import get_file, get_files
|
|
|
-from app.query_filter import filter_type_option, import_filter_config
|
|
|
+from app.query_filter import import_filter_config
|
|
|
|
|
|
+from .auth import clear_session_cookie, safe_next_url, set_session_cookie
|
|
|
+from .config import settings
|
|
|
from .db import get_session
|
|
|
from .docuware_search import get_invoice_pdf_by_document_no
|
|
|
from .jinja_templates import templates
|
|
|
-from .ldap import ldap_authenticate
|
|
|
-from .models import Forderung
|
|
|
+from .ldap import DirectoryUnavailable, ldap_authenticate
|
|
|
+from .oauth_token import create_access_token
|
|
|
+from .permissions import Principal, current_principal, ensure_forderung_access, highest_role, load_entitlements
|
|
|
|
|
|
router = APIRouter()
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
@router.get("/", response_class=HTMLResponse)
|
|
|
def index(request: Request):
|
|
|
- return RedirectResponse(url="/login")
|
|
|
+ return RedirectResponse(url="/select")
|
|
|
+
|
|
|
+
|
|
|
+@router.get("/health")
|
|
|
+def health():
|
|
|
+ return {"status": "ok"}
|
|
|
|
|
|
|
|
|
@router.get("/login", response_class=HTMLResponse)
|
|
|
-def login_get(request: Request):
|
|
|
- return templates.TemplateResponse(request, "base/login.html", {"request": request})
|
|
|
+def login_get(request: Request, next: str | None = None):
|
|
|
+ if getattr(request.state, "principal", None):
|
|
|
+ return RedirectResponse(safe_next_url(next), status_code=303)
|
|
|
+ csrf = secrets.token_urlsafe(32)
|
|
|
+ response = templates.TemplateResponse(
|
|
|
+ request,
|
|
|
+ "base/login.html",
|
|
|
+ {"request": request, "csrf": csrf, "next": safe_next_url(next), "error": None},
|
|
|
+ )
|
|
|
+ response.set_cookie(
|
|
|
+ settings.LOGIN_CSRF_COOKIE_NAME,
|
|
|
+ csrf,
|
|
|
+ secure=settings.COOKIE_SECURE,
|
|
|
+ httponly=True,
|
|
|
+ samesite="lax",
|
|
|
+ path="/login",
|
|
|
+ )
|
|
|
+ return response
|
|
|
|
|
|
|
|
|
@router.get("/select", response_class=HTMLResponse)
|
|
|
@@ -47,22 +72,77 @@ def select_get(request: Request):
|
|
|
|
|
|
|
|
|
@router.post("/login")
|
|
|
-def login_post(username: str = Form(...), password: str = Form(...)):
|
|
|
- user = ldap_authenticate(username, password)
|
|
|
+def login_post(
|
|
|
+ request: Request,
|
|
|
+ username: Annotated[str, Form()],
|
|
|
+ password: Annotated[str, Form()],
|
|
|
+ csrf: Annotated[str, Form(alias="_csrf")],
|
|
|
+ next: Annotated[str | None, Form()] = None,
|
|
|
+ db: Session = Depends(get_session),
|
|
|
+):
|
|
|
+ if getattr(request.state, "principal", None):
|
|
|
+ return RedirectResponse(safe_next_url(next), status_code=303)
|
|
|
+ expected_csrf = request.cookies.get(settings.LOGIN_CSRF_COOKIE_NAME, "")
|
|
|
+ if not expected_csrf or not secrets.compare_digest(expected_csrf, csrf):
|
|
|
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Ungültiger CSRF-Token")
|
|
|
+
|
|
|
+ try:
|
|
|
+ user = ldap_authenticate(username, password)
|
|
|
+ except DirectoryUnavailable:
|
|
|
+ logger.exception("LDAP-Anmeldedienst nicht verfügbar")
|
|
|
+ return templates.TemplateResponse(
|
|
|
+ request,
|
|
|
+ "base/login.html",
|
|
|
+ {
|
|
|
+ "request": request,
|
|
|
+ "csrf": csrf,
|
|
|
+ "next": safe_next_url(next),
|
|
|
+ "error": "Anmeldedienst ist derzeit nicht verfügbar",
|
|
|
+ },
|
|
|
+ status_code=503,
|
|
|
+ )
|
|
|
+ except ValueError:
|
|
|
+ user = None
|
|
|
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"])
|
|
|
+ response = templates.TemplateResponse(
|
|
|
+ request,
|
|
|
+ "base/login.html",
|
|
|
+ {
|
|
|
+ "request": request,
|
|
|
+ "csrf": csrf,
|
|
|
+ "next": safe_next_url(next),
|
|
|
+ "error": "Anmeldung fehlgeschlagen",
|
|
|
+ },
|
|
|
+ status_code=401,
|
|
|
+ )
|
|
|
+ return response
|
|
|
+
|
|
|
+ entitlements = load_entitlements(db, user.username)
|
|
|
+ role = highest_role(entitlements)
|
|
|
+ token = create_access_token(
|
|
|
+ username=user.username,
|
|
|
+ name=user.name,
|
|
|
+ email=user.email,
|
|
|
+ role=role,
|
|
|
+ auth_source="ldap",
|
|
|
+ )
|
|
|
+ response = RedirectResponse(url=safe_next_url(next), status_code=303)
|
|
|
+ set_session_cookie(response, token)
|
|
|
+ response.delete_cookie(settings.LOGIN_CSRF_COOKIE_NAME, path="/login")
|
|
|
return response
|
|
|
|
|
|
|
|
|
-def single_quote(text: str):
|
|
|
- return "'" + unquote(text) + "'"
|
|
|
+@router.post("/logout")
|
|
|
+def logout(request: Request):
|
|
|
+ request.state.suppress_session_refresh = True
|
|
|
+ response = templates.TemplateResponse(request, "base/logout.html", {"request": request})
|
|
|
+ clear_session_cookie(response)
|
|
|
+ return response
|
|
|
|
|
|
|
|
|
@router.get("/app/forderungen/liste", response_class=HTMLResponse)
|
|
|
def forderungen_liste(request: Request, db: Session = Depends(get_session), limit: int = 100, page: int = 0):
|
|
|
+ principal = current_principal(request)
|
|
|
forderung_filter_config = import_filter_config("forderungen")
|
|
|
|
|
|
params = {
|
|
|
@@ -73,24 +153,27 @@ def forderungen_liste(request: Request, db: Session = Depends(get_session), limi
|
|
|
if page == 0 and len(params) < len(request.query_params):
|
|
|
return RedirectResponse(url="/app/forderungen/liste?" + urlencode(params))
|
|
|
|
|
|
- context = {}
|
|
|
+ query_clauses = []
|
|
|
+ query_values = {"auth_username": principal.username}
|
|
|
for f in forderung_filter_config:
|
|
|
- context[f.name] = {filter_type_option[f.filter_type]: single_quote(params.get(f.name, f.default_value))}
|
|
|
- if f.name in params:
|
|
|
- f.current_value = params[f.name]
|
|
|
+ f.current_value = unquote(params.get(f.name, f.default_value))
|
|
|
+ if f.filter_type == "hidden" or not f.current_value:
|
|
|
+ continue
|
|
|
+ parameter = f"filter_{f.name.lower()}"
|
|
|
+ if f.filter_type == "date":
|
|
|
+ operator = "<=" if "bis" in f.name.lower() else ">="
|
|
|
+ query_clauses.append(f"F.[{f.key_column}] {operator} :{parameter}")
|
|
|
+ query_values[parameter] = f.current_value
|
|
|
+ else:
|
|
|
+ query_clauses.append(f"F.[{f.key_column}] LIKE :{parameter}")
|
|
|
+ query_values[parameter] = f"%{f.current_value}%"
|
|
|
|
|
|
- query = templates.TemplateResponse(request, "forderungen/liste/queries/forderungen_liste.sql", context).body.decode(
|
|
|
+ query = templates.TemplateResponse(request, "forderungen/liste/queries/forderungen_liste.sql", {}).body.decode(
|
|
|
"utf-8"
|
|
|
)
|
|
|
- query_params = [
|
|
|
- f.query_param for f in forderung_filter_config if f.query_param is not None and f.filter_type != "hidden"
|
|
|
- ]
|
|
|
- if query_params:
|
|
|
- query = query.replace("1 = 1", " AND ".join(query_params))
|
|
|
- print(query)
|
|
|
- print(query_params)
|
|
|
- # q = db.execute(text("SELECT * FROM [dbo].[Forderungen]"))
|
|
|
- q_res = db.execute(text(query))
|
|
|
+ if query_clauses:
|
|
|
+ query = query.replace("AND 1 = 1", "AND 1 = 1 AND " + " AND ".join(query_clauses))
|
|
|
+ q_res = db.execute(text(query), query_values)
|
|
|
col_names = list(q_res.keys())
|
|
|
q = q_res.fetchall()
|
|
|
|
|
|
@@ -107,10 +190,8 @@ def forderungen_liste(request: Request, db: Session = Depends(get_session), limi
|
|
|
for row in q
|
|
|
]
|
|
|
if filter_temp:
|
|
|
- print(filter_temp[0])
|
|
|
filters[f.name] = {item[1]: item[2] for item in sorted(list(set(filter_temp))) if item[1]}
|
|
|
else:
|
|
|
- print(f"Filter {f.name} has no options, skipping")
|
|
|
filters[f.name] = {}
|
|
|
f.options = filters[f.name]
|
|
|
|
|
|
@@ -150,27 +231,14 @@ def forderungen_liste(request: Request, db: Session = Depends(get_session), limi
|
|
|
|
|
|
@router.get("/app/forderungen/details/{primary_key}", response_class=HTMLResponse)
|
|
|
def forderungen_details(request: Request, primary_key: str, db: Session = Depends(get_session)):
|
|
|
-
|
|
|
+ principal = current_principal(request)
|
|
|
pkey = convert_primary_key("forderungen", primary_key)
|
|
|
-
|
|
|
+ ensure_forderung_access(db, principal, pkey["Client_DB"], pkey["Beleg_Nr"])
|
|
|
template_context = get_template_context(request, pkey, db)
|
|
|
return templates.TemplateResponse(request, "forderungen/details/details.html", template_context)
|
|
|
|
|
|
|
|
|
def get_template_context(request: Request, pkey: dict[str, str], db: Session) -> dict:
|
|
|
- context = {
|
|
|
- "appsmith": {
|
|
|
- "URL": {
|
|
|
- "queryParams": {
|
|
|
- "Client_DB": "'" + pkey["Client_DB"] + "'",
|
|
|
- "Beleg_Nr": "'" + pkey["Beleg_Nr"] + "'",
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- "BelegeFilter": {"selectedOptionValue": "D"},
|
|
|
- "BenutzerSelect": {"selectedOptionValue": "winter"},
|
|
|
- }
|
|
|
-
|
|
|
template_context = {}
|
|
|
for filename in [
|
|
|
"auftraege_positionen",
|
|
|
@@ -180,17 +248,11 @@ def get_template_context(request: Request, pkey: dict[str, str], db: Session) ->
|
|
|
"forderungen_mahnungen",
|
|
|
"versicherungen",
|
|
|
]:
|
|
|
- query = templates.TemplateResponse(request, f"forderungen/details/queries/{filename}.sql", context).body.decode(
|
|
|
+ query = templates.TemplateResponse(request, f"forderungen/details/queries/{filename}.sql", {}).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",
|
|
|
- }
|
|
|
+ values = {"client_db": pkey["Client_DB"], "beleg_nr": pkey["Beleg_Nr"], "belege_filter": "D"}
|
|
|
+ template_context[filename] = db.execute(text(query), values).fetchall()
|
|
|
template_context["files"] = get_files(pkey)
|
|
|
template_context["add7days"] = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d")
|
|
|
return template_context
|
|
|
@@ -204,14 +266,16 @@ async def post_forderungen_details(
|
|
|
redirect: Annotated[str | None, Form()] = None,
|
|
|
db: Session = Depends(get_session),
|
|
|
):
|
|
|
+ principal = current_principal(request)
|
|
|
pkey = convert_primary_key("forderungen", primary_key)
|
|
|
+ ensure_forderung_access(db, principal, pkey["Client_DB"], pkey["Beleg_Nr"])
|
|
|
|
|
|
if source == "comments":
|
|
|
data = {
|
|
|
"Client_DB": "",
|
|
|
"Beleg_Nr": "",
|
|
|
"Timestamp": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
|
|
|
- "Benutzer_ID": "winter",
|
|
|
+ "Benutzer_ID": principal.username,
|
|
|
"Kommentar": "",
|
|
|
}
|
|
|
else:
|
|
|
@@ -247,13 +311,14 @@ async def post_forderungen_details(
|
|
|
v = v.replace(",", ".")
|
|
|
data[k] = v
|
|
|
|
|
|
+ if source not in {"comments", "details"}:
|
|
|
+ raise HTTPException(status_code=400, detail="Ungültige Datenquelle")
|
|
|
query = templates.TemplateResponse(
|
|
|
request, f"forderungen/details/queries/post/post_{source}.sql", context={"data": data}
|
|
|
).body.decode("utf-8")
|
|
|
- print(query)
|
|
|
for subquery in query.split(";"):
|
|
|
- q = db.execute(text(subquery))
|
|
|
- print(q.rowcount)
|
|
|
+ if subquery.strip():
|
|
|
+ db.execute(text(subquery), data)
|
|
|
db.commit()
|
|
|
template_context = get_template_context(request, pkey, db)
|
|
|
|
|
|
@@ -265,32 +330,24 @@ async def post_forderungen_details(
|
|
|
|
|
|
|
|
|
def convert_primary_key(area: str, primary_key: str) -> dict[str, str]:
|
|
|
- if area == "forderungen":
|
|
|
- pkey = ["Client_DB", "Beleg_Nr"]
|
|
|
- pkey_values = primary_key.split("_")
|
|
|
- return dict(zip(pkey, pkey_values))
|
|
|
+ if area != "forderungen" or "_" not in primary_key:
|
|
|
+ raise HTTPException(status_code=400, detail="Ungültiger Primärschlüssel")
|
|
|
+ client_db, beleg_nr = primary_key.split("_", 1)
|
|
|
+ if not client_db or not beleg_nr or len(client_db) > 20 or len(beleg_nr) > 100:
|
|
|
+ raise HTTPException(status_code=400, detail="Ungültiger Primärschlüssel")
|
|
|
+ return {"Client_DB": client_db, "Beleg_Nr": beleg_nr}
|
|
|
|
|
|
|
|
|
@router.get("/app/forderungen/dashboard", response_class=HTMLResponse)
|
|
|
def forderungen_dashboard(request: Request, db: Session = Depends(get_session)):
|
|
|
+ current_principal(request)
|
|
|
context = {"summary": False}
|
|
|
return templates.TemplateResponse(request, "forderungen/dashboard/dashboard.html", context)
|
|
|
|
|
|
|
|
|
@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 "",
|
|
|
- }
|
|
|
- )
|
|
|
+def export_csv(request: Request, db: Session = Depends(get_session)):
|
|
|
+ rows = _export_rows(db, current_principal(request))
|
|
|
df = pd.DataFrame(rows)
|
|
|
buf = io.StringIO()
|
|
|
df.to_csv(buf, index=False)
|
|
|
@@ -303,19 +360,8 @@ def export_csv(db: Session = Depends(get_session)):
|
|
|
|
|
|
|
|
|
@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 "",
|
|
|
- }
|
|
|
- )
|
|
|
+def export_xlsx(request: Request, db: Session = Depends(get_session)):
|
|
|
+ rows = _export_rows(db, current_principal(request))
|
|
|
df = pd.DataFrame(rows)
|
|
|
buf = io.BytesIO()
|
|
|
with pd.ExcelWriter(buf, engine="openpyxl") as writer:
|
|
|
@@ -328,61 +374,74 @@ def export_xlsx(db: Session = Depends(get_session)):
|
|
|
)
|
|
|
|
|
|
|
|
|
-@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"},
|
|
|
+def _export_rows(db: Session, principal: Principal) -> list[dict]:
|
|
|
+ query = text(
|
|
|
+ """
|
|
|
+ SELECT F.Client_DB,
|
|
|
+ F.Beleg_Nr AS Rechnungsnummer,
|
|
|
+ F.Kunde_Text AS Kunde,
|
|
|
+ F.Beleg_offen AS Betrag_offen,
|
|
|
+ F.Beleg_Faellig_Datum AS Faelligkeit,
|
|
|
+ F.Hauptbetrieb_ID,
|
|
|
+ F.Standort_ID,
|
|
|
+ F.Abteilung
|
|
|
+ FROM dbo.Forderungen_Kopf AS F
|
|
|
+ WHERE EXISTS (
|
|
|
+ SELECT 1
|
|
|
+ FROM dbo.Benutzer_Rechte AS BR
|
|
|
+ INNER JOIN dbo.Rollen AS R ON R.Rolle = BR.Rolle AND R.Aktiv = 1
|
|
|
+ WHERE LOWER(LTRIM(RTRIM(BR.Benutzer_ID))) = :auth_username
|
|
|
+ AND NULLIF(LTRIM(RTRIM(BR.Benutzer_ID)), '') IS NOT NULL
|
|
|
+ AND BR.Hauptbetrieb_ID = F.Hauptbetrieb_ID
|
|
|
+ AND BR.Standort_ID = F.Standort_ID
|
|
|
+ AND (BR.Abteilung IS NULL OR BR.Abteilung = F.Abteilung)
|
|
|
+ )
|
|
|
+ ORDER BY F.Beleg_Faellig_Datum, F.Beleg_Nr
|
|
|
+ """
|
|
|
)
|
|
|
+ return [dict(row) for row in db.execute(query, {"auth_username": principal.username}).mappings().all()]
|
|
|
+
|
|
|
+
|
|
|
+@router.get("/detail/{id}/export/docx")
|
|
|
+def export_docx_legacy(id: int):
|
|
|
+ raise HTTPException(status_code=410, detail="Veralteter Export; bitte die Forderungsdetailseite verwenden")
|
|
|
|
|
|
|
|
|
# TODO
|
|
|
@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/details/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"
|
|
|
- )
|
|
|
+ principal = current_principal(request)
|
|
|
+ ensure_forderung_access(db, principal, client_db, document_no)
|
|
|
+ q = db.execute(
|
|
|
+ text(
|
|
|
+ """SELECT TOP 1 F.*, K.Kunde_Name, K.Kunde_Adresse, K.Kunde_PLZ, K.Kunde_Ort
|
|
|
+ FROM dbo.Forderungen_Kopf AS F
|
|
|
+ LEFT JOIN dbo.Kunden AS K ON K.Client_DB = F.Client_DB AND K.Kunde_Nr = F.Kunde_Nr
|
|
|
+ WHERE F.Client_DB = :client_db AND F.Beleg_Nr = :beleg_nr"""
|
|
|
+ ),
|
|
|
+ {"client_db": client_db, "beleg_nr": document_no},
|
|
|
+ ).mappings().first()
|
|
|
+ if not q or not settings.LOCAL_STORAGE:
|
|
|
+ raise HTTPException(status_code=503, detail="Dokumentablage ist nicht konfiguriert")
|
|
|
+
|
|
|
+ doc = DocxTemplate("templates\\forderungen\\details\\docs\\Mahnung_AHR.docx")
|
|
|
+ storage_root = Path(settings.LOCAL_STORAGE).resolve()
|
|
|
+ filename = (storage_root / client_db / document_no / f"Mahnung_{client_db}_{document_no}.docx").resolve()
|
|
|
+ if storage_root not in filename.parents:
|
|
|
+ raise HTTPException(status_code=400, detail="Ungültiger Dokumentpfad")
|
|
|
filename.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
context = {
|
|
|
- "Kunde_Name": q.Kunde,
|
|
|
- "Kunde_Adresse": "Im Waldhof 14a",
|
|
|
- "Kunde_PLZ": "61476",
|
|
|
- "Kunde_Ort": "Kronberg",
|
|
|
+ "Kunde_Name": q["Kunde_Name"] or "",
|
|
|
+ "Kunde_Adresse": q["Kunde_Adresse"] or "",
|
|
|
+ "Kunde_PLZ": q["Kunde_PLZ"] or "",
|
|
|
+ "Kunde_Ort": q["Kunde_Ort"] or "",
|
|
|
"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",
|
|
|
+ "Kunde_Nr": q["Kunde_Nr"] or "",
|
|
|
+ "Fahrzeug_Kennzeichen": q.get("Fahrzeug_Kennzeichen", "") or "",
|
|
|
+ "Rechnung_Nr": q["Beleg_Nr"],
|
|
|
+ "Betrag_SB": q.get("Selbstbeteiligung_Betrag", "") or "",
|
|
|
+ "Betrag_USt": q.get("Mwst_Betrag", "") or "",
|
|
|
}
|
|
|
doc.render(context)
|
|
|
doc.save(filename)
|
|
|
@@ -391,22 +450,31 @@ def export_docx2(request: Request, client_db: str, document_no: str, db: Session
|
|
|
|
|
|
|
|
|
@router.get("/files/")
|
|
|
-def get_files2(request: Request):
|
|
|
- filename = get_file(request.query_params.get("name"))
|
|
|
+def get_files2(request: Request, db: Session = Depends(get_session)):
|
|
|
+ principal = current_principal(request)
|
|
|
+ name = request.query_params.get("name", "")
|
|
|
+ parts = Path(unquote(name)).parts
|
|
|
+ if len(parts) < 3:
|
|
|
+ raise HTTPException(status_code=404, detail="Datei nicht gefunden")
|
|
|
+ ensure_forderung_access(db, principal, parts[0], parts[1])
|
|
|
+ try:
|
|
|
+ filename = get_file(name)
|
|
|
+ except FileNotFoundError:
|
|
|
+ raise HTTPException(status_code=404, detail="Datei nicht gefunden")
|
|
|
return FileResponse(filename, media_type="application/octet-stream", filename=filename.name)
|
|
|
|
|
|
|
|
|
@router.get("/app/forderungen/files/{primary_key}", response_class=StreamingResponse)
|
|
|
def forderungen_files(request: Request, primary_key: str, db: Session = Depends(get_session)):
|
|
|
+ principal = current_principal(request)
|
|
|
pkey = convert_primary_key("forderungen", primary_key)
|
|
|
- filename = "http://localhost:8093/static/assets/pdf/invoice.pdf"
|
|
|
- filestream = requests.get(filename, stream=True).raw
|
|
|
+ ensure_forderung_access(db, principal, pkey["Client_DB"], pkey["Beleg_Nr"])
|
|
|
try:
|
|
|
file_content = get_invoice_pdf_by_document_no(client_db=pkey["Client_DB"], document_no=pkey["Beleg_Nr"])
|
|
|
filestream = io.BytesIO(file_content)
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- print(f"Error fetching invoice PDF: {e}")
|
|
|
+ except Exception:
|
|
|
+ logger.exception("Rechnung konnte nicht aus DocuWare geladen werden")
|
|
|
+ filestream = io.BytesIO(Path("static/assets/pdf/invoice.pdf").read_bytes())
|
|
|
|
|
|
return StreamingResponse(
|
|
|
filestream,
|