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
|
from bson.objectid import ObjectId
|
||||||
import user as user_store
|
import user as user_store
|
||||||
import buchungen
|
import buchungen
|
||||||
|
import server_steering as steering
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = "ASDfhbsdfseiufhgildsrfrjg874368546987s6e8468f4!?FAUS/&s"
|
app.secret_key = "ASDfhbsdfseiufhgildsrfrjg874368546987s6e8468f4!?FAUS/&s"
|
||||||
@@ -2071,38 +2072,82 @@ def booking_payment():
|
|||||||
"booking_data": form_data,
|
"booking_data": form_data,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if booking_flow == "payment":
|
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')}"
|
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"),
|
"username": session.get("username"),
|
||||||
"invoice_number": booking_number,
|
"booking_number": booking_number,
|
||||||
"period": "Sofortbuchung",
|
"package": package_raw,
|
||||||
"amount_eur": invoice_amount,
|
"package_label": selected_package,
|
||||||
"status": "Zu prüfen",
|
"amount_eur": BOOKING_PRICE_MAP.get(package_raw, 0.0),
|
||||||
"due_date": date.today().isoformat(),
|
|
||||||
"pdf_path": "",
|
|
||||||
"created_at": _utc_now_iso(),
|
|
||||||
"source": "booking_payment",
|
|
||||||
"booking_flow": booking_flow,
|
"booking_flow": booking_flow,
|
||||||
"booking_package": package_raw,
|
|
||||||
"booking_package_label": selected_package,
|
|
||||||
"booking_data": form_data,
|
"booking_data": form_data,
|
||||||
"paperwork_status": "Offen",
|
|
||||||
"paperwork_required": True,
|
|
||||||
"assigned_admin_username": assigned_admin_username,
|
"assigned_admin_username": assigned_admin_username,
|
||||||
"assigned_admin_display_name": assigned_admin_display_name,
|
"assigned_admin_display_name": assigned_admin_display_name,
|
||||||
"assigned_admin_email": assigned_admin_email,
|
"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:
|
try:
|
||||||
invoice_client, invoice_col = _get_collection("invoices")
|
prov_client, prov_col = _get_collection("provision_requests")
|
||||||
invoice_col.insert_one(invoice_payload)
|
prov_col.insert_one(provision_payload)
|
||||||
finally:
|
finally:
|
||||||
if invoice_client:
|
if prov_client:
|
||||||
invoice_client.close()
|
prov_client.close()
|
||||||
|
|
||||||
except PyMongoError:
|
except PyMongoError:
|
||||||
flash("Buchungsanfrage konnte nicht gesendet werden.", "error")
|
flash("Buchungsanfrage konnte nicht gesendet werden.", "error")
|
||||||
@@ -2697,7 +2742,20 @@ def my_invoices():
|
|||||||
finally:
|
finally:
|
||||||
if client:
|
if client:
|
||||||
client.close()
|
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'])
|
@app.route('/my/instance', methods=['GET', 'POST'])
|
||||||
@@ -3010,6 +3068,7 @@ def admin_invoices():
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
action = _sanitize_text(request.form.get("action") or "", 50)
|
action = _sanitize_text(request.form.get("action") or "", 50)
|
||||||
invoice_id = _sanitize_text(request.form.get("invoice_id") or "", 64)
|
invoice_id = _sanitize_text(request.form.get("invoice_id") or "", 64)
|
||||||
|
prov_id = _sanitize_text(request.form.get("prov_id") or "", 64)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client, col = _get_collection("invoices")
|
client, col = _get_collection("invoices")
|
||||||
@@ -3047,6 +3106,118 @@ def admin_invoices():
|
|||||||
)
|
)
|
||||||
flash("Rechnung angelegt.", "success")
|
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:
|
elif action == "update" and invoice_id:
|
||||||
amount_text = _sanitize_text(request.form.get("amount_eur") or "0", 20)
|
amount_text = _sanitize_text(request.form.get("amount_eur") or "0", 20)
|
||||||
invoice_number = _sanitize_text(request.form.get("invoice_number") or "", 120)
|
invoice_number = _sanitize_text(request.form.get("invoice_number") or "", 120)
|
||||||
@@ -3099,8 +3270,21 @@ def admin_invoices():
|
|||||||
if client:
|
if client:
|
||||||
client.close()
|
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()
|
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')
|
@app.route('/datenschutz')
|
||||||
|
|||||||
@@ -62,6 +62,51 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<article class="entry"><p>Noch keine Rechnungen vorhanden.</p></article>
|
<article class="entry"><p>Noch keine Rechnungen vorhanden.</p></article>
|
||||||
{% endfor %}
|
{% 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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,40 @@
|
|||||||
</table>
|
</table>
|
||||||
</section>
|
</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>
|
<style>
|
||||||
.panel { margin-bottom: 1rem; }
|
.panel { margin-bottom: 1rem; }
|
||||||
.table-wrap { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; overflow-x: auto; }
|
.table-wrap { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; overflow-x: auto; }
|
||||||
|
|||||||
Reference in New Issue
Block a user