Guest User

invoices.py

a guest
Aug 8th, 2025
60
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.52 KB | None | 0 0
  1. import re, os, datetime
  2. from fastapi import APIRouter, Request, Form, BackgroundTasks
  3. from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse
  4. from backend.routes.pdf_templates import create_documents_pdf
  5. from backend.config import templates, db
  6. from datetime import datetime, timedelta
  7. from bson import ObjectId
  8. from backend.data.currencies import CURRENCY_SYMBOLS
  9. from backend.routes.settings_preferences import settings_col
  10. from typing import List, Optional, Union
  11. from jinja2 import Undefined
  12.  
  13. router = APIRouter()
  14. invoices_col = db["invoices"]
  15. customers_col = db["customers"]
  16. artikelen_col = db["artikelen"]
  17.  
  18. DATE_FORMAT_MAP = {
  19. "DD-MM-YYYY": "%d-%m-%Y",
  20. "YYYY-MM-DD": "%Y-%m-%d",
  21. "MM/DD/YYYY": "%m/%d/%Y",
  22. "DD/MM/YYYY": "%d/%m/%Y",
  23. "YYYY/MM/DD": "%Y/%m/%d",
  24. "MMM DD, YYYY": "%b %d, %Y",
  25. "DD MMM YYYY": "%d %b %Y",
  26. "YYYY.MM.DD": "%Y.%m.%d",
  27. "DD.MM.YYYY": "%d.%m.%Y",
  28. }
  29.  
  30. def clean_item(item):
  31. item = dict(item)
  32. for k, v in item.items():
  33. if isinstance(v, datetime):
  34. item[k] = v.isoformat()
  35. elif isinstance(v, ObjectId):
  36. item[k] = str(v)
  37. if "prijs" in item:
  38. try:
  39. item["prijs"] = "{:.2f}".format(float(item["prijs"])).replace(".", ",")
  40. except Exception:
  41. item["prijs"] = str(item["prijs"])
  42. return {k: v for k, v in item.items() if v is not None and v != "Undefined"}
  43.  
  44. async def get_invoices_for_user(user_id):
  45. invoices = await invoices_col.find({"user_id": user_id}).to_list(length=100)
  46. today = datetime.today().date()
  47. for invoice in invoices:
  48. vervaldatum = invoice.get("vervaldatum")
  49. if vervaldatum:
  50. try:
  51. verval_date = datetime.strptime(vervaldatum, "%Y-%m-%d").date()
  52. if invoice.get("status") not in ["betaald", "geannuleerd"] and verval_date < today:
  53. invoice["status"] = "verlopen"
  54. except Exception:
  55. pass
  56. return invoices
  57.  
  58. @router.get("/admin/invoices", response_class=HTMLResponse)
  59. async def invoices_overview(request: Request):
  60. session_user = request.session.get("user")
  61. if not session_user:
  62. return RedirectResponse("/login")
  63. status = request.query_params.get("status", "all")
  64. query = {"user_id": session_user["id"]}
  65. if status == "concept":
  66. query["status"] = "concept"
  67. elif status == "verzonden":
  68. query["status"] = "verzonden"
  69. invoices_cursor = invoices_col.find(query).sort("toegevoegd", -1)
  70. invoices = []
  71. async for invoice in invoices_cursor:
  72. invoice["id"] = str(invoice["_id"])
  73. klant = await customers_col.find_one({"_id": ObjectId(invoice["klant"])})
  74. invoice["klant_naam"] = klant["naam"] if klant else ""
  75. invoice["totaal"] = sum(item["aantal"] * item["prijs"] for item in invoice.get("items", []))
  76. invoice["status"] = invoice.get("status", "concept")
  77. invoices.append(invoice)
  78. prefs = await settings_col.find_one({"user_id": session_user["id"]}) or {}
  79. date_format_python = DATE_FORMAT_MAP.get(prefs.get("datumnotatie", "DD-MM-YYYY"), "%d-%m-%Y")
  80. valuta_symbol = CURRENCY_SYMBOLS.get(prefs.get("valuta", "EUR"), "€")
  81. return templates.TemplateResponse(
  82. "invoices/invoices.html",
  83. {
  84. "request": request,
  85. "invoices": invoices,
  86. "active_tab": status,
  87. "date_format_python": date_format_python,
  88. "valuta_symbol": valuta_symbol,
  89. }
  90. )
  91.  
  92. @router.get("/admin/invoices/create", response_class=HTMLResponse)
  93. async def create_invoice_page(request: Request):
  94. session_user = request.session.get("user")
  95. if not session_user:
  96. return RedirectResponse("/login")
  97.  
  98. prefs = await settings_col.find_one({"user_id": session_user["id"]}) or {}
  99. date_format = prefs.get("datumnotatie", "YYYY-MM-DD")
  100.  
  101. today_dt = datetime.utcnow()
  102. today = today_dt.strftime("%Y-%m-%d")
  103. vervaldatum = (today_dt + timedelta(days=7)).strftime("%Y-%m-%d")
  104. today_display = today_dt.strftime("%d-%m-%Y")
  105. vervaldatum_display = (today_dt + timedelta(days=7)).strftime("%d-%m-%Y")
  106.  
  107. # Fetch all items system-wide for dropdowns
  108. items_raw = await db["items"].find({}).to_list(None)
  109. artikelList = [clean_item(item) for item in items_raw]
  110.  
  111. # Fetch all customers for selector
  112. customers_raw = await customers_col.find({"user_id": session_user["id"]}).to_list(None)
  113. customers = [clean_item(c) for c in customers_raw]
  114.  
  115. return templates.TemplateResponse(
  116. "invoices/create_edit_invoice.html",
  117. {
  118. "request": request,
  119. "invoice": None,
  120. "selected_customer": None,
  121. "selected_customer_name": "",
  122. "customers": customers,
  123. "today": today,
  124. "nummer": await get_next_invoice_number(session_user["id"]),
  125. "vervaldatum": vervaldatum,
  126. "today_display": today_display,
  127. "vervaldatum_display": vervaldatum_display,
  128. "date_format": date_format,
  129. "items": [],
  130. "section_titles": ['Sectie 1'],
  131. "artikelList": artikelList,
  132. "valuta_symbol": CURRENCY_SYMBOLS.get(prefs.get("valuta", "EUR"), "€"),
  133. }
  134. )
  135.  
  136. @router.post("/admin/invoices/create")
  137. async def create_invoice(
  138. request: Request,
  139. klant: str = Form(...),
  140. datum: str = Form(""),
  141. vervaldatum: str = Form(""),
  142. nummer: str = Form(...),
  143. opmerkingen: str = Form(""),
  144. korting: str = Form("0"),
  145. korting_type: str = Form("€"),
  146. section_titles: Optional[List[str]] = Form(None),
  147. item_naam: List[str] = Form(...),
  148. item_beschrijving: List[str] = Form(...),
  149. item_aantal: List[str] = Form(...),
  150. item_prijs: List[str] = Form(...),
  151. item_section: List[int] = Form(...),
  152. ):
  153.  
  154. session_user = request.session.get("user")
  155. if not session_user:
  156. return RedirectResponse("/login")
  157.  
  158. # Normalize section_titles
  159. if section_titles and isinstance(section_titles, str):
  160. section_titles = [section_titles]
  161.  
  162. # Set default dates if not provided
  163. today = datetime.today()
  164. if not datum:
  165. datum = today.strftime("%Y-%m-%d")
  166. if not vervaldatum:
  167. vervaldatum = (today + timedelta(days=14)).strftime("%Y-%m-%d")
  168.  
  169. items = []
  170. for naam, beschrijving, aantal, prijs, section in zip(item_naam, item_beschrijving, item_aantal, item_prijs, item_section):
  171. items.append({
  172. "naam": naam,
  173. "beschrijving": beschrijving,
  174. "aantal": int(aantal),
  175. "prijs": float(prijs.replace(",", ".")),
  176. "section": int(section)
  177. })
  178.  
  179. invoice = {
  180. "klant": klant,
  181. "datum": datum,
  182. "vervaldatum": vervaldatum,
  183. "nummer": nummer,
  184. "opmerkingen": opmerkingen,
  185. "korting": korting,
  186. "korting_type": korting_type,
  187. "section_titles": section_titles or [],
  188. "items": items,
  189. "user_id": session_user["id"],
  190. "toegevoegd": datetime.utcnow(),
  191. "status": "concept",
  192. }
  193. await invoices_col.insert_one(invoice)
  194. return RedirectResponse(f"/admin/invoices/{nummer}/view", status_code=303)
  195.  
  196. @router.get("/admin/invoices/{nummer}/edit", response_class=HTMLResponse)
  197. async def edit_invoice_page(request: Request, nummer: str):
  198. session_user = request.session.get("user")
  199. if not session_user:
  200. return RedirectResponse("/login")
  201. invoice = await invoices_col.find_one({"nummer": nummer, "user_id": session_user["id"]})
  202. if not invoice:
  203. return HTMLResponse(status_code=404, content="Factuur niet gevonden")
  204. items = [clean_item(item) for item in invoice.get("items", [])]
  205. items_raw = await db["items"].find({}).to_list(None)
  206. artikelList = [clean_item(item) for item in items_raw if item is not None]
  207. artikelList = [item for item in artikelList if item is not None and item != "Undefined" and is_serializable(item)]
  208. prefs = await settings_col.find_one({"user_id": session_user["id"]}) or {}
  209. valuta_symbol = CURRENCY_SYMBOLS.get(prefs.get("valuta", "EUR"), "€")
  210. klant_obj = None
  211. if invoice.get("klant"):
  212. try:
  213. klant_obj = await customers_col.find_one({"_id": ObjectId(invoice["klant"])})
  214. except Exception:
  215. klant_obj = None
  216. customers_raw = await customers_col.find({"user_id": session_user["id"]}).to_list(None)
  217. customers = [clean_item(c) for c in customers_raw if c is not None]
  218. customers = [c for c in customers if c is not None and c != "Undefined" and is_serializable(c)]
  219. return templates.TemplateResponse(
  220. "invoices/create_edit_invoice.html",
  221. {
  222. "request": request,
  223. "invoice": invoice,
  224. "selected_customer": invoice.get("klant"),
  225. "selected_customer_name": klant_obj["naam"] if klant_obj else "",
  226. "customers": customers,
  227. "today": invoice.get("datum", datetime.utcnow().strftime("%Y-%m-%d")),
  228. "nummer": invoice.get("nummer", ""),
  229. "vervaldatum": invoice.get("vervaldatum", ""),
  230. "items": items,
  231. "section_titles": invoice.get("section_titles", ['Sectie 1']),
  232. "artikelList": artikelList,
  233. "valuta_symbol": valuta_symbol,
  234. }
  235. )
  236.  
  237. @router.post("/admin/invoices/{nummer}/edit")
  238. async def update_invoice(
  239. request: Request,
  240. nummer: str,
  241. klant: str = Form(...),
  242. datum: str = Form(...),
  243. vervaldatum: str = Form(""),
  244. opmerkingen: str = Form(""),
  245. korting: str = Form("0"),
  246. korting_type: str = Form("€"),
  247. section_titles: Optional[List[str]] = Form(None),
  248. item_naam: list[str] = Form(...),
  249. item_beschrijving: list[str] = Form(...),
  250. item_aantal: list[str] = Form(...),
  251. item_prijs: list[str] = Form(...),
  252. item_section: list[str] = Form(...),
  253. ):
  254. session_user = request.session.get("user")
  255. if not session_user:
  256. return RedirectResponse("/login")
  257. items = []
  258. for naam, beschrijving, aantal, prijs, section in zip(item_naam, item_beschrijving, item_aantal, item_prijs, item_section):
  259. items.append({
  260. "naam": naam,
  261. "beschrijving": beschrijving,
  262. "aantal": int(aantal),
  263. "prijs": float(prijs.replace(",", ".")),
  264. "section": int(section)
  265. })
  266. if section_titles and isinstance(section_titles, str):
  267. section_titles = [section_titles]
  268. await invoices_col.update_one(
  269. {"nummer": nummer, "user_id": session_user["id"]},
  270. {"$set": {
  271. "klant": klant,
  272. "datum": datum,
  273. "vervaldatum": vervaldatum,
  274. "nummer": nummer,
  275. "opmerkingen": opmerkingen,
  276. "korting": korting,
  277. "korting_type": korting_type,
  278. "items": items,
  279. "section_titles": section_titles or [],
  280. "status": "concept"
  281. }}
  282. )
  283. return RedirectResponse(f"/admin/invoices/{nummer}/view", status_code=303)
  284.  
  285. async def get_next_invoice_number(user_id):
  286. customization_col = db["customization"]
  287. customization = await customization_col.find_one({"user_id": user_id}) or {}
  288. components = customization.get("facturen_components", [])
  289. prefix = ""
  290. seq_length = 4
  291. for comp in components:
  292. if comp.get("type") == "volgnummer":
  293. seq_length = int(comp.get("length", 4))
  294. break
  295. v = comp.get("value", "")
  296. if comp.get("type") == "scheidingsteken":
  297. prefix += "-"
  298. elif comp.get("type") in ("voorvoegsel", "reeksen"):
  299. prefix += v
  300. prefix_regex = re.escape(prefix) + r"\d{" + str(seq_length) + r"}$"
  301. last_doc = await invoices_col.find_one(
  302. {"nummer": {"$regex": prefix_regex}, "user_id": user_id},
  303. sort=[("nummer", -1)]
  304. )
  305. last_number = last_doc["nummer"] if last_doc else None
  306. def generate_invoice_number(components, last_number=None):
  307. number = ""
  308. seq_length = 4
  309. for comp in components:
  310. t = comp.get("type")
  311. v = comp.get("value", "")
  312. if t == "scheidingsteken":
  313. number += "-"
  314. elif t == "voorvoegsel":
  315. number += v
  316. elif t == "reeksen":
  317. number += v
  318. elif t == "volgnummer":
  319. seq_length = int(comp.get("length", 4))
  320. number += "{SEQ}"
  321. seq = 1
  322. if last_number:
  323. regex = re.escape(number).replace("\\{SEQ\\}", f"(\\d{{{seq_length}}})")
  324. m = re.match(regex, last_number)
  325. if m:
  326. seq = int(m.group(1)) + 1
  327. return number.replace("{SEQ}", str(seq).zfill(seq_length))
  328. if components:
  329. return generate_invoice_number(components, last_number)
  330. else:
  331. last = await invoices_col.find({"user_id": user_id}).sort("toegevoegd", -1).to_list(1)
  332. if last and "nummer" in last[0]:
  333. num = last[0]["nummer"]
  334. try:
  335. prefix, number = num.split("-")
  336. return f"{prefix}-{int(number)+1:04d}"
  337. except Exception:
  338. return "Fac-0001"
  339. return "Fac-0001"
  340.  
  341. @router.post("/admin/invoices/{nummer}/delete")
  342. async def delete_invoice(request: Request, nummer: str):
  343. session_user = request.session.get("user")
  344. if not session_user:
  345. return RedirectResponse("/login")
  346. await invoices_col.delete_one({"nummer": nummer, "user_id": session_user["id"]})
  347. return RedirectResponse("/admin/invoices", status_code=303)
  348.  
  349. @router.get("/admin/invoices/{nummer}/view", response_class=HTMLResponse)
  350. async def view_invoice(request: Request, nummer: str):
  351. session_user = request.session.get("user")
  352. if not session_user:
  353. return RedirectResponse("/login")
  354. invoice = await invoices_col.find_one({"nummer": nummer, "user_id": session_user["id"]})
  355. if not invoice:
  356. return HTMLResponse(status_code=404, content="Factuur niet gevonden")
  357. vervaldatum = invoice.get("vervaldatum")
  358. if vervaldatum:
  359. try:
  360. verval_date = datetime.strptime(vervaldatum, "%Y-%m-%d").date()
  361. if invoice.get("status") not in ["betaald", "geannuleerd"] and verval_date < datetime.today().date():
  362. invoice["status"] = "verlopen"
  363. except Exception:
  364. pass
  365. invoice["totaal"] = sum(item["aantal"] * item["prijs"] for item in invoice.get("items", []))
  366. prefs = await settings_col.find_one({"user_id": session_user["id"]}) or {}
  367. valuta_symbol = CURRENCY_SYMBOLS.get(prefs.get("valuta", "EUR"), "€")
  368. company = await db["company"].find_one({"user_id": session_user["id"]}) or {}
  369. facturen = await invoices_col.find({"user_id": session_user["id"]}).to_list(length=100)
  370. for factuur in facturen:
  371. factuur["totaal"] = sum(item["aantal"] * item["prijs"] for item in factuur.get("items", []))
  372. klant_id = factuur.get("klant")
  373. try:
  374. klant_id = ObjectId(klant_id)
  375. except Exception:
  376. pass
  377. klant_obj = await db["customers"].find_one({"_id": klant_id})
  378. factuur["klant_naam"] = klant_obj["naam"] if klant_obj and "naam" in klant_obj else str(factuur["klant"])
  379. return templates.TemplateResponse(
  380. "invoices/view_invoice.html",
  381. {
  382. "request": request,
  383. "company_name": company.get("bedrijfsnaam", ""),
  384. "facturen": facturen,
  385. "invoice": invoice,
  386. "valuta_symbol": valuta_symbol,
  387. }
  388. )
  389.  
  390. @router.get("/admin/invoices/{nummer}/mark-sent")
  391. async def mark_sent_get(request: Request, nummer: str):
  392. session_user = request.session.get("user")
  393. if not session_user:
  394. return RedirectResponse("/login")
  395. await invoices_col.update_one({"nummer": nummer, "user_id": session_user["id"]}, {"$set": {"status": "verzonden"}})
  396. return RedirectResponse(f"/admin/invoices", status_code=303)
  397.  
  398. @router.get("/admin/invoices/{nummer}/mark-paid")
  399. async def mark_paid_get(request: Request, nummer: str):
  400. session_user = request.session.get("user")
  401. if not session_user:
  402. return RedirectResponse("/login")
  403. await invoices_col.update_one({"nummer": nummer, "user_id": session_user["id"]}, {"$set": {"status": "betaald"}})
  404. return RedirectResponse(f"/admin/invoices", status_code=303)
  405.  
  406. @router.get("/admin/invoices/{nummer}/mark-cancelled")
  407. async def mark_cancelled_get(request: Request, nummer: str):
  408. session_user = request.session.get("user")
  409. if not session_user:
  410. return RedirectResponse("/login")
  411. await invoices_col.update_one({"nummer": nummer, "user_id": session_user["id"]}, {"$set": {"status": "geannuleerd"}})
  412. return RedirectResponse(f"/admin/invoices", status_code=303)
  413.  
  414. @router.get("/admin/invoices/{nummer}/copy")
  415. async def copy_invoice(request: Request, nummer: str):
  416. session_user = request.session.get("user")
  417. if not session_user:
  418. return RedirectResponse("/login")
  419. orig = await invoices_col.find_one({"nummer": nummer, "user_id": session_user["id"]})
  420. if not orig:
  421. return RedirectResponse("/admin/invoices")
  422. new_nummer = await get_next_invoice_number(session_user["id"])
  423. new_invoice = orig.copy()
  424. new_invoice.pop("_id", None)
  425. new_invoice["nummer"] = new_nummer
  426. await invoices_col.insert_one(new_invoice)
  427. return RedirectResponse(f"/admin/invoices/{new_nummer}/edit", status_code=303)
  428.  
  429. @router.get("/admin/invoices/{nummer}/pdf")
  430. async def download_invoice_pdf(request: Request, nummer: str, background_tasks: BackgroundTasks):
  431. session_user = request.session.get("user")
  432. if not session_user:
  433. return RedirectResponse("/login")
  434. invoice = await invoices_col.find_one({"nummer": nummer, "user_id": session_user["id"]})
  435. if not invoice:
  436. return HTMLResponse(status_code=404, content="Factuur niet gevonden")
  437. company = await db["company"].find_one({"user_id": session_user["id"]}) or {}
  438. company_info = [
  439. company.get("bedrijfsnaam", ""),
  440. company.get("adres", ""),
  441. f"{company.get('postcode', '')} {company.get('stad', '')}",
  442. f"{company.get('provincie', '')}, {company.get('land', '')}",
  443. company.get("telefoon", ""),
  444. ]
  445. logo_path = company.get("logo", "/static/uploads/logos/company_logo.png")
  446. if logo_path.startswith("/static/"):
  447. logo_path = os.path.join("/opt/invoiceatlas/frontend", logo_path.lstrip("/"))
  448. customer = await customers_col.find_one({"_id": ObjectId(invoice["klant"])}) if invoice.get("klant") else {}
  449. customer_info = [
  450. customer.get("naam", ""),
  451. customer.get("contactpersoon", ""),
  452. customer.get("email", ""),
  453. customer.get("telefoon", ""),
  454. customer.get("adres", ""),
  455. f"{customer.get('postcode', '')} {customer.get('stad', '')}",
  456. customer.get("land", ""),
  457. ]
  458. items = []
  459. for item in invoice.get("items", []):
  460. items.append({
  461. "title": item.get("naam", ""),
  462. "description": item.get("beschrijving", ""),
  463. "quantity": item.get("aantal", 1),
  464. "price": item.get("prijs", 0.0),
  465. "total": item.get("aantal", 1) * item.get("prijs", 0.0),
  466. "section": item.get("section", None)
  467. })
  468. section_titles = invoice.get("section_titles", [])
  469. subtotaal = sum(i["total"] for i in items)
  470. korting = float(invoice.get("korting", 0))
  471. btw = 0.0
  472. totaal = subtotaal - korting + btw
  473. pdf_data = {
  474. "logo_path": logo_path,
  475. "company_name": company.get("bedrijfsnaam", "Bedrijfsnaam"),
  476. "company_info": company_info,
  477. "customer_info": customer_info,
  478. "kvk_nummer": company.get("kvk_nummer", ""),
  479. "btw_nummer": company.get("btw_nummer", ""),
  480. "iban": company.get("iban", ""),
  481. "section_titles": section_titles,
  482. "factuur_nummer": invoice["nummer"],
  483. "factuur_datum": invoice["datum"],
  484. "vervaldatum": invoice["vervaldatum"],
  485. "items": items,
  486. "opmerkingen": invoice.get("opmerkingen", ""),
  487. "subtotaal": subtotaal,
  488. "kortingen": korting,
  489. "btw": btw,
  490. "totaal": totaal
  491. }
  492. pdf_filename = create_documents_pdf(pdf_data)
  493. background_tasks.add_task(os.remove, pdf_filename)
  494. return FileResponse(
  495. pdf_filename,
  496. media_type="application/pdf",
  497. headers={"Content-Disposition": "inline; filename=factuur.pdf"}
  498. )
Advertisement
Add Comment
Please, Sign In to add comment