feat: Implement provisioning request management in admin and user invoice views
This commit is contained in:
+207
-23
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user