Browse Source

Fix app startup and add version footer support

main
def 2 weeks ago
parent
commit
da559c89b1
  1. 519
      backend/app.py
  2. 1
      templates/clients/edit.html
  3. 1
      templates/clients/list.html
  4. 1
      templates/dashboard.html
  5. 1
      templates/footer.html
  6. 1
      templates/invoices/list.html
  7. 1
      templates/payments/list.html
  8. 1
      templates/services/list.html

519
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:
@ -120,7 +118,6 @@ def dbtest():
cursor.execute("SELECT NOW()")
result = cursor.fetchone()
conn.close()
return f"""
<h1>OTB Billing v{APP_VERSION}</h1>
<h2>Database OK</h2>
@ -131,4 +128,512 @@ def dbtest():
except Exception as e:
return f"<h1>Database FAILED</h1><pre>{e}</pre>"
# 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/<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, debug=True)

1
templates/clients/edit.html

@ -70,5 +70,6 @@ Notes<br>
</form>
{% include "footer.html" %}
</body>
</html>

1
templates/clients/list.html

@ -38,5 +38,6 @@
</table>
{% include "footer.html" %}
</body>
</html>

1
templates/dashboard.html

@ -30,5 +30,6 @@
<p>Displayed times are shown in Eastern Time (Toronto).</p>
{% include "footer.html" %}
</body>
</html>

1
templates/footer.html

@ -2,4 +2,3 @@
<div style="font-size:12px;color:#666;">
OTB Billing v{{ app_version }}
</div>
{% include "footer.html" %}

1
templates/invoices/list.html

@ -42,5 +42,6 @@
</table>
{% include "footer.html" %}
</body>
</html>

1
templates/payments/list.html

@ -39,5 +39,6 @@
</table>
{% include "footer.html" %}
</body>
</html>

1
templates/services/list.html

@ -41,5 +41,6 @@
</table>
{% include "footer.html" %}
</body>
</html>

Loading…
Cancel
Save