From da559c89b1de6ff0d0e1d7a9ecb6e58a8cdeb010 Mon Sep 17 00:00:00 2001 From: def Date: Sun, 8 Mar 2026 18:08:44 +0000 Subject: [PATCH] Fix app startup and add version footer support --- backend/app.py | 535 ++++++++++++++++++++++++++++++++++- templates/clients/edit.html | 1 + templates/clients/list.html | 1 + templates/dashboard.html | 1 + templates/footer.html | 1 - templates/invoices/list.html | 1 + templates/payments/list.html | 1 + templates/services/list.html | 1 + 8 files changed, 526 insertions(+), 16 deletions(-) diff --git a/backend/app.py b/backend/app.py index d01f0a6..b634fc9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -4,7 +4,6 @@ from utils import generate_client_code, generate_service_code from datetime import datetime, timezone from zoneinfo import ZoneInfo from decimal import Decimal, InvalidOperation -import os app = Flask( __name__, @@ -14,19 +13,18 @@ app = Flask( LOCAL_TZ = ZoneInfo("America/Toronto") -# load version def load_version(): try: - with open("../VERSION") as f: + with open("/home/def/otb_billing/VERSION", "r") as f: return f.read().strip() - except: + except Exception: return "unknown" APP_VERSION = load_version() @app.context_processor def inject_version(): - return dict(app_version=APP_VERSION) + return {"app_version": APP_VERSION} def fmt_local(dt_value): if not dt_value: @@ -96,9 +94,9 @@ def index(): outstanding_invoices = cursor.fetchone()["outstanding_invoices"] cursor.execute(""" - SELECT COALESCE(SUM(cad_value_at_payment),0) AS revenue_received + SELECT COALESCE(SUM(cad_value_at_payment), 0) AS revenue_received FROM payments - WHERE payment_status='confirmed' + WHERE payment_status = 'confirmed' """) revenue_received = cursor.fetchone()["revenue_received"] @@ -120,15 +118,522 @@ def dbtest(): cursor.execute("SELECT NOW()") result = cursor.fetchone() conn.close() - return f""" -

OTB Billing v{APP_VERSION}

-

Database OK

-

Home

-

DB server time (UTC): {result[0]}

-

Displayed local time: {fmt_local(result[0])}

-""" +

OTB Billing v{APP_VERSION}

+

Database OK

+

Home

+

DB server time (UTC): {result[0]}

+

Displayed local time: {fmt_local(result[0])}

+ """ except Exception as e: return f"

Database FAILED

{e}
" -# rest of routes remain unchanged +@app.route("/clients") +def clients(): + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT * FROM clients ORDER BY id DESC") + clients = cursor.fetchall() + conn.close() + return render_template("clients/list.html", clients=clients) + +@app.route("/clients/new", methods=["GET", "POST"]) +def new_client(): + if request.method == "POST": + company_name = request.form["company_name"] + contact_name = request.form["contact_name"] + email = request.form["email"] + phone = request.form["phone"] + + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT MAX(id) AS last_id FROM clients") + result = cursor.fetchone() + last_number = result["last_id"] if result["last_id"] else 0 + + client_code = generate_client_code(company_name, last_number) + + insert_cursor = conn.cursor() + insert_cursor.execute( + """ + INSERT INTO clients + (client_code, company_name, contact_name, email, phone) + VALUES (%s, %s, %s, %s, %s) + """, + (client_code, company_name, contact_name, email, phone) + ) + conn.commit() + conn.close() + + return redirect("/clients") + + return render_template("clients/new.html") + +@app.route("/clients/edit/", methods=["GET", "POST"]) +def edit_client(client_id): + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + if request.method == "POST": + company_name = request.form.get("company_name", "").strip() + contact_name = request.form.get("contact_name", "").strip() + email = request.form.get("email", "").strip() + phone = request.form.get("phone", "").strip() + status = request.form.get("status", "").strip() + notes = request.form.get("notes", "").strip() + + errors = [] + + if not company_name: + errors.append("Company name is required.") + if not status: + errors.append("Status is required.") + + if errors: + cursor.execute("SELECT * FROM clients WHERE id = %s", (client_id,)) + client = cursor.fetchone() + conn.close() + return render_template("clients/edit.html", client=client, errors=errors) + + update_cursor = conn.cursor() + update_cursor.execute(""" + UPDATE clients + SET company_name = %s, + contact_name = %s, + email = %s, + phone = %s, + status = %s, + notes = %s + WHERE id = %s + """, ( + company_name, + contact_name or None, + email or None, + phone or None, + status, + notes or None, + client_id + )) + conn.commit() + conn.close() + return redirect("/clients") + + cursor.execute("SELECT * FROM clients WHERE id = %s", (client_id,)) + client = cursor.fetchone() + conn.close() + + if not client: + return "Client not found", 404 + + return render_template("clients/edit.html", client=client, errors=[]) + +@app.route("/services") +def services(): + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute(""" + SELECT s.*, c.client_code, c.company_name + FROM services s + JOIN clients c ON s.client_id = c.id + ORDER BY s.id DESC + """) + services = cursor.fetchall() + conn.close() + return render_template("services/list.html", services=services) + +@app.route("/services/new", methods=["GET", "POST"]) +def new_service(): + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + if request.method == "POST": + client_id = request.form["client_id"] + service_name = request.form["service_name"] + service_type = request.form["service_type"] + billing_cycle = request.form["billing_cycle"] + currency_code = request.form["currency_code"] + recurring_amount = request.form["recurring_amount"] + status = request.form["status"] + start_date = request.form["start_date"] or None + description = request.form["description"] + + cursor.execute("SELECT MAX(id) AS last_id FROM services") + result = cursor.fetchone() + last_number = result["last_id"] if result["last_id"] else 0 + service_code = generate_service_code(service_name, last_number) + + insert_cursor = conn.cursor() + insert_cursor.execute( + """ + INSERT INTO services + ( + client_id, + service_code, + service_name, + service_type, + billing_cycle, + status, + currency_code, + recurring_amount, + start_date, + description + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, + ( + client_id, + service_code, + service_name, + service_type, + billing_cycle, + status, + currency_code, + recurring_amount, + start_date, + description + ) + ) + conn.commit() + conn.close() + + return redirect("/services") + + cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC") + clients = cursor.fetchall() + conn.close() + return render_template("services/new.html", clients=clients) + +@app.route("/invoices") +def invoices(): + refresh_overdue_invoices() + + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute(""" + SELECT + i.*, + c.client_code, + c.company_name + FROM invoices i + JOIN clients c ON i.client_id = c.id + ORDER BY i.id DESC + """) + invoices = cursor.fetchall() + conn.close() + return render_template("invoices/list.html", invoices=invoices) + +@app.route("/invoices/new", methods=["GET", "POST"]) +def new_invoice(): + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + if request.method == "POST": + 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() + due_at = request.form.get("due_at", "").strip() + notes = request.form.get("notes", "").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 errors: + try: + amount_value = float(total_amount) + if amount_value <= 0: + errors.append("Total amount must be greater than zero.") + except ValueError: + errors.append("Total amount must be a valid number.") + + if errors: + 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() + + form_data = { + "client_id": client_id, + "service_id": service_id, + "currency_code": currency_code, + "total_amount": total_amount, + "due_at": due_at, + "notes": notes, + } + + return render_template( + "invoices/new.html", + clients=clients, + services=services, + errors=errors, + form_data=form_data, + ) + + cursor.execute("SELECT MAX(id) AS last_id FROM invoices") + result = cursor.fetchone() + number = (result["last_id"] or 0) + 1 + invoice_number = f"INV-{number:04d}" + + insert_cursor = conn.cursor() + insert_cursor.execute(""" + INSERT INTO invoices + ( + client_id, + service_id, + invoice_number, + currency_code, + total_amount, + subtotal_amount, + issued_at, + due_at, + status, + notes + ) + VALUES (%s, %s, %s, %s, %s, %s, UTC_TIMESTAMP(), %s, 'pending', %s) + """, ( + client_id, + service_id, + invoice_number, + currency_code, + total_amount, + total_amount, + due_at, + notes + )) + + conn.commit() + conn.close() + + return redirect("/invoices") + + 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/new.html", + clients=clients, + services=services, + errors=[], + form_data={}, + ) + +@app.route("/payments") +def payments(): + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + cursor.execute(""" + SELECT + p.*, + i.invoice_number, + c.client_code, + c.company_name + FROM payments p + JOIN invoices i ON p.invoice_id = i.id + JOIN clients c ON p.client_id = c.id + ORDER BY p.id DESC + """) + payments = cursor.fetchall() + + conn.close() + return render_template("payments/list.html", payments=payments) + +@app.route("/payments/new", methods=["GET", "POST"]) +def new_payment(): + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + if request.method == "POST": + invoice_id = request.form.get("invoice_id", "").strip() + payment_method = request.form.get("payment_method", "").strip() + payment_currency = request.form.get("payment_currency", "").strip() + payment_amount = request.form.get("payment_amount", "").strip() + cad_value_at_payment = request.form.get("cad_value_at_payment", "").strip() + reference = request.form.get("reference", "").strip() + sender_name = request.form.get("sender_name", "").strip() + txid = request.form.get("txid", "").strip() + wallet_address = request.form.get("wallet_address", "").strip() + notes = request.form.get("notes", "").strip() + + errors = [] + + if not invoice_id: + errors.append("Invoice is required.") + if not payment_method: + errors.append("Payment method is required.") + if not payment_currency: + errors.append("Payment currency is required.") + if not payment_amount: + errors.append("Payment amount is required.") + if not cad_value_at_payment: + errors.append("CAD value at payment is required.") + + amount_value = None + + if not errors: + try: + amount_value = float(payment_amount) + if amount_value <= 0: + errors.append("Payment amount must be greater than zero.") + except ValueError: + errors.append("Payment amount must be a valid number.") + + try: + cad_value = float(cad_value_at_payment) + if cad_value < 0: + errors.append("CAD value at payment cannot be negative.") + except ValueError: + errors.append("CAD value at payment must be a valid number.") + + if errors: + cursor.execute(""" + SELECT + i.id, + i.invoice_number, + c.client_code, + c.company_name, + i.total_amount, + i.amount_paid, + i.currency_code + FROM invoices i + JOIN clients c ON i.client_id = c.id + ORDER BY i.id DESC + """) + invoices = cursor.fetchall() + conn.close() + + form_data = { + "invoice_id": invoice_id, + "payment_method": payment_method, + "payment_currency": payment_currency, + "payment_amount": payment_amount, + "cad_value_at_payment": cad_value_at_payment, + "reference": reference, + "sender_name": sender_name, + "txid": txid, + "wallet_address": wallet_address, + "notes": notes, + } + + return render_template( + "payments/new.html", + invoices=invoices, + errors=errors, + form_data=form_data, + ) + + cursor.execute("SELECT client_id, total_amount, amount_paid FROM invoices WHERE id = %s", (invoice_id,)) + invoice = cursor.fetchone() + + if not invoice: + conn.close() + return "Invoice not found", 404 + + client_id = invoice["client_id"] + new_amount_paid = float(invoice["amount_paid"]) + amount_value + + if new_amount_paid >= float(invoice["total_amount"]): + new_status = "paid" + elif new_amount_paid > 0: + new_status = "partial" + else: + new_status = "pending" + + insert_cursor = conn.cursor() + insert_cursor.execute(""" + INSERT INTO payments + ( + invoice_id, + client_id, + payment_method, + payment_currency, + payment_amount, + cad_value_at_payment, + reference, + sender_name, + txid, + wallet_address, + payment_status, + received_at, + notes + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'confirmed', UTC_TIMESTAMP(), %s) + """, ( + invoice_id, + client_id, + payment_method, + payment_currency, + payment_amount, + cad_value_at_payment, + reference or None, + sender_name or None, + txid or None, + wallet_address or None, + notes or None + )) + + update_cursor = conn.cursor() + if new_status == "paid": + update_cursor.execute(""" + UPDATE invoices + SET amount_paid = %s, + status = %s, + paid_at = UTC_TIMESTAMP() + WHERE id = %s + """, (new_amount_paid, new_status, invoice_id)) + else: + update_cursor.execute(""" + UPDATE invoices + SET amount_paid = %s, + status = %s + WHERE id = %s + """, (new_amount_paid, new_status, invoice_id)) + + conn.commit() + conn.close() + + return redirect("/payments") + + cursor.execute(""" + SELECT + i.id, + i.invoice_number, + c.client_code, + c.company_name, + i.total_amount, + i.amount_paid, + i.currency_code + FROM invoices i + JOIN clients c ON i.client_id = c.id + ORDER BY i.id DESC + """) + invoices = cursor.fetchall() + conn.close() + + return render_template( + "payments/new.html", + invoices=invoices, + errors=[], + form_data={}, + ) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5050, debug=True) diff --git a/templates/clients/edit.html b/templates/clients/edit.html index 0b24e4f..1f7a39e 100644 --- a/templates/clients/edit.html +++ b/templates/clients/edit.html @@ -70,5 +70,6 @@ Notes
+{% include "footer.html" %} diff --git a/templates/clients/list.html b/templates/clients/list.html index 9e84c5b..ef81b19 100644 --- a/templates/clients/list.html +++ b/templates/clients/list.html @@ -38,5 +38,6 @@ +{% include "footer.html" %} diff --git a/templates/dashboard.html b/templates/dashboard.html index 24cf1ab..87908a5 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -30,5 +30,6 @@

Displayed times are shown in Eastern Time (Toronto).

+{% include "footer.html" %} diff --git a/templates/footer.html b/templates/footer.html index 6ca1c2c..7fa0c0d 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -2,4 +2,3 @@
OTB Billing v{{ app_version }}
-{% include "footer.html" %} diff --git a/templates/invoices/list.html b/templates/invoices/list.html index fd4f420..81c6459 100644 --- a/templates/invoices/list.html +++ b/templates/invoices/list.html @@ -42,5 +42,6 @@ +{% include "footer.html" %} diff --git a/templates/payments/list.html b/templates/payments/list.html index e49c74f..3d2dce0 100644 --- a/templates/payments/list.html +++ b/templates/payments/list.html @@ -39,5 +39,6 @@ +{% include "footer.html" %} diff --git a/templates/services/list.html b/templates/services/list.html index f56c940..fdb3f03 100644 --- a/templates/services/list.html +++ b/templates/services/list.html @@ -41,5 +41,6 @@ +{% include "footer.html" %}