changes to the Rechnungs Processing

This commit is contained in:
2026-08-29 08:55:03 +02:00
parent d08117e1a3
commit dd35b37fb5
2 changed files with 63 additions and 48 deletions
+18 -14
View File
@@ -2818,18 +2818,18 @@ def view_hauptvertrag(invoice_id):
flash("Kein Hauptvertrag für diese Rechnung vorhanden.", "error") flash("Kein Hauptvertrag für diese Rechnung vorhanden.", "error")
return redirect(url_for("my_invoices")) return redirect(url_for("my_invoices"))
# Sicherheitsprüfung # Sicherheitsprüfung: Gehört die Rechnung dem User oder ist es ein Admin?
current_user = session.get("username") current_user = session.get("username")
is_admin = session.get("is_admin") is_admin = session.get("is_admin", False)
if invoice.get("username") != current_user and not is_admin: if invoice.get("username") != current_user and not is_admin:
flash("Keine Berechtigung zum Anschauen dieses Hauptvertrags.", "error") flash("Keine Berechtigung zum Anschauen dieses Hauptvertrags.", "error")
return redirect(url_for("my_invoices")) return redirect(url_for("my_invoices"))
file_data = invoice["hauptvertrag_data"] file_data = invoice["hauptvertrag_data"]
mimetype = invoice.get("hauptvertrag_mimetype", "application/pdf") mimetype = invoice.get("hauptvertrag_mimetype", "application/pdf")
filename = invoice.get("hauptvertrag_filename", "hauptvertrag") filename = invoice.get("hauptvertrag_filename", "Hauptvertrag.pdf")
# Fallback für unterschiedliche Flask-Versionen (< 2.0 vs >= 2.0) # Rückgabe als Stream an den Browser (inline anzeigen, nicht erzwingen als Download)
try: try:
return send_file( return send_file(
io.BytesIO(file_data), io.BytesIO(file_data),
@@ -2838,6 +2838,7 @@ def view_hauptvertrag(invoice_id):
download_name=filename download_name=filename
) )
except TypeError: except TypeError:
# Fallback für ältere Flask-Versionen (< 2.0)
return send_file( return send_file(
io.BytesIO(file_data), io.BytesIO(file_data),
mimetype=mimetype, mimetype=mimetype,
@@ -2846,18 +2847,14 @@ def view_hauptvertrag(invoice_id):
) )
except Exception as e: except Exception as e:
# WICHTIG: Das schreibt den tatsächlichen Fehler in deine Docker-Logs!
import traceback
print(f"KRITISCHER FEHLER in view_hauptvertrag: {e}", flush=True) print(f"KRITISCHER FEHLER in view_hauptvertrag: {e}", flush=True)
traceback.print_exc() traceback.print_exc()
flash("Fehler beim Abrufen der Datei aus der Datenbank.", "error") flash("Fehler beim Abrufen der Datei aus der Datenbank.", "error")
return redirect(url_for("my_invoices")) return redirect(url_for("my_invoices"))
finally: finally:
if client: if client:
client.close() client.close()
@app.route('/my/invoices', methods=['GET', 'POST']) @app.route('/my/invoices', methods=['GET', 'POST'])
@login_required @login_required
def my_invoices(): def my_invoices():
@@ -2902,7 +2899,13 @@ def my_invoices():
invoices = [] invoices = []
try: try:
client, col = _get_collection("invoices") client, col = _get_collection("invoices")
raw_invoices = list(col.find({"username": session.get("username")}).sort("created_at", -1))
# WICHTIG: "hauptvertrag_data" und "rechnung_data" (die schweren Binärdaten) bei der Abfrage ausschließen (0),
# um den Arbeitsspeicher (RAM) nicht zu überlasten!
raw_invoices = list(col.find(
{"username": session.get("username")},
{"hauptvertrag_data": 0, "rechnung_data": 0}
).sort("created_at", -1))
for inv in raw_invoices: for inv in raw_invoices:
inv["id"] = str(inv["_id"]) inv["id"] = str(inv["_id"])
@@ -2913,11 +2916,13 @@ def my_invoices():
except ValueError: except ValueError:
amt = 0.0 amt = 0.0
inv["amount_eur"] = amt inv["amount_eur"] = amt
# Flag setzen, ob Vertrag in MongoDB liegt (ohne die schweren Binärdaten ins Template zu laden)
inv["has_hauptvertrag"] = "hauptvertrag_data" in inv # Flags setzen, ob Verträge/Rechnungen in MongoDB existieren (anhand des Dateinamens)
inv["has_hauptvertrag"] = bool(inv.get("hauptvertrag_filename"))
inv["has_rechnung"] = bool(inv.get("rechnung_filename"))
invoices.append(inv) invoices.append(inv)
except PyMongoError: except Exception as e:
flash("Rechnungen konnten nicht geladen werden.", "error") flash("Rechnungen konnten nicht geladen werden.", "error")
finally: finally:
if client: if client:
@@ -2936,7 +2941,7 @@ def my_invoices():
except ValueError: except ValueError:
amt = 0.0 amt = 0.0
item["amount_eur"] = amt item["amount_eur"] = amt
except PyMongoError: except Exception:
flash("Instanz-Anfragen konnten nicht geladen werden.", "error") flash("Instanz-Anfragen konnten nicht geladen werden.", "error")
finally: finally:
if 'req_client' in locals() and req_client: if 'req_client' in locals() and req_client:
@@ -2944,7 +2949,6 @@ def my_invoices():
return render_template("my_invoices.html", invoices=invoices, instance_requests=instance_requests) return render_template("my_invoices.html", invoices=invoices, instance_requests=instance_requests)
@app.route('/my/instance', methods=['GET', 'POST']) @app.route('/my/instance', methods=['GET', 'POST'])
@login_required @login_required
def my_instance_management(): def my_instance_management():
+45 -34
View File
@@ -9,6 +9,7 @@ from fpdf import FPDF
from flask import current_app from flask import current_app
from pymongo import MongoClient from pymongo import MongoClient
import threading import threading
from bson import Binary, ObjectId
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
@@ -516,11 +517,6 @@ def send_accreditation_email(
invoice_to_attach = invoice_path invoice_to_attach = invoice_path
db_pdf_path = relative_invoice_path db_pdf_path = relative_invoice_path
if current_app:
static_dir = current_app.static_folder
else:
static_dir = os.path.join(os.getcwd(), "static")
def get_valid_pdf_path(filename): def get_valid_pdf_path(filename):
possible_paths = [ possible_paths = [
os.path.join(static_dir, "downloads", filename), os.path.join(static_dir, "downloads", filename),
@@ -536,7 +532,7 @@ def send_accreditation_email(
agb_pdf_path = get_valid_pdf_path("AGB Invario.pdf") agb_pdf_path = get_valid_pdf_path("AGB Invario.pdf")
avv_pdf_path = get_valid_pdf_path("AVV Invario.pdf") avv_pdf_path = get_valid_pdf_path("AVV Invario.pdf")
# 3. E-Mail Inhalte # 4. E-Mail Inhalte
subject = "Ihre Zugangsdaten und Unterlagen für Invario" subject = "Ihre Zugangsdaten und Unterlagen für Invario"
text_note = ( text_note = (
@@ -571,18 +567,14 @@ def send_accreditation_email(
</div> </div>
""" """
# 4. Vorhandene Anhänge sammeln # 5. Vorhandene Anhänge sammeln
attachments = [] attachments = []
if os.path.exists(contract_pdf_path): if os.path.exists(contract_pdf_path):
attachments.append(contract_pdf_path) attachments.append(contract_pdf_path)
if invoice_to_attach and os.path.exists(invoice_to_attach): if invoice_to_attach and os.path.exists(invoice_to_attach):
attachments.append(invoice_to_attach) attachments.append(invoice_to_attach)
if agb_pdf_path: if agb_pdf_path:
attachments.append(agb_pdf_path) attachments.append(agb_pdf_path)
if avv_pdf_path: if avv_pdf_path:
attachments.append(avv_pdf_path) attachments.append(avv_pdf_path)
@@ -615,7 +607,7 @@ def send_accreditation_email(
def close(self): def close(self):
return None return None
def _get_mongo_client() -> MongoClient: def _get_mongo_client():
global _MONGO_CLIENT global _MONGO_CLIENT
if _MONGO_CLIENT is not None: if _MONGO_CLIENT is not None:
return _MONGO_CLIENT return _MONGO_CLIENT
@@ -645,33 +637,50 @@ def send_accreditation_email(
def _utc_now_iso(): def _utc_now_iso():
return datetime.now(timezone.utc).isoformat() return datetime.now(timezone.utc).isoformat()
# Berechne das Fälligkeitsdatum (30 Tage ab heute) # Berechne das Fälligkeitsdatum (14 Tage ab heute)
computed_due_date = (datetime.now(timezone.utc) + timedelta(days=14)).strftime("%d %B %Y") computed_due_date = (datetime.now(timezone.utc) + timedelta(days=14)).strftime("%d %B %Y")
# Collections abrufen # Collections abrufen
req_client, req_col = _get_collection("instance_requests") req_client, req_col = _get_collection("instance_requests")
inv_client, col = _get_collection("invoices") # Collection für Rechnungen initialisieren inv_client, col = _get_collection("invoices")
# 1. Die neue Rechnung in die Datenbank (invoices-Collection) schreiben # 6. Die generierte PDF-Datei als Binär-Daten für MongoDB einlesen
# Wir nutzen hier die übergebenen Funktionsparameter anstatt des undefinierten "prov" rechnung_bytes = None
col.insert_one( if invoice_to_attach and os.path.exists(invoice_to_attach):
{ try:
"username": user_id, with open(invoice_to_attach, "rb") as f:
"school_name": school_name, rechnung_bytes = f.read()
"address": address, except Exception as e:
"invoice_number": current_invoice_number, # Im Falle eines Dateilesefehlers überspringen wir das Speichern des Binaries
"period": date, pass
"amount_eur": price,
"status": "Zu prüfen",
"due_date": computed_due_date,
"pdf_path": db_pdf_path,
"created_at": _utc_now_iso(),
"source": "booking_payment",
"software_name": software_name,
"domain": domain
}
)
# 7. Die neue Rechnung in die Datenbank (invoices-Collection) schreiben
invoice_document = {
"username": user_id,
"school_name": school_name,
"address": address,
"invoice_number": current_invoice_number,
"period": date,
"amount_eur": price,
"status": "Zu prüfen",
"due_date": computed_due_date,
"pdf_path": db_pdf_path,
"created_at": _utc_now_iso(),
"source": "booking_payment",
"software_name": software_name,
"domain": domain
}
# Binärdaten der Rechnung hinzufügen, falls erfolgreich eingelesen
if rechnung_bytes:
invoice_document["rechnung_data"] = Binary(rechnung_bytes)
invoice_document["rechnung_filename"] = invoice_filename
invoice_document["rechnung_mimetype"] = "application/pdf"
# In MongoDB einfügen
col.insert_one(invoice_document)
# Anfrage-Status aktualisieren
req_col.update_one( req_col.update_one(
{"username": user_id}, {"username": user_id},
{"$set": { {"$set": {
@@ -680,11 +689,13 @@ def send_accreditation_email(
}} }}
) )
# 5. E-Mail versenden & NUR temporäre Dateien löschen # 8. E-Mail versenden & NUR temporäre Dateien löschen
try: try:
success = send(recipient, subject, text_body=text_note, html_body=html_note, attachments=attachments) success = send(recipient, subject, text_body=text_note, html_body=html_note, attachments=attachments)
return success return success
finally: finally:
# Nur der generierte Hauptvertrag liegt in 'tempfile.gettempdir()' und wird gelöscht.
# Die Rechnung im '/static/invoices/' Ordner bleibt unangetastet (sowie als Backup auf der Festplatte).
if os.path.exists(contract_pdf_path): if os.path.exists(contract_pdf_path):
try: try:
os.remove(contract_pdf_path) os.remove(contract_pdf_path)