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(` - + `, }); @@ -3988,7 +4014,7 @@ window.Page_admin = (() => { if (submitBtn) submitBtn.disabled = true; try { - await API.post('/admin/invoices', { + const payload = { recipient_name: fd.get('recipient_name'), recipient_email: fd.get('recipient_email') || null, recipient_address: fd.get('recipient_address') || null, @@ -3996,9 +4022,14 @@ window.Page_admin = (() => { discount_pct: parseFloat(fd.get('discount_pct')) || 0, notes: fd.get('notes') || null, items, - }); + }; + if (isEdit) { + await API.patch(`/admin/invoices/${invoiceId}`, payload); + } else { + await API.post('/admin/invoices', payload); + } UI.modal.close(); - UI.toast.success('Rechnung erstellt.'); + UI.toast.success(isEdit ? 'Rechnung gespeichert.' : 'Rechnung erstellt.'); reload(); } catch (err) { UI.toast.error(err.message || 'Fehler beim Erstellen.'); diff --git a/backend/static/sw.js b/backend/static/sw.js index 6e8b3b5..cd091e8 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v967'; +const CACHE_VERSION = 'by-v968'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache