from flask import Flask, render_template, request, redirect from db import get_db_connection from utils import generate_client_code, generate_service_code from datetime import datetime, timezone from zoneinfo import ZoneInfo from decimal import Decimal, InvalidOperation app = Flask( __name__, template_folder="../templates", static_folder="../static", ) LOCAL_TZ = ZoneInfo("America/Toronto") def load_version(): try: with open("/home/def/otb_billing/VERSION", "r") as f: return f.read().strip() except Exception: return "unknown" APP_VERSION = load_version() @app.context_processor def inject_version(): return {"app_version": APP_VERSION} def fmt_local(dt_value): if not dt_value: return "" if isinstance(dt_value, str): try: dt_value = datetime.fromisoformat(dt_value) except ValueError: return str(dt_value) if dt_value.tzinfo is None: dt_value = dt_value.replace(tzinfo=timezone.utc) return dt_value.astimezone(LOCAL_TZ).strftime("%Y-%m-%d %I:%M:%S %p") def to_decimal(value): if value is None or value == "": return Decimal("0") try: return Decimal(str(value)) except (InvalidOperation, ValueError): return Decimal("0") def fmt_money(value, currency_code="CAD"): amount = to_decimal(value) if currency_code == "CAD": return f"{amount:.2f}" return f"{amount:.8f}" def refresh_overdue_invoices(): conn = get_db_connection() cursor = conn.cursor() cursor.execute(""" UPDATE invoices SET status = 'overdue' WHERE due_at IS NOT NULL AND due_at < UTC_TIMESTAMP() AND status IN ('pending', 'partial') """) conn.commit() conn.close() def recalc_invoice_totals(invoice_id): conn = get_db_connection() cursor = conn.cursor(dictionary=True) cursor.execute(""" SELECT id, total_amount, due_at, status FROM invoices WHERE id = %s """, (invoice_id,)) invoice = cursor.fetchone() if not invoice: conn.close() return cursor.execute(""" SELECT COALESCE(SUM(payment_amount), 0) AS total_paid FROM payments WHERE invoice_id = %s AND payment_status = 'confirmed' """, (invoice_id,)) row = cursor.fetchone() total_paid = to_decimal(row["total_paid"]) total_amount = to_decimal(invoice["total_amount"]) if invoice["status"] == "cancelled": update_cursor = conn.cursor() update_cursor.execute(""" UPDATE invoices SET amount_paid = %s, paid_at = NULL WHERE id = %s """, ( str(total_paid), invoice_id )) conn.commit() conn.close() return if total_paid >= total_amount and total_amount > 0: new_status = "paid" paid_at_value = "UTC_TIMESTAMP()" elif total_paid > 0: new_status = "partial" paid_at_value = "NULL" else: if invoice["due_at"] and invoice["due_at"] < datetime.utcnow(): new_status = "overdue" else: new_status = "pending" paid_at_value = "NULL" update_cursor = conn.cursor() update_cursor.execute(f""" UPDATE invoices SET amount_paid = %s, status = %s, paid_at = {paid_at_value} WHERE id = %s """, ( str(total_paid), new_status, invoice_id )) conn.commit() conn.close() def get_client_credit_balance(client_id): conn = get_db_connection() cursor = conn.cursor(dictionary=True) cursor.execute(""" SELECT COALESCE(SUM(amount), 0) AS balance FROM credit_ledger WHERE client_id = %s """, (client_id,)) row = cursor.fetchone() conn.close() return to_decimal(row["balance"]) @app.template_filter("localtime") def localtime_filter(value): return fmt_local(value) @app.template_filter("money") def money_filter(value, currency_code="CAD"): return fmt_money(value, currency_code) @app.route("/") def index(): refresh_overdue_invoices() conn = get_db_connection() cursor = conn.cursor(dictionary=True) cursor.execute("SELECT COUNT(*) AS total_clients FROM clients") total_clients = cursor.fetchone()["total_clients"] cursor.execute("SELECT COUNT(*) AS active_services FROM services WHERE status = 'active'") active_services = cursor.fetchone()["active_services"] cursor.execute(""" SELECT COUNT(*) AS outstanding_invoices FROM invoices WHERE status IN ('pending', 'partial', 'overdue') """) outstanding_invoices = cursor.fetchone()["outstanding_invoices"] cursor.execute(""" SELECT COALESCE(SUM(cad_value_at_payment), 0) AS revenue_received FROM payments WHERE payment_status = 'confirmed' """) revenue_received = cursor.fetchone()["revenue_received"] conn.close() return render_template( "dashboard.html", total_clients=total_clients, active_services=active_services, outstanding_invoices=outstanding_invoices, revenue_received=revenue_received, ) @app.route("/dbtest") def dbtest(): try: conn = get_db_connection() cursor = conn.cursor() 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])}

