implementation of the upload
This commit is contained in:
+92
-12
@@ -23,6 +23,7 @@ from markupsafe import escape
|
|||||||
from pymongo import MongoClient
|
from pymongo import MongoClient
|
||||||
from pymongo.errors import PyMongoError
|
from pymongo.errors import PyMongoError
|
||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
|
from bson.binary import Binary
|
||||||
import user as user_store
|
import user as user_store
|
||||||
import buchungen
|
import buchungen
|
||||||
import server_steering as steering
|
import server_steering as steering
|
||||||
@@ -119,6 +120,7 @@ LEGAL_DOWNLOAD_FILES = (
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
ALLOWED_CONTRACT_EXTENSIONS = {'pdf', 'png', 'jpg', 'jpeg', 'webp'}
|
||||||
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
|
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
|
||||||
MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website")
|
MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website")
|
||||||
INSTANCE_REPO_URL = os.environ.get("INSTANCE_REPO_URL", "https://git.invario-software.eu/Invario/Inventarsystem")
|
INSTANCE_REPO_URL = os.environ.get("INSTANCE_REPO_URL", "https://git.invario-software.eu/Invario/Inventarsystem")
|
||||||
@@ -184,6 +186,10 @@ def _is_allowed_image_filename(filename: str) -> bool:
|
|||||||
return lowered.endswith(".jpg") or lowered.endswith(".jpeg") or lowered.endswith(".png") or lowered.endswith(".webp")
|
return lowered.endswith(".jpg") or lowered.endswith(".jpeg") or lowered.endswith(".png") or lowered.endswith(".webp")
|
||||||
|
|
||||||
|
|
||||||
|
def _allowed_contract_file(filename):
|
||||||
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_CONTRACT_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
def _is_allowed_tutorial_video_filename(filename: str) -> bool:
|
def _is_allowed_tutorial_video_filename(filename: str) -> bool:
|
||||||
if not filename:
|
if not filename:
|
||||||
return False
|
return False
|
||||||
@@ -2732,25 +2738,102 @@ def blog_post(post_id):
|
|||||||
return render_template("blog_post.html", post=post)
|
return render_template("blog_post.html", post=post)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/my/invoices')
|
@app.route('/invoices/<invoice_id>/hauptvertrag')
|
||||||
@login_required
|
@login_required
|
||||||
def my_invoices():
|
def view_hauptvertrag(invoice_id):
|
||||||
invoices = []
|
|
||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
client, col = _get_collection("invoices")
|
client, col = _get_collection("invoices")
|
||||||
invoices = list(col.find({"username": session.get("username")}, {"_id": 0}).sort("created_at", -1))
|
invoice = col.find_one({"_id": ObjectId(invoice_id)})
|
||||||
|
|
||||||
# Beträge in Float umwandeln, da das Jinja-Template echte Zahlen erwartet
|
if not invoice or "hauptvertrag_data" not in invoice:
|
||||||
for inv in invoices:
|
flash("Kein Hauptvertrag für diese Rechnung vorhanden.", "error")
|
||||||
|
return redirect(url_for("my_invoices"))
|
||||||
|
|
||||||
|
# Sicherheitsprüfung: Darf der Nutzer die Datei sehen? (Nutzer selbst oder Admin)
|
||||||
|
current_user = session.get("username")
|
||||||
|
is_admin = session.get("is_admin")
|
||||||
|
if invoice.get("username") != current_user and not is_admin:
|
||||||
|
flash("Keine Berechtigung zum Anschauen dieses Hauptvertrags.", "error")
|
||||||
|
return redirect(url_for("my_invoices"))
|
||||||
|
|
||||||
|
# Datei-Streams aus den Binärdaten in MongoDB erzeugen
|
||||||
|
file_data = invoice["hauptvertrag_data"]
|
||||||
|
mimetype = invoice.get("hauptvertrag_mimetype", "application/pdf")
|
||||||
|
filename = invoice.get("hauptvertrag_filename", "hauptvertrag")
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
io.BytesIO(file_data),
|
||||||
|
mimetype=mimetype,
|
||||||
|
as_attachment=False,
|
||||||
|
download_name=filename
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
flash("Fehler beim Abrufen der Datei aus der Datenbank.", "error")
|
||||||
|
return redirect(url_for("my_invoices"))
|
||||||
|
finally:
|
||||||
|
if client:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/my/invoices', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def my_invoices():
|
||||||
|
client = None
|
||||||
|
|
||||||
|
# POST-Request: Datei in MongoDB speichern
|
||||||
|
if request.method == 'POST':
|
||||||
|
action = request.form.get("action")
|
||||||
|
invoice_id = request.form.get("invoice_id")
|
||||||
|
uploaded_file = request.files.get("hauptvertrag_file")
|
||||||
|
|
||||||
|
if action == "upload_hauptvertrag" and invoice_id and uploaded_file:
|
||||||
|
if _allowed_contract_file(uploaded_file.filename):
|
||||||
|
file_bytes = uploaded_file.read()
|
||||||
|
filename = uploaded_file.filename
|
||||||
|
mimetype = uploaded_file.mimetype or "application/octet-stream"
|
||||||
|
|
||||||
|
try:
|
||||||
|
client, col = _get_collection("invoices")
|
||||||
|
# Datei direkt als Binary in das Invoice-Dokument schreiben
|
||||||
|
col.update_one(
|
||||||
|
{"_id": ObjectId(invoice_id), "username": session.get("username")},
|
||||||
|
{"$set": {
|
||||||
|
"hauptvertrag_data": Binary(file_bytes),
|
||||||
|
"hauptvertrag_filename": filename,
|
||||||
|
"hauptvertrag_mimetype": mimetype,
|
||||||
|
"paperwork_status": "Hauptvertrag hochgeladen",
|
||||||
|
"updated_at": _utc_now_iso()
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
flash("Hauptvertrag erfolgreich in der Datenbank gespeichert.", "success")
|
||||||
|
except Exception:
|
||||||
|
flash("Fehler beim Speichern in MongoDB.", "error")
|
||||||
|
finally:
|
||||||
|
if client:
|
||||||
|
client.close()
|
||||||
|
else:
|
||||||
|
flash("Ungültiges Format. Erlaubt sind PDF, JPG, PNG und WEBP.", "error")
|
||||||
|
return redirect(url_for("my_invoices"))
|
||||||
|
|
||||||
|
# GET-Request: Rechnungen laden
|
||||||
|
invoices = []
|
||||||
|
try:
|
||||||
|
client, col = _get_collection("invoices")
|
||||||
|
raw_invoices = list(col.find({"username": session.get("username")}).sort("created_at", -1))
|
||||||
|
|
||||||
|
for inv in raw_invoices:
|
||||||
|
inv["id"] = str(inv["_id"])
|
||||||
amt = inv.get("amount_eur", 0)
|
amt = inv.get("amount_eur", 0)
|
||||||
if isinstance(amt, str):
|
if isinstance(amt, str):
|
||||||
try:
|
try:
|
||||||
# Entfernt Tausenderpunkte und wandelt das deutsche Komma in einen Punkt um
|
|
||||||
amt = float(amt.replace('.', '').replace(',', '.'))
|
amt = float(amt.replace('.', '').replace(',', '.'))
|
||||||
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
|
||||||
|
invoices.append(inv)
|
||||||
|
|
||||||
except PyMongoError:
|
except PyMongoError:
|
||||||
flash("Rechnungen konnten nicht geladen werden.", "error")
|
flash("Rechnungen konnten nicht geladen werden.", "error")
|
||||||
@@ -2758,15 +2841,12 @@ def my_invoices():
|
|||||||
if client:
|
if client:
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
# Load instance requests for this user
|
|
||||||
instance_requests = []
|
instance_requests = []
|
||||||
try:
|
try:
|
||||||
req_client, req_col = _get_collection("instance_requests")
|
req_client, req_col = _get_collection("instance_requests")
|
||||||
instance_requests = list(req_col.find({"username": session.get("username")}).sort("created_at", -1))
|
instance_requests = list(req_col.find({"username": session.get("username")}).sort("created_at", -1))
|
||||||
for item in instance_requests:
|
for item in instance_requests:
|
||||||
item["id"] = str(item.get("_id"))
|
item["id"] = str(item.get("_id"))
|
||||||
|
|
||||||
# Auch hier sicherstellen, dass der Betrag ein Float ist
|
|
||||||
amt = item.get("amount_eur", 0)
|
amt = item.get("amount_eur", 0)
|
||||||
if isinstance(amt, str):
|
if isinstance(amt, str):
|
||||||
try:
|
try:
|
||||||
@@ -2774,15 +2854,15 @@ def my_invoices():
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
amt = 0.0
|
amt = 0.0
|
||||||
item["amount_eur"] = amt
|
item["amount_eur"] = amt
|
||||||
|
|
||||||
except PyMongoError:
|
except PyMongoError:
|
||||||
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:
|
||||||
req_client.close()
|
req_client.close()
|
||||||
|
|
||||||
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():
|
||||||
|
|||||||
@@ -44,11 +44,23 @@
|
|||||||
<p><strong>Unterlagen:</strong> {{ item.paperwork_status or 'Offen' }}{% if item.consent_accepted %} | Einwilligung bestätigt{% endif %}</p>
|
<p><strong>Unterlagen:</strong> {{ item.paperwork_status or 'Offen' }}{% if item.consent_accepted %} | Einwilligung bestätigt{% endif %}</p>
|
||||||
<p><strong>Kontaktdaten:</strong> {{ item.booking_data.get('contact_person', '-') }} | {{ item.booking_data.get('contact_email', '-') }} | {{ item.booking_data.get('billing_city', '-') }}</p>
|
<p><strong>Kontaktdaten:</strong> {{ item.booking_data.get('contact_person', '-') }} | {{ item.booking_data.get('contact_email', '-') }} | {{ item.booking_data.get('billing_city', '-') }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div style="margin: 0.5rem 0; padding: 0.5rem; background: #f4f8fa; border-radius: 8px;">
|
||||||
{% if item.pdf_path %}
|
{% if item.pdf_path %}
|
||||||
<p><strong>PDF:</strong> <a href="{{ url_for('static', filename=item.pdf_path) }}" target="_blank" rel="noopener">Ansehen</a>
|
<p style="margin:0.2rem 0;"><strong>Rechnungs-PDF:</strong>
|
||||||
<button class="print-btn" type="button" onclick="printPdf('{{ url_for('static', filename=item.pdf_path) }}')">PDF drucken</button>
|
<a href="{{ url_for('static', filename=item.pdf_path) }}" target="_blank" rel="noopener">Ansehen</a>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if item.hauptvertrag_data is defined or item.get('hauptvertrag_filename') %}
|
||||||
|
<p style="margin:0.2rem 0;"><strong>Hauptvertrag (aus MongoDB):</strong>
|
||||||
|
<a href="{{ url_for('view_hauptvertrag', invoice_id=item.id) }}" target="_blank" rel="noopener">
|
||||||
|
{{ item.hauptvertrag_filename or 'Hauptvertrag öffnen' }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p style="margin:0.2rem 0; color: #777;"><strong>Hauptvertrag:</strong> Noch nicht in MongoDB hochgeladen</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<form method="post" class="inline-form" enctype="multipart/form-data">
|
<form method="post" class="inline-form" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="action" value="update">
|
<input type="hidden" name="action" value="update">
|
||||||
<input type="hidden" name="invoice_id" value="{{ item.id }}">
|
<input type="hidden" name="invoice_id" value="{{ item.id }}">
|
||||||
@@ -82,6 +94,7 @@
|
|||||||
<form method="post" style="display:inline-block; margin-right:0.6rem;">
|
<form method="post" style="display:inline-block; margin-right:0.6rem;">
|
||||||
<input type="hidden" name="action" value="create_from_provision">
|
<input type="hidden" name="action" value="create_from_provision">
|
||||||
<input type="hidden" name="prov_id" value="{{ prov.id }}">
|
<input type="hidden" name="prov_id" value="{{ prov.id }}">
|
||||||
|
<input type="file" name="contract_file" accept=".pdf,.png,.jpg,.jpeg,.webp">
|
||||||
<input type="text" name="period" placeholder="Zeitraum (optional)">
|
<input type="text" name="period" placeholder="Zeitraum (optional)">
|
||||||
<input type="text" name="due_date" placeholder="Fälligkeit (YYYY-MM-DD)">
|
<input type="text" name="due_date" placeholder="Fälligkeit (YYYY-MM-DD)">
|
||||||
<button type="submit">Rechnung erzeugen</button>
|
<button type="submit">Rechnung erzeugen</button>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<section class="panel" style="display:flex; align-items:center; justify-content:space-between; gap:1rem;">
|
<section class="panel" style="display:flex; align-items:center; justify-content:space-between; gap:1rem;">
|
||||||
<div>
|
<div>
|
||||||
<h1>Rechnungseinsicht</h1>
|
<h1>Rechnungseinsicht</h1>
|
||||||
<p style="margin:0">Alle Rechnungen im Überblick. Der Admin prüft neue Rechnungen zuerst; der Status zeigt dir, ob die Rechnung noch geprüft wird oder bereits bearbeitet wurde.</p>
|
<p style="margin:0">Alle Rechnungen im Überblick.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="no-print">
|
<div class="no-print">
|
||||||
<button class="print-btn" type="button" onclick="printDocument()">Seite drucken</button>
|
<button class="print-btn" type="button" onclick="printDocument()">Seite drucken</button>
|
||||||
@@ -22,7 +22,8 @@
|
|||||||
<th>Betrag</th>
|
<th>Betrag</th>
|
||||||
<th>Fälligkeit</th>
|
<th>Fälligkeit</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>PDF</th>
|
<th>Rechnung PDF</th>
|
||||||
|
<th>Hauptvertrag</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -36,26 +37,29 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if invoice.pdf_path %}
|
{% if invoice.pdf_path %}
|
||||||
<a href="{{ url_for('static', filename=invoice.pdf_path) }}" target="_blank" rel="noopener">PDF ansehen</a>
|
<a href="{{ url_for('static', filename=invoice.pdf_path) }}" target="_blank" rel="noopener">PDF ansehen</a>
|
||||||
<button class="print-btn" type="button" onclick="printPdf('{{ url_for('static', filename=invoice.pdf_path) }}')">PDF drucken</button>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
-
|
-
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if invoice.has_hauptvertrag %}
|
||||||
|
<a href="{{ url_for('view_hauptvertrag', invoice_id=invoice.id) }}" target="_blank" rel="noopener">Hauptvertrag ansehen</a>
|
||||||
|
{% else %}
|
||||||
|
<form method="post" enctype="multipart/form-data" style="display:flex; gap:0.3rem; align-items:center;">
|
||||||
|
<input type="hidden" name="action" value="upload_hauptvertrag">
|
||||||
|
<input type="hidden" name="invoice_id" value="{{ invoice.id }}">
|
||||||
|
<input type="file" name="hauptvertrag_file" accept="image/*,application/pdf" required style="font-size:0.8rem;">
|
||||||
|
<button type="submit" style="padding: 0.2rem 0.6rem; font-size:0.8rem;">Hochladen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6">Noch keine Rechnungen vorhanden.</td>
|
<td colspan="7">Noch keine Rechnungen vorhanden.</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
{% endblock %}
|
||||||
<style>
|
|
||||||
.panel { margin-bottom: 1rem; }
|
|
||||||
.table-wrap { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; overflow-x: auto; }
|
|
||||||
table { width: 100%; border-collapse: collapse; }
|
|
||||||
th, td { text-align: left; padding: 0.7rem; border-bottom: 1px solid #e8eef2; }
|
|
||||||
a { color: #0a4c74; font-weight: 700; }
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
Reference in New Issue
Block a user