feat: Implement provisioning request management in admin and user invoice views

This commit is contained in:
2026-05-26 22:45:10 +02:00
parent b2cf004a78
commit 94f7d301d1
3 changed files with 286 additions and 23 deletions
+207 -23
View File
@@ -25,6 +25,7 @@ from pymongo.errors import PyMongoError
from bson.objectid import ObjectId
import user as user_store
import buchungen
import server_steering as steering
app = Flask(__name__)
app.secret_key = "ASDfhbsdfseiufhgildsrfrjg874368546987s6e8468f4!?FAUS/&s"
@@ -2071,38 +2072,82 @@ def booking_payment():
"booking_data": form_data,
}
)
if booking_flow == "payment":
# Do NOT create an invoice directly. Instead request provisioning via server_steering
booking_number = f"BOOK-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
invoice_amount = BOOKING_PRICE_MAP.get(package_raw, 0.0)
invoice_payload = {
# Build a subdomain from the school name (fallback to username) and ensure uniqueness
base = _slugify_subdomain(form_data.get("school_name") or session.get("username") or booking_number)
subdomain = f"{base}-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
subdomain = subdomain[:63]
provision_status = "not_started"
provision_port = None
provision_error = ""
provision_config_ok = False
try:
try:
port = steering.instace.new(subdomain)
except Exception as e:
port = False
provision_error = str(e)
if port and port is not False:
provision_status = "started"
try:
provision_port = int(port) if isinstance(port, (int, str)) and str(port).isdigit() else port
except Exception:
provision_port = port
# Configure module if applicable
module = None
if package_raw in {"inventarsystem", "buecherei", "terminverwaltung", "emailversand", "starter", "advanced"}:
module = package_raw
if module:
try:
edit_ok = steering.instace.edit(subdomain, module)
provision_config_ok = bool(edit_ok)
if not provision_config_ok and not provision_error:
provision_error = f"Module configuration failed for {module}"
except Exception as e:
provision_config_ok = False
provision_error = str(e)
else:
provision_status = "failed"
if not provision_error:
provision_error = "Provisioning script returned failure"
except Exception as e:
provision_status = "failed"
provision_error = str(e)
provision_payload = {
"username": session.get("username"),
"invoice_number": booking_number,
"period": "Sofortbuchung",
"amount_eur": invoice_amount,
"status": "Zu prüfen",
"due_date": date.today().isoformat(),
"pdf_path": "",
"created_at": _utc_now_iso(),
"source": "booking_payment",
"booking_number": booking_number,
"package": package_raw,
"package_label": selected_package,
"amount_eur": BOOKING_PRICE_MAP.get(package_raw, 0.0),
"booking_flow": booking_flow,
"booking_package": package_raw,
"booking_package_label": selected_package,
"booking_data": form_data,
"paperwork_status": "Offen",
"paperwork_required": True,
"assigned_admin_username": assigned_admin_username,
"assigned_admin_display_name": assigned_admin_display_name,
"assigned_admin_email": assigned_admin_email,
"consent_accepted": True,
"subdomain": subdomain,
"provision_status": provision_status,
"provision_port": provision_port or "",
"provision_config_ok": provision_config_ok,
"provision_error": provision_error,
"created_at": _utc_now_iso(),
"source": "booking_payment",
}
invoice_client = None
prov_client = None
try:
invoice_client, invoice_col = _get_collection("invoices")
invoice_col.insert_one(invoice_payload)
prov_client, prov_col = _get_collection("provision_requests")
prov_col.insert_one(provision_payload)
finally:
if invoice_client:
invoice_client.close()
if prov_client:
prov_client.close()
except PyMongoError:
flash("Buchungsanfrage konnte nicht gesendet werden.", "error")
@@ -2697,7 +2742,20 @@ def my_invoices():
finally:
if client:
client.close()
return render_template("my_invoices.html", invoices=invoices)
# Load provisioning requests for this user
provision_requests = []
try:
prov_client, prov_col = _get_collection("provision_requests")
provision_requests = list(prov_col.find({"username": session.get("username")}).sort("created_at", -1))
for item in provision_requests:
item["id"] = str(item.get("_id"))
except PyMongoError:
flash("Provisionierungsanfragen konnten nicht geladen werden.", "error")
finally:
if 'prov_client' in locals() and prov_client:
prov_client.close()
return render_template("my_invoices.html", invoices=invoices, provision_requests=provision_requests)
@app.route('/my/instance', methods=['GET', 'POST'])
@@ -3010,6 +3068,7 @@ def admin_invoices():
if request.method == 'POST':
action = _sanitize_text(request.form.get("action") or "", 50)
invoice_id = _sanitize_text(request.form.get("invoice_id") or "", 64)
prov_id = _sanitize_text(request.form.get("prov_id") or "", 64)
try:
client, col = _get_collection("invoices")
@@ -3047,6 +3106,118 @@ def admin_invoices():
)
flash("Rechnung angelegt.", "success")
elif action == "create_from_provision" and prov_id:
# Create an invoice from a provision request
prov_client = None
try:
prov_client, prov_col = _get_collection("provision_requests")
prov = prov_col.find_one({"_id": ObjectId(prov_id)})
if not prov:
flash("Provisionierungsanfrage nicht gefunden.", "error")
return redirect(url_for("admin_invoices"))
username = prov.get("username") or ""
invoice_number = f"INV-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
period = _sanitize_text(request.form.get("period") or prov.get("period") or "Sofortbuchung", 20)
due_date = _sanitize_text(request.form.get("due_date") or date.today().isoformat(), 20)
amount = float(prov.get("amount_eur") or 0.0)
col.insert_one(
{
"username": username,
"invoice_number": invoice_number,
"period": period,
"amount_eur": amount,
"status": "Zu prüfen",
"due_date": due_date,
"pdf_path": "",
"created_at": _utc_now_iso(),
"source": "booking_payment",
"booking_flow": prov.get("booking_flow"),
"booking_package": prov.get("package"),
"booking_package_label": prov.get("package_label"),
"booking_data": prov.get("booking_data"),
"assigned_admin_username": prov.get("assigned_admin_username"),
"assigned_admin_display_name": prov.get("assigned_admin_display_name"),
"assigned_admin_email": prov.get("assigned_admin_email"),
}
)
prov_col.update_one({"_id": prov.get("_id")}, {"$set": {"provision_status": "invoice_created", "updated_at": _utc_now_iso()}})
flash("Rechnung aus Provisionierungsanfrage erstellt.", "success")
except Exception:
flash("Fehler beim Erstellen der Rechnung aus Provisionierungsanfrage.", "error")
finally:
if prov_client:
prov_client.close()
elif action == "update_provision" and prov_id:
# Update provision request status (accept/reject/complete)
new_status = _sanitize_text(request.form.get("prov_status") or "", 40)
prov_client = None
try:
prov_client, prov_col = _get_collection("provision_requests")
prov_col.update_one({"_id": ObjectId(prov_id)}, {"$set": {"provision_status": new_status, "updated_at": _utc_now_iso()}})
flash("Provisionierungsanfrage aktualisiert.", "success")
except Exception:
flash("Provisionierungsanfrage konnte nicht aktualisiert werden.", "error")
finally:
if prov_client:
prov_client.close()
elif action in {"run_provision", "retry_provision", "configure_provision"} and prov_id:
# Admin-triggered provisioning actions: start/retry/configure
prov_client = None
try:
prov_client, prov_col = _get_collection("provision_requests")
prov = prov_col.find_one({"_id": ObjectId(prov_id)})
if not prov:
flash("Provisionierungsanfrage nicht gefunden.", "error")
return redirect(url_for("admin_invoices"))
subdomain = prov.get("subdomain") or _slugify_subdomain(prov.get("booking_data", {}).get("school_name") or prov.get("username") or prov.get("booking_number") or "prov")
provision_status = prov.get("provision_status") or "not_started"
provision_port = prov.get("provision_port") or None
provision_error = ""
provision_config_ok = prov.get("provision_config_ok") or False
if action in {"run_provision", "retry_provision"}:
try:
port = steering.instace.new(subdomain)
except Exception as e:
port = False
provision_error = str(e)
if port and port is not False:
provision_status = "started"
try:
provision_port = int(port) if isinstance(port, (int, str)) and str(port).isdigit() else port
except Exception:
provision_port = port
else:
provision_status = "failed"
if not provision_error:
provision_error = "Provisioning script returned failure"
if action == "configure_provision" or (action in {"run_provision", "retry_provision"} and prov.get("package")):
module = prov.get("package")
if module in {"inventarsystem", "buecherei", "terminverwaltung", "emailversand", "starter", "advanced"}:
try:
edit_ok = steering.instace.edit(subdomain, module)
provision_config_ok = bool(edit_ok)
if not provision_config_ok and not provision_error:
provision_error = f"Module configuration failed for {module}"
except Exception as e:
provision_config_ok = False
provision_error = str(e)
prov_col.update_one({"_id": prov.get("_id")}, {"$set": {"provision_status": provision_status, "provision_port": provision_port or "", "provision_config_ok": provision_config_ok, "provision_error": provision_error, "updated_at": _utc_now_iso()}})
flash("Provisionierungsaktion ausgeführt.", "success")
except Exception as e:
flash(f"Provisionierungsaktion fehlgeschlagen: {str(e)}", "error")
finally:
if prov_client:
prov_client.close()
elif action == "update" and invoice_id:
amount_text = _sanitize_text(request.form.get("amount_eur") or "0", 20)
invoice_number = _sanitize_text(request.form.get("invoice_number") or "", 120)
@@ -3099,8 +3270,21 @@ def admin_invoices():
if client:
client.close()
# Load provisioning requests for admin review
provision_requests = []
try:
prov_client, prov_col = _get_collection("provision_requests")
provision_requests = list(prov_col.find().sort("created_at", -1))
for item in provision_requests:
item["id"] = str(item.get("_id"))
except PyMongoError:
flash("Provisionierungsanfragen konnten nicht geladen werden.", "error")
finally:
if 'prov_client' in locals() and prov_client:
prov_client.close()
users = _list_users_for_admin()
return render_template("admin_invoices.html", invoices=invoices, users=users)
return render_template("admin_invoices.html", invoices=invoices, users=users, provision_requests=provision_requests)
@app.route('/datenschutz')
+45
View File
@@ -62,6 +62,51 @@
{% else %}
<article class="entry"><p>Noch keine Rechnungen vorhanden.</p></article>
{% endfor %}
{% if provision_requests %}
<h2>Provisionierungsanfragen</h2>
{% for prov in provision_requests %}
<article class="entry">
<h3>{{ prov.booking_number or prov.booking_number }}</h3>
<p><strong>Nutzer:</strong> {{ prov.username }} | <strong>Subdomain:</strong> {{ prov.subdomain }}</p>
<p><strong>Paket:</strong> {{ prov.package_label or prov.package }} | <strong>Betrag:</strong> {{ '%.2f'|format(prov.amount_eur or 0) }} EUR</p>
<p><strong>Status:</strong> {{ prov.provision_status or 'not_started' }}{% if prov.provision_error %} | <strong>Fehler:</strong> {{ prov.provision_error }}{% endif %}</p>
<p><strong>Kontaktdaten:</strong> {{ prov.booking_data.contact_person }} | {{ prov.booking_data.contact_email }} | {{ prov.booking_data.billing_city }}</p>
<form method="post" style="display:inline-block; margin-right:0.6rem;">
<input type="hidden" name="action" value="create_from_provision">
<input type="hidden" name="prov_id" value="{{ prov.id }}">
<input type="text" name="period" placeholder="Zeitraum (optional)">
<input type="text" name="due_date" placeholder="Fälligkeit (YYYY-MM-DD)">
<button type="submit">Rechnung erzeugen</button>
</form>
<form method="post" style="display:inline-block; margin-right:0.6rem;">
<input type="hidden" name="action" value="update_provision">
<input type="hidden" name="prov_id" value="{{ prov.id }}">
<select name="prov_status">
<option value="accepted">accepted</option>
<option value="rejected">rejected</option>
<option value="completed">completed</option>
</select>
<button type="submit">Status setzen</button>
</form>
<form method="post" style="display:inline-block; margin-right:0.6rem;">
<input type="hidden" name="action" value="run_provision">
<input type="hidden" name="prov_id" value="{{ prov.id }}">
<button type="submit">Provision starten</button>
</form>
<form method="post" style="display:inline-block; margin-right:0.6rem;">
<input type="hidden" name="action" value="retry_provision">
<input type="hidden" name="prov_id" value="{{ prov.id }}">
<button type="submit">Provision wiederholen</button>
</form>
<form method="post" style="display:inline-block; margin-right:0.6rem;">
<input type="hidden" name="action" value="configure_provision">
<input type="hidden" name="prov_id" value="{{ prov.id }}">
<button type="submit">Konfiguration ausführen</button>
</form>
</article>
{% endfor %}
{% endif %}
</div>
</section>
+34
View File
@@ -45,6 +45,40 @@
</table>
</section>
<section class="panel">
<h2>Provisionierungsanfragen</h2>
<p>Hier siehst du den Status deiner angeforderten Instanzen/ Buchungen, die vom Admin geprüft werden.</p>
</section>
<section class="table-wrap">
<table>
<thead>
<tr>
<th>Anfrage</th>
<th>Paket</th>
<th>Subdomain</th>
<th>Betrag</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for prov in provision_requests %}
<tr>
<td>{{ prov.booking_number or prov.booking_number }}</td>
<td>{{ prov.package_label or prov.package }}</td>
<td>{{ prov.subdomain }}</td>
<td>{{ '%.2f'|format(prov.amount_eur or 0) }} EUR</td>
<td>{{ prov.provision_status or 'not_started' }}</td>
</tr>
{% else %}
<tr>
<td colspan="5">Keine Provisionierungsanfragen vorhanden.</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<style>
.panel { margin-bottom: 1rem; }
.table-wrap { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; overflow-x: auto; }