diff --git a/backend/main.py b/backend/main.py
index 853e8a4..8118b87 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -408,7 +408,7 @@ async def serve_media(path: str, request: _Request):
raise _HE(404, "Nicht gefunden.")
return _media_response(filepath)
-APP_VER = "967" # muss mit APP_VER in app.js übereinstimmen
+APP_VER = "968" # muss mit APP_VER in app.js übereinstimmen
@app.get("/.well-known/assetlinks.json")
async def assetlinks():
diff --git a/backend/routes/invoices.py b/backend/routes/invoices.py
index e330a12..de23157 100644
--- a/backend/routes/invoices.py
+++ b/backend/routes/invoices.py
@@ -444,6 +444,58 @@ def get_invoice(invoice_id: int, admin=Depends(require_admin)):
return result
+@router.patch("/{invoice_id}")
+def update_invoice(invoice_id: int, data: InvoiceCreate, admin=Depends(require_admin)):
+ with db() as conn:
+ row = conn.execute("SELECT * FROM invoices WHERE id=?", (invoice_id,)).fetchone()
+ if not row:
+ raise HTTPException(404, "Rechnung nicht gefunden.")
+ if row["status"] != "draft":
+ raise HTTPException(400, "Nur Entwürfe können bearbeitet werden.")
+ if not data.items:
+ raise HTTPException(400, "Mindestens eine Position erforderlich.")
+
+ KLEINUNTERNEHMER = os.getenv("KLEINUNTERNEHMER", "true").lower() == "true"
+ TAX_RATE = 0.0 if KLEINUNTERNEHMER else float(os.getenv("RECHNUNG_MWST", "19"))
+
+ amount_net = round(sum(i.quantity * i.unit_price for i in data.items), 2)
+ discount_pct = data.discount_pct or 0.0
+ discount_amount = round(amount_net * discount_pct / 100, 2)
+ amount_after_discount = round(amount_net - discount_amount, 2)
+ tax_amount = round(amount_after_discount * TAX_RATE / 100, 2)
+ amount_gross = round(amount_after_discount + tax_amount, 2)
+ description = data.items[0].description if len(data.items) == 1 else f"{len(data.items)} Positionen"
+
+ conn.execute("""
+ UPDATE invoices SET
+ recipient_name=?, recipient_email=?, recipient_address=?,
+ description=?, service_period=?,
+ amount_net=?, discount_pct=?, discount_amount=?,
+ amount_after_discount=?, tax_rate=?, tax_amount=?, amount_gross=?,
+ notes=?
+ WHERE id=?
+ """, (
+ data.recipient_name, data.recipient_email, data.recipient_address,
+ description, data.service_period,
+ amount_net, discount_pct, discount_amount,
+ amount_after_discount, TAX_RATE, tax_amount, amount_gross,
+ data.notes, invoice_id,
+ ))
+ conn.execute("DELETE FROM invoice_items WHERE invoice_id=?", (invoice_id,))
+ for item in data.items:
+ total = round(item.quantity * item.unit_price, 2)
+ conn.execute(
+ "INSERT INTO invoice_items (invoice_id, description, quantity, unit_price, total) VALUES (?,?,?,?,?)",
+ (invoice_id, item.description, item.quantity, item.unit_price, total)
+ )
+ row = conn.execute("SELECT * FROM invoices WHERE id=?", (invoice_id,)).fetchone()
+ items = _fetch_items(conn, invoice_id)
+
+ result = _row_to_dict(row)
+ result["items"] = items
+ return result
+
+
@router.post("", status_code=201)
def create_invoice(data: InvoiceCreate, admin=Depends(require_admin)):
if not data.items:
@@ -501,6 +553,8 @@ async def send_invoice(invoice_id: int, admin=Depends(require_admin)):
raise HTTPException(404, "Rechnung nicht gefunden.")
if row["status"] == "cancelled":
raise HTTPException(400, "Stornierte Rechnung kann nicht gesendet werden.")
+ if row["status"] == "paid":
+ raise HTTPException(400, "Bezahlte Rechnung kann nicht erneut gesendet werden.")
items = _fetch_items(conn, invoice_id)
invoice = _row_to_dict(row)
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index b972a93..ef71b8a 100644
--- a/backend/static/js/app.js
+++ b/backend/static/js/app.js
@@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
-const APP_VER = '967'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '968'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js
index fed5b2b..7472a79 100644
--- a/backend/static/js/pages/admin.js
+++ b/backend/static/js/pages/admin.js
@@ -3726,10 +3726,19 @@ window.Page_admin = (() => {
const rows = invoices.map((inv, i) => {
const actions = [];
if (inv.status === 'draft') {
+ actions.push(``);
actions.push(``);
}
+ if (inv.status === 'sent') {
+ actions.push(``);
+ }
if (inv.status === 'sent') {
actions.push(`