""" except Exception as e: return f"

Database FAILED

{e}
" @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() for client in clients: client["credit_balance"] = get_client_credit_balance(client["id"]) 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() client["credit_balance"] = get_client_credit_balance(client_id) 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 client["credit_balance"] = get_client_credit_balance(client_id) return render_template("clients/edit.html", client=client, errors=[]) @app.route("/credits/") def client_credits(client_id): conn = get_db_connection() cursor = conn.cursor(dictionary=True) cursor.execute(""" SELECT id, client_code, company_name FROM clients WHERE id = %s """, (client_id,)) client = cursor.fetchone() if not client: conn.close() return "Client not found", 404 cursor.execute(""" SELECT * FROM credit_ledger WHERE client_id = %s ORDER BY id DESC """, (client_id,)) entries = cursor.fetchall() conn.close() balance = get_client_credit_balance(client_id) return render_template( "credits/list.html", client=client, entries=entries, balance=balance, ) @app.route("/credits/add/", methods=["GET", "POST"]) def add_credit(client_id): conn = get_db_connection() cursor = conn.cursor(dictionary=True) cursor.execute(""" SELECT id, client_code, company_name FROM clients WHERE id = %s """, (client_id,)) client = cursor.fetchone() if not client: conn.close() return "Client not found", 404 if request.method == "POST": entry_type = request.form.get("entry_type", "").strip() amount = request.form.get("amount", "").strip() currency_code = request.form.get("currency_code", "").strip() notes = request.form.get("notes", "").strip() errors = [] if not entry_type: errors.append("Entry type is required.") if not amount: errors.append("Amount is required.") if not currency_code: errors.append("Currency code is required.") if not errors: try: amount_value = Decimal(str(amount)) if amount_value == 0: errors.append("Amount cannot be zero.") except Exception: errors.append("Amount must be a valid number.") if errors: conn.close() return render_template("credits/add.html", client=client, errors=errors) insert_cursor = conn.cursor() insert_cursor.execute(""" INSERT INTO credit_ledger ( client_id, entry_type, amount, currency_code, notes ) VALUES (%s, %s, %s, %s, %s) """, ( client_id, entry_type, amount, currency_code, notes or None )) conn.commit() conn.close() return redirect(f"/credits/{client_id}") conn.close() return render_template("credits/add.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("/services/edit/", methods=["GET", "POST"]) def edit_service(service_id): conn = get_db_connection() cursor = conn.cursor(dictionary=True) if request.method == "POST": client_id = request.form.get("client_id", "").strip() service_name = request.form.get("service_name", "").strip() service_type = request.form.get("service_type", "").strip() billing_cycle = request.form.get("billing_cycle", "").strip() currency_code = request.form.get("currency_code", "").strip() recurring_amount = request.form.get("recurring_amount", "").strip() status = request.form.get("status", "").strip() start_date = request.form.get("start_date", "").strip() description = request.form.get("description", "").strip() errors = [] if not client_id: errors.append("Client is required.") if not service_name: errors.append("Service name is required.") if not service_type: errors.append("Service type is required.") if not billing_cycle: errors.append("Billing cycle is required.") if not currency_code: errors.append("Currency code is required.") if not recurring_amount: errors.append("Recurring amount is required.") if not status: errors.append("Status is required.") if not errors: try: recurring_amount_value = float(recurring_amount) if recurring_amount_value < 0: errors.append("Recurring amount cannot be negative.") except ValueError: errors.append("Recurring amount must be a valid number.") if errors: cursor.execute(""" SELECT s.*, c.company_name FROM services s LEFT JOIN clients c ON s.client_id = c.id WHERE s.id = %s """, (service_id,)) service = cursor.fetchone() cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC") clients = cursor.fetchall() conn.close() return render_template("services/edit.html", service=service, clients=clients, errors=errors) update_cursor = conn.cursor() update_cursor.execute(""" UPDATE services SET client_id = %s, service_name = %s, service_type = %s, billing_cycle = %s, status = %s, currency_code = %s, recurring_amount = %s, start_date = %s, description = %s WHERE id = %s """, ( client_id, service_name, service_type, billing_cycle, status, currency_code, recurring_amount, start_date or None, description or None, service_id )) conn.commit() conn.close() return redirect("/services") cursor.execute(""" SELECT s.*, c.company_name FROM services s LEFT JOIN clients c ON s.client_id = c.id WHERE s.id = %s """, (service_id,)) service = cursor.fetchone() cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC") clients = cursor.fetchall() conn.close() if not service: return "Service not found", 404 return render_template("services/edit.html", service=service, clients=clients, errors=[]) @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, 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 """) 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("/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: update_cursor = conn.cursor() update_cursor.execute(""" UPDATE invoices SET due_at = %s, notes = %s WHERE id = %s """, ( due_at or None, 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.") manual_statuses = {"draft", "pending", "cancelled"} if status and status not in manual_statuses: errors.append("Manual invoice status must be draft, pending, or cancelled.") 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() 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 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"] 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 )) conn.commit() conn.close() recalc_invoice_totals(invoice_id) 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={}, ) @app.route("/payments/edit/", methods=["GET", "POST"]) def edit_payment(payment_id): 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 WHERE p.id = %s """, (payment_id,)) payment = cursor.fetchone() if not payment: conn.close() return "Payment not found", 404 if request.method == "POST": 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 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.") 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: payment["payment_method"] = payment_method or payment["payment_method"] payment["payment_currency"] = payment_currency or payment["payment_currency"] payment["payment_amount"] = payment_amount or payment["payment_amount"] payment["cad_value_at_payment"] = cad_value_at_payment or payment["cad_value_at_payment"] payment["reference"] = reference payment["sender_name"] = sender_name payment["txid"] = txid payment["wallet_address"] = wallet_address payment["notes"] = notes conn.close() return render_template("payments/edit.html", payment=payment, errors=errors) update_cursor = conn.cursor() update_cursor.execute(""" UPDATE payments SET payment_method = %s, payment_currency = %s, payment_amount = %s, cad_value_at_payment = %s, reference = %s, sender_name = %s, txid = %s, wallet_address = %s, notes = %s WHERE id = %s """, ( 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, payment_id )) conn.commit() invoice_id = payment["invoice_id"] conn.close() recalc_invoice_totals(invoice_id) return redirect("/payments") conn.close() return render_template("payments/edit.html", payment=payment, errors=[]) if __name__ == "__main__": app.run(host="0.0.0.0", port=5050, debug=True)