From be1b1a790f1a32067799ecc823edd254e23c1979 Mon Sep 17 00:00:00 2001 From: def Date: Sun, 8 Mar 2026 19:04:17 +0000 Subject: [PATCH] Add v0.1.6 invoice edit page with payment lock behavior --- VERSION | 2 +- backend/app.py | 136 ++++++++++++++++++++++++++++++++++- templates/invoices/edit.html | 113 +++++++++++++++++++++++++++++ templates/invoices/list.html | 7 ++ 4 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 templates/invoices/edit.html diff --git a/VERSION b/VERSION index 9faa1b7..c946ee6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.5 +0.1.6 diff --git a/backend/app.py b/backend/app.py index 3f57a3e..58c1b21 100644 --- a/backend/app.py +++ b/backend/app.py @@ -415,7 +415,8 @@ def invoices(): SELECT i.*, c.client_code, - c.company_name + c.company_name, + COALESCE((SELECT COUNT(*) FROM payments p WHERE p.invoice_id = i.id), 0) AS payment_count FROM invoices i JOIN clients c ON i.client_id = c.id ORDER BY i.id DESC @@ -537,6 +538,139 @@ def new_invoice(): form_data={}, ) +@app.route("/invoices/edit/", methods=["GET", "POST"]) +def edit_invoice(invoice_id): + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + cursor.execute(""" + SELECT i.*, + COALESCE((SELECT COUNT(*) FROM payments p WHERE p.invoice_id = i.id), 0) AS payment_count + FROM invoices i + WHERE i.id = %s + """, (invoice_id,)) + invoice = cursor.fetchone() + + if not invoice: + conn.close() + return "Invoice not found", 404 + + locked = invoice["payment_count"] > 0 or float(invoice["amount_paid"]) > 0 + + if request.method == "POST": + due_at = request.form.get("due_at", "").strip() + notes = request.form.get("notes", "").strip() + + if locked: + status = request.form.get("status", "").strip() + + if not status: + conn.close() + return render_template("invoices/edit.html", invoice=invoice, clients=[], services=[], errors=["Status is required."], locked=locked) + + update_cursor = conn.cursor() + update_cursor.execute(""" + UPDATE invoices + SET due_at = %s, + status = %s, + notes = %s + WHERE id = %s + """, ( + due_at or None, + status, + notes or None, + invoice_id + )) + conn.commit() + conn.close() + return redirect("/invoices") + + client_id = request.form.get("client_id", "").strip() + service_id = request.form.get("service_id", "").strip() + currency_code = request.form.get("currency_code", "").strip() + total_amount = request.form.get("total_amount", "").strip() + status = request.form.get("status", "").strip() + + errors = [] + + if not client_id: + errors.append("Client is required.") + if not service_id: + errors.append("Service is required.") + if not currency_code: + errors.append("Currency is required.") + if not total_amount: + errors.append("Total amount is required.") + if not due_at: + errors.append("Due date is required.") + if not status: + errors.append("Status is required.") + + if not errors: + try: + amount_value = float(total_amount) + if amount_value < 0: + errors.append("Total amount cannot be negative.") + except ValueError: + errors.append("Total amount must be a valid number.") + + cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name") + clients = cursor.fetchall() + + cursor.execute("SELECT id, service_code, service_name FROM services ORDER BY service_name") + services = cursor.fetchall() + + if errors: + invoice["client_id"] = int(client_id) if client_id else invoice["client_id"] + invoice["service_id"] = int(service_id) if service_id else invoice["service_id"] + invoice["currency_code"] = currency_code or invoice["currency_code"] + invoice["total_amount"] = total_amount or invoice["total_amount"] + invoice["due_at"] = due_at or invoice["due_at"] + invoice["status"] = status or invoice["status"] + invoice["notes"] = notes + conn.close() + return render_template("invoices/edit.html", invoice=invoice, clients=clients, services=services, errors=errors, locked=locked) + + update_cursor = conn.cursor() + update_cursor.execute(""" + UPDATE invoices + SET client_id = %s, + service_id = %s, + currency_code = %s, + total_amount = %s, + subtotal_amount = %s, + due_at = %s, + status = %s, + notes = %s + WHERE id = %s + """, ( + client_id, + service_id, + currency_code, + total_amount, + total_amount, + due_at, + status, + notes or None, + invoice_id + )) + conn.commit() + conn.close() + return redirect("/invoices") + + clients = [] + services = [] + + if not locked: + cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name") + clients = cursor.fetchall() + + cursor.execute("SELECT id, service_code, service_name FROM services ORDER BY service_name") + services = cursor.fetchall() + + conn.close() + return render_template("invoices/edit.html", invoice=invoice, clients=clients, services=services, errors=[], locked=locked) + @app.route("/payments") def payments(): conn = get_db_connection() diff --git a/templates/invoices/edit.html b/templates/invoices/edit.html new file mode 100644 index 0000000..5d2ab00 --- /dev/null +++ b/templates/invoices/edit.html @@ -0,0 +1,113 @@ + + + +Edit Invoice + + + + +

Edit Invoice

+ +

Home

+

Back to Invoices

+ +{% if errors %} +
+ Please fix the following: +
    + {% for error in errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+{% endif %} + +{% if locked %} +
+ This invoice is locked for core edits because payments exist.
+ Core accounting fields cannot be changed after payment activity begins. +
+{% endif %} + +
+ +

+Invoice Number
+ +

+ +{% if not locked %} +

+Client *
+ +

+ +

+Service *
+ +

+ +

+Currency *
+ +

+ +

+Total Amount *
+ +

+{% else %} +

Client

+

Service

+

Currency

+

Total Amount

+{% endif %} + +

+Due Date *
+ +

+ +

+Status *
+ +

+ +

+Notes
+ +

+ +

+ +

+ +
+ +{% include "footer.html" %} + + diff --git a/templates/invoices/list.html b/templates/invoices/list.html index 81c6459..411a97c 100644 --- a/templates/invoices/list.html +++ b/templates/invoices/list.html @@ -23,6 +23,7 @@ Status Issued Due +Actions {% for i in invoices %} @@ -37,6 +38,12 @@ {{ i.status }} {{ i.issued_at|localtime }} {{ i.due_at|localtime }} + + Edit + {% if i.payment_count > 0 %} + (Locked) + {% endif %} + {% endfor %}