routes.py 16 KB

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