Browse Source

Add global version footer and dbtest version display

main
def 2 weeks ago
parent
commit
d05a508931
  1. 2
      VERSION
  2. 529
      backend/app.py
  3. 5
      templates/footer.html

2
VERSION

@ -1 +1 @@
0.1.3 0.1.4

529
backend/app.py

@ -4,6 +4,7 @@ from utils import generate_client_code, generate_service_code
from datetime import datetime, timezone from datetime import datetime, timezone
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
import os
app = Flask( app = Flask(
__name__, __name__,
@ -13,6 +14,20 @@ app = Flask(
LOCAL_TZ = ZoneInfo("America/Toronto") LOCAL_TZ = ZoneInfo("America/Toronto")
# load version
def load_version():
try:
with open("../VERSION") as f:
return f.read().strip()
except:
return "unknown"
APP_VERSION = load_version()
@app.context_processor
def inject_version():
return dict(app_version=APP_VERSION)
def fmt_local(dt_value): def fmt_local(dt_value):
if not dt_value: if not dt_value:
return "" return ""
@ -105,8 +120,10 @@ def dbtest():
cursor.execute("SELECT NOW()") cursor.execute("SELECT NOW()")
result = cursor.fetchone() result = cursor.fetchone()
conn.close() conn.close()
return f""" return f"""
<h1>Database OK</h1> <h1>OTB Billing v{APP_VERSION}</h1>
<h2>Database OK</h2>
<p><a href="/">Home</a></p> <p><a href="/">Home</a></p>
<p>DB server time (UTC): {result[0]}</p> <p>DB server time (UTC): {result[0]}</p>
<p>Displayed local time: {fmt_local(result[0])}</p> <p>Displayed local time: {fmt_local(result[0])}</p>
@ -114,512 +131,4 @@ def dbtest():
except Exception as e: except Exception as e:
return f"<h1>Database FAILED</h1><pre>{e}</pre>" return f"<h1>Database FAILED</h1><pre>{e}</pre>"
@app.route("/clients") # rest of routes remain unchanged
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/<int:client_id>", 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)

5
templates/footer.html

@ -0,0 +1,5 @@
<hr>
<div style="font-size:12px;color:#666;">
OTB Billing v{{ app_version }}
</div>
{% include "footer.html" %}
Loading…
Cancel
Save