routes.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. import io
  2. import re
  3. from datetime import datetime, timedelta
  4. from pathlib import Path
  5. from typing import Annotated
  6. from urllib.parse import unquote, urlencode
  7. import pandas as pd
  8. import requests
  9. from docx import Document
  10. from docxtpl import DocxTemplate
  11. from fastapi import APIRouter, Depends, Form, HTTPException, Request, Response
  12. from fastapi.responses import (
  13. FileResponse,
  14. HTMLResponse,
  15. RedirectResponse,
  16. StreamingResponse,
  17. )
  18. from sqlalchemy import text
  19. from sqlalchemy.orm import Session
  20. from app.file_storage import get_file, get_files
  21. from app.query_filter import filter_type_option, import_filter_config
  22. from .db import get_session
  23. from .docuware_search import get_invoice_pdf_by_document_no
  24. from .jinja_templates import templates
  25. from .ldap import ldap_authenticate
  26. from .models import Forderung
  27. router = APIRouter()
  28. @router.get("/", response_class=HTMLResponse)
  29. def index(request: Request):
  30. return RedirectResponse(url="/login")
  31. @router.get("/login", response_class=HTMLResponse)
  32. def login_get(request: Request):
  33. return templates.TemplateResponse(request, "base/login.html", {"request": request})
  34. @router.get("/select", response_class=HTMLResponse)
  35. def select_get(request: Request):
  36. return templates.TemplateResponse(request, "base/select.html", {"request": request})
  37. @router.post("/login")
  38. def login_post(username: str = Form(...), password: str = Form(...)):
  39. user = ldap_authenticate(username, password)
  40. if not user:
  41. raise HTTPException(status_code=401, detail="Invalid credentials")
  42. # For a simple demo we set a cookie (not secure) -- replace with real session in production
  43. response = RedirectResponse(url="/forderungen", status_code=302)
  44. response.set_cookie("user", user["username"])
  45. return response
  46. def single_quote(text: str):
  47. return "'" + unquote(text) + "'"
  48. @router.get("/app/forderungen/liste", response_class=HTMLResponse)
  49. def forderungen_liste(request: Request, db: Session = Depends(get_session), limit: int = 100, page: int = 0):
  50. forderung_filter_config = import_filter_config("forderungen")
  51. params = {
  52. f.name: request.query_params[f.name]
  53. for f in forderung_filter_config
  54. if request.query_params.get(f.name, "") != ""
  55. }
  56. if page == 0 and len(params) < len(request.query_params):
  57. return RedirectResponse(url="/app/forderungen/liste?" + urlencode(params))
  58. context = {}
  59. for f in forderung_filter_config:
  60. context[f.name] = {filter_type_option[f.filter_type]: single_quote(params.get(f.name, f.default_value))}
  61. if f.name in params:
  62. f.current_value = params[f.name]
  63. query = templates.TemplateResponse(request, "forderungen/liste/queries/forderungen_liste.sql", context).body.decode(
  64. "utf-8"
  65. )
  66. query_params = [
  67. f.query_param for f in forderung_filter_config if f.query_param is not None and f.filter_type != "hidden"
  68. ]
  69. if query_params:
  70. query = query.replace("1 = 1", " AND ".join(query_params))
  71. print(query)
  72. print(query_params)
  73. # q = db.execute(text("SELECT * FROM [dbo].[Forderungen]"))
  74. q_res = db.execute(text(query))
  75. col_names = list(q_res.keys())
  76. q = q_res.fetchall()
  77. filters = {}
  78. for f in forderung_filter_config:
  79. if f.key_column not in col_names:
  80. continue
  81. filter_temp = [
  82. (
  83. row[col_names.index(f.sort_column)] or "",
  84. row[col_names.index(f.key_column)],
  85. row[col_names.index(f.value_column)],
  86. )
  87. for row in q
  88. ]
  89. if filter_temp:
  90. print(filter_temp[0])
  91. filters[f.name] = {item[1]: item[2] for item in sorted(list(set(filter_temp)))}
  92. else:
  93. print(f"Filter {f.name} has no options, skipping")
  94. filters[f.name] = {}
  95. f.options = filters[f.name]
  96. summary = {
  97. "Beleg_offen": sum([r[col_names.index("Beleg_offen")] for r in q]),
  98. "Anzahl": len(q),
  99. }
  100. q_limit = q[page * limit : (page + 1) * limit]
  101. defaults = {}
  102. for f in forderung_filter_config:
  103. defaults[f.name] = unquote(request.query_params.get(f.name, f.default_value))
  104. f.current_value = defaults[f.name]
  105. if len(q_limit) == 0:
  106. return None
  107. template = "base/liste.html"
  108. if page > 0:
  109. template = "base/liste_tabelle.html"
  110. return templates.TemplateResponse(
  111. request,
  112. template,
  113. {
  114. "request": request,
  115. "forderungen_liste": q_limit,
  116. "filter": filters,
  117. "defaults": defaults,
  118. "summary": summary,
  119. "page": page,
  120. "filter_config": forderung_filter_config,
  121. },
  122. )
  123. @router.get("/app/forderungen/details/{primary_key}", response_class=HTMLResponse)
  124. def forderungen_details(request: Request, primary_key: str, db: Session = Depends(get_session)):
  125. pkey = convert_primary_key("forderungen", primary_key)
  126. template_context = get_template_context(request, pkey, db)
  127. return templates.TemplateResponse(request, "forderungen/details/details.html", template_context)
  128. def get_template_context(request: Request, pkey: dict[str, str], db: Session) -> dict:
  129. context = {
  130. "appsmith": {
  131. "URL": {
  132. "queryParams": {
  133. "Client_DB": "'" + pkey["Client_DB"] + "'",
  134. "Beleg_Nr": "'" + pkey["Beleg_Nr"] + "'",
  135. }
  136. }
  137. },
  138. "BelegeFilter": {"selectedOptionValue": "D"},
  139. "BenutzerSelect": {"selectedOptionValue": "winter"},
  140. }
  141. template_context = {}
  142. for filename in [
  143. "auftraege_positionen",
  144. "forderungen_buchungen",
  145. "forderungen_kommentare",
  146. "forderungen_details",
  147. "forderungen_mahnungen",
  148. "versicherungen",
  149. ]:
  150. query = templates.TemplateResponse(request, f"forderungen/details/queries/{filename}.sql", context).body.decode(
  151. "utf-8"
  152. )
  153. template_context[filename] = db.execute(text(query)).fetchall()
  154. template_context["forderung"] = {
  155. "id": "12345",
  156. "rechnungsnummer": "WERE123445",
  157. "kunde": "Burghard",
  158. "betrag": "120,00",
  159. "faelligkeit": "23.12.1983",
  160. }
  161. template_context["files"] = get_files(pkey)
  162. template_context["add7days"] = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d")
  163. return template_context
  164. @router.post("/app/forderungen/details/{primary_key}", response_class=HTMLResponse)
  165. async def post_forderungen_details(
  166. request: Request,
  167. primary_key: str,
  168. source: Annotated[str, Form()],
  169. redirect: Annotated[str | None, Form()] = None,
  170. db: Session = Depends(get_session),
  171. ):
  172. pkey = convert_primary_key("forderungen", primary_key)
  173. if source == "comments":
  174. data = {
  175. "Client_DB": "",
  176. "Beleg_Nr": "",
  177. "Timestamp": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
  178. "Benutzer_ID": "winter",
  179. "Kommentar": "",
  180. }
  181. else:
  182. data = {
  183. "Client_DB": "",
  184. "Beleg_Nr": "",
  185. "Bearbeitet": "J",
  186. "Versicherung": "N",
  187. "Rechtsanwalt": "N",
  188. "Reklamation": "N",
  189. "Mahnen_aussetzen": "N",
  190. "Selbstbeteiligung": "",
  191. "Selbstbeteiligung_Betrag": "0.0",
  192. "Mwst": "",
  193. "Mwst_Betrag": "0.0",
  194. "Wiedervorlage": "",
  195. "Vers_Adresse_ID": "",
  196. "Versicherung_Typ": "",
  197. "Reklamation_Begruendung": "",
  198. "Rechtsanwalt_Begruendung": "",
  199. "Rechtsanwalt_Aktenzeichen": "",
  200. "Rechtsanwalt_Stand": "",
  201. "Mahnen_Begruendung": "",
  202. }
  203. data.update(pkey)
  204. async with request.form() as form:
  205. for k, v in form.items():
  206. if k in data:
  207. if v == "on":
  208. v = "J"
  209. if re.match(r"\d+,?\d*", v):
  210. v = v.replace(",", ".")
  211. data[k] = v
  212. query = templates.TemplateResponse(
  213. request, f"forderungen/details/queries/post/post_{source}.sql", context={"data": data}
  214. ).body.decode("utf-8")
  215. print(query)
  216. for subquery in query.split(";"):
  217. q = db.execute(text(subquery))
  218. print(q.rowcount)
  219. db.commit()
  220. template_context = get_template_context(request, pkey, db)
  221. if redirect:
  222. return RedirectResponse(url=redirect, status_code=302)
  223. if source == "comments":
  224. return templates.TemplateResponse(request, "base/chat_container.html", template_context)
  225. return templates.TemplateResponse(request, "forderungen/details/details_formular.html", template_context)
  226. def convert_primary_key(area: str, primary_key: str) -> dict[str, str]:
  227. if area == "forderungen":
  228. pkey = ["Client_DB", "Beleg_Nr"]
  229. pkey_values = primary_key.split("_")
  230. return dict(zip(pkey, pkey_values))
  231. @router.get("/app/forderungen/dashboard", response_class=HTMLResponse)
  232. def forderungen_dashboard(request: Request, db: Session = Depends(get_session)):
  233. context = {"summary": False}
  234. return templates.TemplateResponse(request, "forderungen/dashboard/dashboard.html", context)
  235. @router.get("/export/csv")
  236. def export_csv(db: Session = Depends(get_session)):
  237. q = db.query(Forderung).all()
  238. rows = []
  239. for f in q:
  240. rows.append(
  241. {
  242. "id": f.id,
  243. "rechnungsnummer": f.rechnungsnummer,
  244. "kunde": f.kunde.name if f.kunde else "",
  245. "betrag": float(f.betrag),
  246. "faelligkeit": f.faelligkeit.isoformat() if f.faelligkeit else "",
  247. }
  248. )
  249. df = pd.DataFrame(rows)
  250. buf = io.StringIO()
  251. df.to_csv(buf, index=False)
  252. buf.seek(0)
  253. return Response(
  254. content=buf.getvalue(),
  255. media_type="text/csv",
  256. headers={"Content-Disposition": "attachment; filename=forderungen.csv"},
  257. )
  258. @router.get("/export/xlsx")
  259. def export_xlsx(db: Session = Depends(get_session)):
  260. q = db.query(Forderung).all()
  261. rows = []
  262. for f in q:
  263. rows.append(
  264. {
  265. "id": f.id,
  266. "rechnungsnummer": f.rechnungsnummer,
  267. "kunde": f.kunde.name if f.kunde else "",
  268. "betrag": float(f.betrag),
  269. "faelligkeit": f.faelligkeit.isoformat() if f.faelligkeit else "",
  270. }
  271. )
  272. df = pd.DataFrame(rows)
  273. buf = io.BytesIO()
  274. with pd.ExcelWriter(buf, engine="openpyxl") as writer:
  275. df.to_excel(writer, index=False, sheet_name="Forderungen")
  276. buf.seek(0)
  277. return StreamingResponse(
  278. buf,
  279. media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  280. headers={"Content-Disposition": "attachment; filename=forderungen.xlsx"},
  281. )
  282. @router.get("/detail/{id}/export/docx")
  283. def export_docx(id: int, db: Session = Depends(get_session)):
  284. f = db.query(Forderung).filter(Forderung.id == id).first()
  285. if not f:
  286. raise HTTPException(status_code=404)
  287. doc = Document()
  288. doc.add_heading(f"Rechnung {f.rechnungsnummer}", level=1)
  289. doc.add_paragraph(f'Kunde: {f.kunde.name if f.kunde else ""}')
  290. doc.add_paragraph(f"Betrag: {float(f.betrag)}")
  291. doc.add_paragraph(f"Fälligkeit: {f.faelligkeit}")
  292. buf = io.BytesIO()
  293. doc.save(buf)
  294. buf.seek(0)
  295. return StreamingResponse(
  296. buf,
  297. media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  298. headers={"Content-Disposition": f"attachment; filename=rechnung_{f.rechnungsnummer}.docx"},
  299. )
  300. # TODO
  301. @router.get("/app/forderungen/export/{client_db}_{document_no}/")
  302. def export_docx2(request: Request, client_db: str, document_no: str, db: Session = Depends(get_session)):
  303. context = {
  304. "appsmith": {
  305. "URL": {
  306. "queryParams": {
  307. "Client_DB": "'" + client_db + "'",
  308. "Document_No": "'" + document_no + "'",
  309. }
  310. }
  311. },
  312. }
  313. query = templates.TemplateResponse(request, "forderungen/details/queries/forderung_kopf.sql", context).body.decode(
  314. "utf-8"
  315. )
  316. q = db.execute(text(query)).fetchone()
  317. doc = DocxTemplate("templates\\forderungen\\docs\\Mahnung_AHR.docx")
  318. filename = Path(
  319. f"C:\\Projekte\\Reisacher-Fileserver\\{client_db}\\{document_no}\\Mahnung_{client_db}_{document_no}.docx"
  320. )
  321. filename.parent.mkdir(parents=True, exist_ok=True)
  322. context = {
  323. "Kunde_Name": q.Kunde,
  324. "Kunde_Adresse": "Im Waldhof 14a",
  325. "Kunde_PLZ": "61476",
  326. "Kunde_Ort": "Kronberg",
  327. "Datum_heute": datetime.now().strftime("%d.%m.%Y"),
  328. "Kunde_Nr": q.Kunde.split("-")[-1].strip(),
  329. "Fahrzeug_Kennzeichen": "OF-RB 512",
  330. "Rechnung_Nr": q.Document_No,
  331. "Betrag_SB": "250,00",
  332. "Betrag_USt": "380,00",
  333. }
  334. doc.render(context)
  335. doc.save(filename)
  336. return FileResponse(filename, media_type="application/octet-stream", filename=filename.name)
  337. @router.get("/files/")
  338. def get_files2(request: Request):
  339. filename = get_file(request.query_params.get("name"))
  340. return FileResponse(filename, media_type="application/octet-stream", filename=filename.name)
  341. @router.get("/app/forderungen/files/{primary_key}", response_class=HTMLResponse)
  342. def forderungen_files(request: Request, primary_key: str, db: Session = Depends(get_session)):
  343. pkey = convert_primary_key("forderungen", primary_key)
  344. try:
  345. filename = get_invoice_pdf_by_document_no(client_db=pkey["Client_DB"], document_no=pkey["Beleg_Nr"])
  346. # filename = "http://localhost:8093/static/assets/pdf/invoice.pdf"
  347. return StreamingResponse(
  348. requests.get(filename, stream=True).raw,
  349. media_type="application/pdf",
  350. )
  351. except ValueError as e:
  352. raise HTTPException(status_code=404, detail=str(e))