routes.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import io
  2. from datetime import datetime
  3. from operator import itemgetter
  4. from pathlib import Path
  5. from urllib.parse import unquote, urlencode
  6. import pandas as pd
  7. from docx import Document
  8. from docxtpl import DocxTemplate
  9. from fastapi import APIRouter, Depends, Form, HTTPException, Request, Response
  10. from fastapi.responses import (
  11. FileResponse,
  12. HTMLResponse,
  13. RedirectResponse,
  14. StreamingResponse,
  15. )
  16. from fastapi.templating import Jinja2Templates
  17. from sqlalchemy import text
  18. from sqlalchemy.orm import Session
  19. from .auth import ldap_authenticate
  20. from .db import get_session
  21. from .models import Bemerkung, Forderung
  22. from .schemas import BemerkungIn
  23. router = APIRouter()
  24. templates = Jinja2Templates(directory="templates")
  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 number_format(input: float) -> str:
  48. return format(input, "0,.2f").replace(".", ":").replace(",", ".").replace(":", ",")
  49. def date_format(input: datetime) -> str:
  50. if input is None:
  51. return ""
  52. return input.strftime("%d.%m.%Y")
  53. templates.env.filters["number_format"] = number_format
  54. templates.env.filters["date_format"] = date_format
  55. def single_quote(text: str):
  56. return "'" + unquote(text) + "'"
  57. @router.get("/app/forderungen/liste", response_class=HTMLResponse)
  58. def forderungen_liste(request: Request, db: Session = Depends(get_session), limit: int = 100, page: int = 0):
  59. params = {k: v for k, v in request.query_params.items() if v != ""}
  60. if len(params) < len(request.query_params):
  61. return RedirectResponse(url="/app/forderungen/liste?" + urlencode(params))
  62. context = {
  63. "Hauptbetrieb": {"selectedOptionValue": single_quote(params.get("Hauptbetrieb", ""))},
  64. "Standort": {"selectedOptionValue": single_quote(params.get("Standort", ""))},
  65. "Bereich": {"selectedOptionValue": single_quote(params.get("Bereich", ""))},
  66. "Verursacher": {"selectedOptionValue": single_quote(params.get("Verursacher", ""))},
  67. "Rechnungsnummer": {"text": single_quote(params.get("Rechnungsnummer", ""))},
  68. "RechnungsdatumVon": {"selectedDate": single_quote(params.get("RechnungsdatumVon", "2000-01-01T00:00:00"))},
  69. "RechnungsdatumBis": {"selectedDate": single_quote(params.get("RechnungsdatumBis", "2027-01-01T00:00:00"))},
  70. "Kunde": {"selectedOptionValue": single_quote(params.get("Kunde", ""))},
  71. "WiedervorlageVon": {"selectedDate": single_quote(params.get("WiedervorlageVon", "2000-01-01T00:00:00"))},
  72. "WiedervorlageBis": {"selectedDate": single_quote(params.get("WiedervorlageBis", "2027-01-01T00:00:00"))},
  73. "Fahrzeug": {"selectedOptionValue": single_quote(params.get("Fahrzeug", ""))},
  74. "Staffel": {"selectedOptionValue": single_quote(params.get("Staffel", ""))},
  75. "Mahnstufe": {"selectedOptionValue": single_quote(params.get("Mahnstufe", ""))},
  76. "BenutzerSelect": {"selectedOptionValue": "winter"},
  77. }
  78. query = templates.TemplateResponse(request, "forderungen/queries/forderungen_liste.sql", context).body.decode(
  79. "utf-8"
  80. )
  81. # print(query)
  82. # q = db.execute(text("SELECT * FROM [dbo].[Forderungen]"))
  83. q = db.execute(text(query)).fetchall()
  84. col_names = list(q[0]._asdict().keys())
  85. config = {
  86. # "RechnungsdatumVon": (),
  87. # "RechnungsdatumBis": {"selectedDate": "'2027-01-01T00:00:00'"},
  88. "Hauptbetrieb": ("Client_DB", "Hauptbetrieb_Name"),
  89. "Standort": ("Standort_ID", "Standort_Name"),
  90. # "Rechnungsnummer": {"text": "''"},
  91. "Kunde": ("Kunde", "Kunde"),
  92. "Bereich": ("Bereich", "Bereich"),
  93. "Verursacher": ("Verursacher", "Verursacher"),
  94. "Fahrzeug": ("VIN", "VIN"),
  95. "Staffel": ("Staffel", "Staffel"),
  96. "Mahnstufe": ("Mahnstufe", "Mahnstufe"),
  97. # "WiedervorlageVon": {"selectedDate": "'2000-01-01T00:00:00'"},
  98. # "WiedervorlageBis": {"selectedDate": "'2027-01-01T00:00:00'"},
  99. # "BenutzerSelect": {"selectedOptionValue": "winter"},
  100. }
  101. filters = {}
  102. for filter_name, (key, value) in config.items():
  103. filters[filter_name] = {row[col_names.index(key)]: row[col_names.index(value)] for row in q}
  104. summary = {
  105. "offen": sum([r[col_names.index("offen")] for r in q]),
  106. "Anzahl": len(q),
  107. }
  108. q_limit = q[page * limit : (page + 1) * limit]
  109. defaults = {}
  110. for filter_name in config.keys():
  111. defaults[filter_name] = unquote(request.query_params.get(filter_name, ""))
  112. # print(filters["Standort"])
  113. if len(q_limit) == 0:
  114. return None
  115. template = "forderungen/liste.html"
  116. if page > 0:
  117. template = "forderungen/liste_tabelle.html"
  118. return templates.TemplateResponse(
  119. request,
  120. template,
  121. {
  122. "request": request,
  123. "forderungen_liste": q_limit,
  124. "filter": filters,
  125. "defaults": defaults,
  126. "summary": summary,
  127. "page": page,
  128. },
  129. )
  130. @router.get("/app/forderungen/details/{client_db}_{document_no}", response_class=HTMLResponse)
  131. def forderungen_details(
  132. request: Request, client_db: str, document_no: str, db: Session = Depends(get_session), limit: int = 100
  133. ):
  134. context = {
  135. "appsmith": {
  136. "URL": {
  137. "queryParams": {
  138. "Client_DB": "'" + client_db + "'",
  139. "Document_No": "'" + document_no + "'",
  140. }
  141. }
  142. },
  143. "BelegeFilter": {"selectedOptionValue": "D"},
  144. "BenutzerSelect": {"selectedOptionValue": "winter"},
  145. }
  146. template_context = {}
  147. for filename in [
  148. "auftrag_positionen",
  149. "forderung_belege",
  150. "forderung_kommentar",
  151. "forderung_kopf",
  152. "forderung_mahnung",
  153. ]:
  154. query = templates.TemplateResponse(request, f"forderungen/queries/{filename}.sql", context).body.decode("utf-8")
  155. template_context[filename] = db.execute(text(query)).fetchall()
  156. template_context["forderung"] = {
  157. "id": "12345",
  158. "rechnungsnummer": "WERE123445",
  159. "kunde": "Burghard",
  160. "betrag": "120,00",
  161. "faelligkeit": "23.12.1983",
  162. }
  163. template_context["files"] = list(Path(f"C:\\Projekte\\Reisacher-Fileserver\\{client_db}\\{document_no}").glob("*"))
  164. return templates.TemplateResponse(request, "forderungen/details.html", template_context)
  165. @router.get("/app/forderungen/dashboard", response_class=HTMLResponse)
  166. def forderungen_dashboard(request: Request, db: Session = Depends(get_session)):
  167. context = {"summary": False}
  168. return templates.TemplateResponse(request, "forderungen/dashboard.html", context)
  169. @router.get("/chat", response_class=HTMLResponse)
  170. def chat(request: Request):
  171. # q = db.query(Forderung).order_by(Forderung.faelligkeit.asc()).limit(limit).all()
  172. return templates.TemplateResponse(request, "base/chat.html")
  173. @router.get("/export/csv")
  174. def export_csv(db: Session = Depends(get_session)):
  175. q = db.query(Forderung).all()
  176. rows = []
  177. for f in q:
  178. rows.append(
  179. {
  180. "id": f.id,
  181. "rechnungsnummer": f.rechnungsnummer,
  182. "kunde": f.kunde.name if f.kunde else "",
  183. "betrag": float(f.betrag),
  184. "faelligkeit": f.faelligkeit.isoformat() if f.faelligkeit else "",
  185. }
  186. )
  187. df = pd.DataFrame(rows)
  188. buf = io.StringIO()
  189. df.to_csv(buf, index=False)
  190. buf.seek(0)
  191. return Response(
  192. content=buf.getvalue(),
  193. media_type="text/csv",
  194. headers={"Content-Disposition": "attachment; filename=forderungen.csv"},
  195. )
  196. @router.get("/export/xlsx")
  197. def export_xlsx(db: Session = Depends(get_session)):
  198. q = db.query(Forderung).all()
  199. rows = []
  200. for f in q:
  201. rows.append(
  202. {
  203. "id": f.id,
  204. "rechnungsnummer": f.rechnungsnummer,
  205. "kunde": f.kunde.name if f.kunde else "",
  206. "betrag": float(f.betrag),
  207. "faelligkeit": f.faelligkeit.isoformat() if f.faelligkeit else "",
  208. }
  209. )
  210. df = pd.DataFrame(rows)
  211. buf = io.BytesIO()
  212. with pd.ExcelWriter(buf, engine="openpyxl") as writer:
  213. df.to_excel(writer, index=False, sheet_name="Forderungen")
  214. buf.seek(0)
  215. return StreamingResponse(
  216. buf,
  217. media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  218. headers={"Content-Disposition": "attachment; filename=forderungen.xlsx"},
  219. )
  220. @router.get("/detail/{id}", response_class=HTMLResponse)
  221. def detail(request: Request, id: int, db: Session = Depends(get_session)):
  222. f = db.query(Forderung).filter(Forderung.id == id).first()
  223. if not f:
  224. raise HTTPException(status_code=404)
  225. return templates.TemplateResponse(request, "base/detail.html", {"request": request, "forderung": f})
  226. @router.post("/detail/{id}/bemerkung")
  227. def save_bemerkung(id: int, data: BemerkungIn, request: Request, db: Session = Depends(get_session)):
  228. user = request.cookies.get("user") or "anonymous"
  229. b = Bemerkung(
  230. forderung_id=id, benutzer=user, bemerkung=data.bemerkung, wiedervorlage_datum=data.wiedervorlage_datum
  231. )
  232. db.add(b)
  233. db.commit()
  234. return RedirectResponse(url=f"/detail/{id}", status_code=302)
  235. @router.get("/detail/{id}/export/docx")
  236. def export_docx(id: int, db: Session = Depends(get_session)):
  237. f = db.query(Forderung).filter(Forderung.id == id).first()
  238. if not f:
  239. raise HTTPException(status_code=404)
  240. doc = Document()
  241. doc.add_heading(f"Rechnung {f.rechnungsnummer}", level=1)
  242. doc.add_paragraph(f'Kunde: {f.kunde.name if f.kunde else ""}')
  243. doc.add_paragraph(f"Betrag: {float(f.betrag)}")
  244. doc.add_paragraph(f"Fälligkeit: {f.faelligkeit}")
  245. buf = io.BytesIO()
  246. doc.save(buf)
  247. buf.seek(0)
  248. return StreamingResponse(
  249. buf,
  250. media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  251. headers={"Content-Disposition": f"attachment; filename=rechnung_{f.rechnungsnummer}.docx"},
  252. )
  253. @router.get("/app/forderungen/export/{client_db}_{document_no}/")
  254. def export_docx2(request: Request, client_db: str, document_no: str, db: Session = Depends(get_session)):
  255. context = {
  256. "appsmith": {
  257. "URL": {
  258. "queryParams": {
  259. "Client_DB": "'" + client_db + "'",
  260. "Document_No": "'" + document_no + "'",
  261. }
  262. }
  263. },
  264. }
  265. query = templates.TemplateResponse(request, "forderungen/queries/forderung_kopf.sql", context).body.decode("utf-8")
  266. q = db.execute(text(query)).fetchone()
  267. doc = DocxTemplate("templates\\forderungen\\docs\\Mahnung_AHR.docx")
  268. filename = Path(
  269. f"C:\\Projekte\\Reisacher-Fileserver\\{client_db}\\{document_no}\\Mahnung_{client_db}_{document_no}.docx"
  270. )
  271. filename.parent.mkdir(parents=True, exist_ok=True)
  272. context = {
  273. "Kunde_Name": q.Kunde,
  274. "Kunde_Adresse": "Im Waldhof 14a",
  275. "Kunde_PLZ": "61476",
  276. "Kunde_Ort": "Kronberg",
  277. "Datum_heute": datetime.now().strftime("%d.%m.%Y"),
  278. "Kunde_Nr": q.Kunde.split("-")[-1].strip(),
  279. "Fahrzeug_Kennzeichen": "OF-RB 512",
  280. "Rechnung_Nr": q.Document_No,
  281. "Betrag_SB": "250,00",
  282. "Betrag_USt": "380,00",
  283. }
  284. doc.render(context)
  285. doc.save(filename)
  286. return FileResponse(filename, media_type="application/octet-stream", filename=filename.name)