routes.py 15 KB

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