Browse Source

Release v0.3.0 — reporting, exports, printing, settings

main
def 2 weeks ago
parent
commit
1e34a6e365
  1. 53
      PROJECT_STATE.md
  2. 23
      README.md
  3. 2
      VERSION
  4. 775
      backend/app.py
  5. 1224
      backend/app.py.bak_payments_query_fix
  6. 1224
      backend/app.py.bak_payments_route_exact_fix
  7. 1224
      backend/app.py.bak_payments_route_fix2
  8. 1220
      backend/app.py.bak_void_fix
  9. 2
      backend/config.py
  10. 1184
      backup_fix_void_route_2026-03-08/app.py.bak
  11. 100
      backup_fix_void_route_2026-03-08/payments_list.html.bak
  12. 1583
      backup_logo_support_2026-03-09/app.py.bak
  13. 36
      backup_logo_support_2026-03-09/dashboard.html.bak
  14. 202
      backup_logo_support_2026-03-09/invoice_view.html.bak
  15. 169
      backup_logo_support_2026-03-09/settings.html.bak
  16. 1872
      backup_pre_batch_pdf_export_2026-03-09/app.py.bak
  17. 143
      backup_pre_batch_pdf_export_2026-03-09/invoices_list.html.bak
  18. 2168
      backup_pre_batch_print_2026-03-09/app.py.bak
  19. 169
      backup_pre_batch_print_2026-03-09/invoices_list.html.bak
  20. 1593
      backup_pre_csv_export_2026-03-09/app.py.bak
  21. 49
      backup_pre_csv_export_2026-03-09/clients_list.html.bak
  22. 80
      backup_pre_csv_export_2026-03-09/invoices_list.html.bak
  23. 102
      backup_pre_csv_export_2026-03-09/payments_list.html.bak
  24. 1437
      backup_pre_invoice_numbering_2026-03-09/app.py.bak
  25. 81
      backup_pre_invoice_numbering_2026-03-09/invoices_new.html.bak
  26. 5
      backup_pre_invoice_numbering_2026-03-09/requirements.txt.bak
  27. 1258
      backup_pre_invoice_pdf_2026-03-09/app.py.bak
  28. 79
      backup_pre_invoice_pdf_2026-03-09/invoices_list.html.bak
  29. 187
      backup_pre_invoice_pdf_2026-03-09/invoices_view.html.bak
  30. 1228
      backup_pre_invoice_print_view_2026-03-09/app.py.bak
  31. 78
      backup_pre_invoice_print_view_2026-03-09/invoices_list.html.bak
  32. 1813
      backup_pre_invoice_range_export_2026-03-09/app.py.bak
  33. 81
      backup_pre_invoice_range_export_2026-03-09/invoices_list.html.bak
  34. 1149
      backup_pre_new_payment_rebuild_2026-03-08/app.py.bak
  35. 1149
      backup_pre_overpayment_guard_2026-03-08/app.py.bak
  36. 1149
      backup_pre_payment_filter_2026-03-08/app.py.bak
  37. 103
      backup_pre_payment_filter_2026-03-08/payments_new.html.bak
  38. 1184
      backup_pre_payment_policy_guard_2026-03-08/app.py.bak
  39. 107
      backup_pre_payment_policy_guard_2026-03-08/payments_edit.html.bak
  40. 139
      backup_pre_payment_policy_guard_2026-03-08/payments_new.html.bak
  41. 1184
      backup_pre_payment_void_2026-03-08/app.py.bak
  42. 100
      backup_pre_payment_void_2026-03-08/payments_list.html.bak
  43. 1224
      backup_pre_payments_list_cleanup_2026-03-08/app.py.bak
  44. 100
      backup_pre_payments_list_cleanup_2026-03-08/payments_list.html.bak
  45. 1584
      backup_pre_pdf_logo_2026-03-09/app.py.bak
  46. 2242
      backup_pre_revenue_report_json_2026-03-09/app.py.bak
  47. 42
      backup_pre_revenue_report_json_2026-03-09/dashboard.html.bak
  48. 192
      backup_pre_revenue_report_json_2026-03-09/settings.html.bak
  49. 1462
      backup_pre_settings_config_2026-03-09/app.py.bak
  50. 35
      backup_pre_settings_config_2026-03-09/dashboard.html.bak
  51. 188
      backup_pre_settings_config_2026-03-09/invoices_view.html.bak
  52. 1138
      backup_pre_status_hardening_2026-03-08/app.py.bak
  53. 113
      backup_pre_status_hardening_2026-03-08/invoices_edit.html.bak
  54. 54
      backup_pre_status_hardening_2026-03-08/invoices_list.html.bak
  55. BIN
      favicon.png
  56. BIN
      static/favicon.png
  57. 1
      templates/base.html
  58. 43
      templates/clients/list.html
  59. 1
      templates/clients/new.html
  60. 7
      templates/dashboard.html
  61. 86
      templates/invoices/list.html
  62. 222
      templates/invoices/print_batch.html
  63. 5
      templates/invoices/view.html
  64. 1
      templates/payments/list.html
  65. 73
      templates/reports/revenue.html
  66. 77
      templates/reports/revenue_print.html
  67. 1
      templates/services/new.html
  68. 38
      templates/settings.html
  69. 173
      templates/settings.html.bak_logo_layout_fix
  70. 26
      update_project_state.sh

53
PROJECT_STATE.md

@ -298,3 +298,56 @@ During active development, run in a visible terminal so logs stay visible.
Do not rely on hidden/background launch during normal debug workflow.
=================================================
Version: v0.3.0
Date: 2026-03-09
=================================================
Major milestone release.
Core billing workflow now complete.
Working systems:
Invoices
--------
Create / Edit / Lock after payment
Payments
--------
Manual payments with invoice recalculation
Exports
-------
CSV export
Batch CSV export
PDF export
Batch PDF ZIP export
JSON export
Printing
--------
Single invoice print
Batch invoice print
Reporting
---------
Revenue report
Printable report
JSON report export
Selectable report frequency
Configuration
-------------
Business identity
Tax settings
Logo support
Report frequency selector
Deployment
----------
Flask backend
MariaDB database
Lightweight container operation confirmed

23
README.md

@ -1 +1,24 @@
# otb-billing
## v0.3.0 — 2026-03-09
Major operational milestone release.
New Features
------------
- Revenue reporting system
- JSON export for reports
- Batch invoice printing
- Batch CSV export
- Filtered invoice export
- Invoice logo support (PDF + print)
- Business identity settings
- Report frequency selector (monthly / quarterly / yearly)
Infrastructure
--------------
- Improved reporting backend
- Cleaner filter handling
- Settings system extended
- Print layouts stabilized

2
VERSION

@ -1 +1 @@
0.2.1
v0.3.0

775
backend/app.py

@ -1,13 +1,16 @@
from flask import Flask, render_template, request, redirect, send_file
from flask import Flask, render_template, request, redirect, send_file, make_response, jsonify
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
from io import BytesIO
from io import BytesIO, StringIO
import csv
import zipfile
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
app = Flask(
__name__,
@ -187,6 +190,7 @@ def generate_invoice_number():
APP_SETTINGS_DEFAULTS = {
"business_name": "OTB Billing",
"business_tagline": "By a contractor, for contractors",
"business_logo_url": "",
"business_email": "",
"business_phone": "",
"business_address": "",
@ -196,6 +200,7 @@ APP_SETTINGS_DEFAULTS = {
"tax_number": "",
"business_number": "",
"default_currency": "CAD",
"report_frequency": "monthly",
"invoice_footer": "",
"payment_terms": "",
"local_country": "Canada",
@ -270,6 +275,89 @@ def money_filter(value, currency_code="CAD"):
return fmt_money(value, currency_code)
def get_report_period_bounds(frequency):
now_local = datetime.now(LOCAL_TZ)
if frequency == "yearly":
start_local = now_local.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
label = f"{now_local.year}"
elif frequency == "quarterly":
quarter = ((now_local.month - 1) // 3) + 1
start_month = (quarter - 1) * 3 + 1
start_local = now_local.replace(month=start_month, day=1, hour=0, minute=0, second=0, microsecond=0)
label = f"Q{quarter} {now_local.year}"
else:
start_local = now_local.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
label = now_local.strftime("%B %Y")
start_utc = start_local.astimezone(timezone.utc).replace(tzinfo=None)
end_utc = now_local.astimezone(timezone.utc).replace(tzinfo=None)
return start_utc, end_utc, label
def get_revenue_report_data():
settings = get_app_settings()
frequency = (settings.get("report_frequency") or "monthly").strip().lower()
if frequency not in {"monthly", "quarterly", "yearly"}:
frequency = "monthly"
start_utc, end_utc, label = get_report_period_bounds(frequency)
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT COALESCE(SUM(cad_value_at_payment), 0) AS collected
FROM payments
WHERE payment_status = 'confirmed'
AND received_at >= %s
AND received_at <= %s
""", (start_utc, end_utc))
collected_row = cursor.fetchone()
cursor.execute("""
SELECT COUNT(*) AS invoice_count,
COALESCE(SUM(total_amount), 0) AS invoiced
FROM invoices
WHERE issued_at >= %s
AND issued_at <= %s
""", (start_utc, end_utc))
invoiced_row = cursor.fetchone()
cursor.execute("""
SELECT COUNT(*) AS overdue_count,
COALESCE(SUM(total_amount - amount_paid), 0) AS overdue_balance
FROM invoices
WHERE status = 'overdue'
""")
overdue_row = cursor.fetchone()
cursor.execute("""
SELECT COUNT(*) AS outstanding_count,
COALESCE(SUM(total_amount - amount_paid), 0) AS outstanding_balance
FROM invoices
WHERE status IN ('pending', 'partial', 'overdue')
""")
outstanding_row = cursor.fetchone()
conn.close()
return {
"frequency": frequency,
"period_label": label,
"period_start": start_utc.isoformat(sep=" "),
"period_end": end_utc.isoformat(sep=" "),
"collected_cad": str(to_decimal(collected_row["collected"])),
"invoice_count": int(invoiced_row["invoice_count"] or 0),
"invoiced_total": str(to_decimal(invoiced_row["invoiced"])),
"overdue_count": int(overdue_row["overdue_count"] or 0),
"overdue_balance": str(to_decimal(overdue_row["overdue_balance"])),
"outstanding_count": int(outstanding_row["outstanding_count"] or 0),
"outstanding_balance": str(to_decimal(outstanding_row["outstanding_balance"])),
}
@app.route("/settings", methods=["GET", "POST"])
def settings():
ensure_app_settings_table()
@ -281,6 +369,22 @@ def settings():
settings = get_app_settings()
return render_template("settings.html", settings=settings)
@app.route("/reports/revenue")
def revenue_report():
report = get_revenue_report_data()
return render_template("reports/revenue.html", report=report)
@app.route("/reports/revenue.json")
def revenue_report_json():
report = get_revenue_report_data()
return jsonify(report)
@app.route("/reports/revenue/print")
def revenue_report_print():
report = get_revenue_report_data()
return render_template("reports/revenue_print.html", report=report)
@app.route("/")
def index():
refresh_overdue_invoices()
@ -336,6 +440,61 @@ def dbtest():
except Exception as e:
return f"<h1>Database FAILED</h1><pre>{e}</pre>"
@app.route("/clients/export.csv")
def export_clients_csv():
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT
id,
client_code,
company_name,
contact_name,
email,
phone,
status,
created_at,
updated_at
FROM clients
ORDER BY id ASC
""")
rows = cursor.fetchall()
conn.close()
output = StringIO()
writer = csv.writer(output)
writer.writerow([
"id",
"client_code",
"company_name",
"contact_name",
"email",
"phone",
"status",
"created_at",
"updated_at",
])
for r in rows:
writer.writerow([
r.get("id", ""),
r.get("client_code", ""),
r.get("company_name", ""),
r.get("contact_name", ""),
r.get("email", ""),
r.get("phone", ""),
r.get("status", ""),
r.get("created_at", ""),
r.get("updated_at", ""),
])
response = make_response(output.getvalue())
response.headers["Content-Type"] = "text/csv; charset=utf-8"
response.headers["Content-Disposition"] = "attachment; filename=clients.csv"
return response
@app.route("/clients")
def clients():
conn = get_db_connection()
@ -724,25 +883,536 @@ def edit_service(service_id):
return render_template("services/edit.html", service=service, clients=clients, errors=[])
@app.route("/invoices/export.csv")
def export_invoices_csv():
start_date = (request.args.get("start_date") or "").strip()
end_date = (request.args.get("end_date") or "").strip()
status = (request.args.get("status") or "").strip()
client_id = (request.args.get("client_id") or "").strip()
limit_count = (request.args.get("limit") or "").strip()
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
query = """
SELECT
i.id,
i.invoice_number,
i.client_id,
c.client_code,
c.company_name,
i.service_id,
i.currency_code,
i.subtotal_amount,
i.tax_amount,
i.total_amount,
i.amount_paid,
i.status,
i.issued_at,
i.due_at,
i.paid_at,
i.notes,
i.created_at,
i.updated_at
FROM invoices i
JOIN clients c ON i.client_id = c.id
WHERE 1=1
"""
params = []
if start_date:
query += " AND DATE(i.issued_at) >= %s"
params.append(start_date)
if end_date:
query += " AND DATE(i.issued_at) <= %s"
params.append(end_date)
if status:
query += " AND i.status = %s"
params.append(status)
if client_id:
query += " AND i.client_id = %s"
params.append(client_id)
query += " ORDER BY i.id ASC"
if limit_count:
try:
limit_int = int(limit_count)
if limit_int > 0:
query += " LIMIT %s"
params.append(limit_int)
except ValueError:
pass
cursor.execute(query, tuple(params))
rows = cursor.fetchall()
conn.close()
output = StringIO()
writer = csv.writer(output)
writer.writerow([
"id",
"invoice_number",
"client_id",
"client_code",
"company_name",
"service_id",
"currency_code",
"subtotal_amount",
"tax_amount",
"total_amount",
"amount_paid",
"status",
"issued_at",
"due_at",
"paid_at",
"notes",
"created_at",
"updated_at",
])
for r in rows:
writer.writerow([
r.get("id", ""),
r.get("invoice_number", ""),
r.get("client_id", ""),
r.get("client_code", ""),
r.get("company_name", ""),
r.get("service_id", ""),
r.get("currency_code", ""),
r.get("subtotal_amount", ""),
r.get("tax_amount", ""),
r.get("total_amount", ""),
r.get("amount_paid", ""),
r.get("status", ""),
r.get("issued_at", ""),
r.get("due_at", ""),
r.get("paid_at", ""),
r.get("notes", ""),
r.get("created_at", ""),
r.get("updated_at", ""),
])
filename = "invoices"
if start_date or end_date or status or client_id or limit_count:
filename += "_filtered"
filename += ".csv"
response = make_response(output.getvalue())
response.headers["Content-Type"] = "text/csv; charset=utf-8"
response.headers["Content-Disposition"] = f"attachment; filename={filename}"
return response
@app.route("/invoices/export-pdf.zip")
def export_invoices_pdf_zip():
start_date = (request.args.get("start_date") or "").strip()
end_date = (request.args.get("end_date") or "").strip()
status = (request.args.get("status") or "").strip()
client_id = (request.args.get("client_id") or "").strip()
limit_count = (request.args.get("limit") or "").strip()
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
query = """
SELECT
i.*,
c.client_code,
c.company_name,
c.contact_name,
c.email,
c.phone,
s.service_code,
s.service_name
FROM invoices i
JOIN clients c ON i.client_id = c.id
LEFT JOIN services s ON i.service_id = s.id
WHERE 1=1
"""
params = []
if start_date:
query += " AND DATE(i.issued_at) >= %s"
params.append(start_date)
if end_date:
query += " AND DATE(i.issued_at) <= %s"
params.append(end_date)
if status:
query += " AND i.status = %s"
params.append(status)
if client_id:
query += " AND i.client_id = %s"
params.append(client_id)
query += " ORDER BY i.id ASC"
if limit_count:
try:
limit_int = int(limit_count)
if limit_int > 0:
query += " LIMIT %s"
params.append(limit_int)
except ValueError:
pass
cursor.execute(query, tuple(params))
invoices = cursor.fetchall()
conn.close()
settings = get_app_settings()
def build_invoice_pdf_bytes(invoice, settings):
buffer = BytesIO()
pdf = canvas.Canvas(buffer, pagesize=letter)
width, height = letter
left = 50
right = 560
y = height - 50
def money(value, currency="CAD"):
return f"{to_decimal(value):.2f} {currency}"
pdf.setTitle(f"Invoice {invoice['invoice_number']}")
logo_url = (settings.get("business_logo_url") or "").strip()
if logo_url.startswith("/static/"):
local_logo_path = "/home/def/otb_billing" + logo_url
try:
pdf.drawImage(ImageReader(local_logo_path), left, y - 35, width=42, height=42, preserveAspectRatio=True, mask='auto')
except Exception:
pass
pdf.setFont("Helvetica-Bold", 22)
pdf.drawString(left + 60, y, f"Invoice {invoice['invoice_number']}")
pdf.setFont("Helvetica-Bold", 14)
pdf.drawRightString(right, y, settings.get("business_name") or "OTB Billing")
y -= 18
pdf.setFont("Helvetica", 12)
pdf.drawRightString(right, y, settings.get("business_tagline") or "")
y -= 15
right_lines = [
settings.get("business_address", ""),
settings.get("business_email", ""),
settings.get("business_phone", ""),
settings.get("business_website", ""),
]
for item in right_lines:
if item:
pdf.drawRightString(right, y, item[:80])
y -= 14
y -= 10
pdf.setFont("Helvetica-Bold", 12)
pdf.drawString(left, y, "Status:")
pdf.setFont("Helvetica", 12)
pdf.drawString(left + 45, y, str(invoice["status"]).upper())
y -= 28
pdf.setFont("Helvetica-Bold", 13)
pdf.drawString(left, y, "Bill To")
y -= 20
pdf.setFont("Helvetica-Bold", 12)
pdf.drawString(left, y, invoice["company_name"] or "")
y -= 16
pdf.setFont("Helvetica", 11)
if invoice.get("contact_name"):
pdf.drawString(left, y, str(invoice["contact_name"]))
y -= 15
if invoice.get("email"):
pdf.drawString(left, y, str(invoice["email"]))
y -= 15
if invoice.get("phone"):
pdf.drawString(left, y, str(invoice["phone"]))
y -= 15
pdf.drawString(left, y, f"Client Code: {invoice.get('client_code', '')}")
y -= 28
pdf.setFont("Helvetica-Bold", 13)
pdf.drawString(left, y, "Invoice Details")
y -= 20
pdf.setFont("Helvetica", 11)
pdf.drawString(left, y, f"Invoice #: {invoice['invoice_number']}")
y -= 15
pdf.drawString(left, y, f"Issued: {fmt_local(invoice.get('issued_at'))}")
y -= 15
pdf.drawString(left, y, f"Due: {fmt_local(invoice.get('due_at'))}")
y -= 15
if invoice.get("paid_at"):
pdf.drawString(left, y, f"Paid: {fmt_local(invoice.get('paid_at'))}")
y -= 15
pdf.drawString(left, y, f"Currency: {invoice.get('currency_code', 'CAD')}")
y -= 28
pdf.setFont("Helvetica-Bold", 11)
pdf.drawString(left, y, "Service Code")
pdf.drawString(180, y, "Service")
pdf.drawString(330, y, "Description")
pdf.drawRightString(right, y, "Total")
y -= 14
pdf.line(left, y, right, y)
y -= 18
pdf.setFont("Helvetica", 11)
pdf.drawString(left, y, str(invoice.get("service_code") or "-"))
pdf.drawString(180, y, str(invoice.get("service_name") or "-"))
pdf.drawString(330, y, str(invoice.get("notes") or "-")[:28])
pdf.drawRightString(right, y, money(invoice.get("total_amount"), invoice.get("currency_code", "CAD")))
y -= 28
totals_x_label = 360
totals_x_value = right
totals = [
("Subtotal", money(invoice.get("subtotal_amount"), invoice.get("currency_code", "CAD"))),
((settings.get("tax_label") or "Tax"), money(invoice.get("tax_amount"), invoice.get("currency_code", "CAD"))),
("Total", money(invoice.get("total_amount"), invoice.get("currency_code", "CAD"))),
("Paid", money(invoice.get("amount_paid"), invoice.get("currency_code", "CAD"))),
]
remaining = to_decimal(invoice.get("total_amount")) - to_decimal(invoice.get("amount_paid"))
for label, value in totals:
pdf.setFont("Helvetica-Bold", 11)
pdf.drawString(totals_x_label, y, label)
pdf.setFont("Helvetica", 11)
pdf.drawRightString(totals_x_value, y, value)
y -= 18
pdf.setFont("Helvetica-Bold", 11)
pdf.drawString(totals_x_label, y, "Remaining")
pdf.drawRightString(totals_x_value, y, f"{remaining:.2f} {invoice.get('currency_code', 'CAD')}")
y -= 25
if settings.get("tax_number"):
pdf.setFont("Helvetica", 10)
pdf.drawString(left, y, f"{settings.get('tax_label') or 'Tax'} Number: {settings.get('tax_number')}")
y -= 14
if settings.get("business_number"):
pdf.setFont("Helvetica", 10)
pdf.drawString(left, y, f"Business Number: {settings.get('business_number')}")
y -= 14
if settings.get("payment_terms"):
y -= 8
pdf.setFont("Helvetica-Bold", 11)
pdf.drawString(left, y, "Payment Terms")
y -= 15
pdf.setFont("Helvetica", 10)
terms = settings.get("payment_terms", "")
for chunk_start in range(0, len(terms), 90):
line_text = terms[chunk_start:chunk_start+90]
pdf.drawString(left, y, line_text)
y -= 13
if settings.get("invoice_footer"):
y -= 8
pdf.setFont("Helvetica-Bold", 11)
pdf.drawString(left, y, "Footer")
y -= 15
pdf.setFont("Helvetica", 10)
footer = settings.get("invoice_footer", "")
for chunk_start in range(0, len(footer), 90):
line_text = footer[chunk_start:chunk_start+90]
pdf.drawString(left, y, line_text)
y -= 13
pdf.showPage()
pdf.save()
buffer.seek(0)
return buffer.getvalue()
zip_buffer = BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf:
for invoice in invoices:
pdf_bytes = build_invoice_pdf_bytes(invoice, settings)
zipf.writestr(f"{invoice['invoice_number']}.pdf", pdf_bytes)
zip_buffer.seek(0)
filename = "invoices_export"
if start_date:
filename += f"_{start_date}"
if end_date:
filename += f"_to_{end_date}"
if status:
filename += f"_{status}"
if client_id:
filename += f"_client_{client_id}"
if limit_count:
filename += f"_limit_{limit_count}"
filename += ".zip"
return send_file(
zip_buffer,
mimetype="application/zip",
as_attachment=True,
download_name=filename
)
@app.route("/invoices/print")
def print_invoices():
refresh_overdue_invoices()
start_date = (request.args.get("start_date") or "").strip()
end_date = (request.args.get("end_date") or "").strip()
status = (request.args.get("status") or "").strip()
client_id = (request.args.get("client_id") or "").strip()
limit_count = (request.args.get("limit") or "").strip()
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
query = """
SELECT
i.*,
c.client_code,
c.company_name,
c.contact_name,
c.email,
c.phone,
s.service_code,
s.service_name
FROM invoices i
JOIN clients c ON i.client_id = c.id
LEFT JOIN services s ON i.service_id = s.id
WHERE 1=1
"""
params = []
if start_date:
query += " AND DATE(i.issued_at) >= %s"
params.append(start_date)
if end_date:
query += " AND DATE(i.issued_at) <= %s"
params.append(end_date)
if status:
query += " AND i.status = %s"
params.append(status)
if client_id:
query += " AND i.client_id = %s"
params.append(client_id)
query += " ORDER BY i.id ASC"
if limit_count:
try:
limit_int = int(limit_count)
if limit_int > 0:
query += " LIMIT %s"
params.append(limit_int)
except ValueError:
pass
cursor.execute(query, tuple(params))
invoices = cursor.fetchall()
conn.close()
settings = get_app_settings()
filters = {
"start_date": start_date,
"end_date": end_date,
"status": status,
"client_id": client_id,
"limit": limit_count,
}
return render_template("invoices/print_batch.html", invoices=invoices, settings=settings, filters=filters)
@app.route("/invoices")
def invoices():
refresh_overdue_invoices()
start_date = (request.args.get("start_date") or "").strip()
end_date = (request.args.get("end_date") or "").strip()
status = (request.args.get("status") or "").strip()
client_id = (request.args.get("client_id") or "").strip()
limit_count = (request.args.get("limit") or "").strip()
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("""
query = """
SELECT
i.*,
c.client_code,
c.company_name,
COALESCE((SELECT COUNT(*) FROM payments p WHERE p.invoice_id = i.id), 0) AS payment_count
COALESCE((SELECT COUNT(*) FROM payments p WHERE p.invoice_id = i.id AND p.payment_status = 'confirmed'), 0) AS payment_count
FROM invoices i
JOIN clients c ON i.client_id = c.id
ORDER BY i.id DESC
""")
WHERE 1=1
"""
params = []
if start_date:
query += " AND DATE(i.issued_at) >= %s"
params.append(start_date)
if end_date:
query += " AND DATE(i.issued_at) <= %s"
params.append(end_date)
if status:
query += " AND i.status = %s"
params.append(status)
if client_id:
query += " AND i.client_id = %s"
params.append(client_id)
query += " ORDER BY i.id DESC"
if limit_count:
try:
limit_int = int(limit_count)
if limit_int > 0:
query += " LIMIT %s"
params.append(limit_int)
except ValueError:
pass
cursor.execute(query, tuple(params))
invoices = cursor.fetchall()
cursor.execute("""
SELECT id, client_code, company_name
FROM clients
ORDER BY company_name ASC
""")
clients = cursor.fetchall()
conn.close()
return render_template("invoices/list.html", invoices=invoices)
filters = {
"start_date": start_date,
"end_date": end_date,
"status": status,
"client_id": client_id,
"limit": limit_count,
}
return render_template("invoices/list.html", invoices=invoices, filters=filters, clients=clients)
@app.route("/invoices/new", methods=["GET", "POST"])
def new_invoice():
@ -907,8 +1577,16 @@ def invoice_pdf(invoice_id):
pdf.setTitle(f"Invoice {invoice['invoice_number']}")
logo_url = (settings.get("business_logo_url") or "").strip()
if logo_url.startswith("/static/"):
local_logo_path = "/home/def/otb_billing" + logo_url
try:
pdf.drawImage(ImageReader(local_logo_path), left, y - 35, width=42, height=42, preserveAspectRatio=True, mask='auto')
except Exception:
pass
pdf.setFont("Helvetica-Bold", 22)
pdf.drawString(left, y, f"Invoice {invoice['invoice_number']}")
pdf.drawString(left + 60, y, f"Invoice {invoice['invoice_number']}")
pdf.setFont("Helvetica-Bold", 14)
pdf.drawRightString(right, y, settings.get("business_name") or "OTB Billing")
@ -1215,6 +1893,87 @@ def edit_invoice(invoice_id):
conn.close()
return render_template("invoices/edit.html", invoice=invoice, clients=clients, services=services, errors=[], locked=locked)
@app.route("/payments/export.csv")
def export_payments_csv():
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT
p.id,
p.invoice_id,
i.invoice_number,
p.client_id,
c.client_code,
c.company_name,
p.payment_method,
p.payment_currency,
p.payment_amount,
p.cad_value_at_payment,
p.reference,
p.sender_name,
p.txid,
p.wallet_address,
p.payment_status,
p.received_at,
p.notes
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 ASC
""")
rows = cursor.fetchall()
conn.close()
output = StringIO()
writer = csv.writer(output)
writer.writerow([
"id",
"invoice_id",
"invoice_number",
"client_id",
"client_code",
"company_name",
"payment_method",
"payment_currency",
"payment_amount",
"cad_value_at_payment",
"reference",
"sender_name",
"txid",
"wallet_address",
"payment_status",
"received_at",
"notes",
])
for r in rows:
writer.writerow([
r.get("id", ""),
r.get("invoice_id", ""),
r.get("invoice_number", ""),
r.get("client_id", ""),
r.get("client_code", ""),
r.get("company_name", ""),
r.get("payment_method", ""),
r.get("payment_currency", ""),
r.get("payment_amount", ""),
r.get("cad_value_at_payment", ""),
r.get("reference", ""),
r.get("sender_name", ""),
r.get("txid", ""),
r.get("wallet_address", ""),
r.get("payment_status", ""),
r.get("received_at", ""),
r.get("notes", ""),
])
response = make_response(output.getvalue())
response.headers["Content-Type"] = "text/csv; charset=utf-8"
response.headers["Content-Disposition"] = "attachment; filename=payments.csv"
return response
@app.route("/payments")
def payments():
conn = get_db_connection()

1224
backend/app.py.bak_payments_query_fix

File diff suppressed because it is too large Load Diff

1224
backend/app.py.bak_payments_route_exact_fix

File diff suppressed because it is too large Load Diff

1224
backend/app.py.bak_payments_route_fix2

File diff suppressed because it is too large Load Diff

1220
backend/app.py.bak_void_fix

File diff suppressed because it is too large Load Diff

2
backend/config.py

@ -5,4 +5,4 @@ class Config:
DB_PORT = 3306
DB_NAME = "otb_billing"
DB_USER = "otb_billing"
DB_PASSWORD = "CHANGE_THIS_PASSWORD"
DB_PASSWORD ="!2Eas678"

1184
backup_fix_void_route_2026-03-08/app.py.bak

File diff suppressed because it is too large Load Diff

100
backup_fix_void_route_2026-03-08/payments_list.html.bak

@ -0,0 +1,100 @@
<!doctype html>
<html>
<head>
<title>Payments</title>
<style>
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.status-confirmed { background: #dcfce7; color: #166534; }
.status-reversed { background: #fee2e2; color: #991b1b; }
.invoice-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.invoice-pending { background: #dbeafe; color: #1d4ed8; }
.invoice-partial { background: #fef3c7; color: #92400e; }
.invoice-paid { background: #dcfce7; color: #166534; }
.invoice-overdue { background: #fee2e2; color: #991b1b; }
.invoice-cancelled { background: #e5e7eb; color: #4b5563; }
.invoice-draft { background: #e5e7eb; color: #111827; }
.inline-form {
display: inline;
margin: 0;
}
.void-btn {
background: #991b1b;
color: white;
border: 0;
padding: 4px 8px;
cursor: pointer;
}
.void-btn:hover {
opacity: 0.9;
}
</style>
</head>
<body>
<h1>Payments</h1>
<p><a href="/">Home</a></p>
<p><a href="/payments/new">Record Payment</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Invoice</th>
<th>Client</th>
<th>Method</th>
<th>Currency</th>
<th>Amount</th>
<th>CAD Value</th>
<th>Payment Status</th>
<th>Invoice Status</th>
<th>Received</th>
<th>Actions</th>
</tr>
{% for p in payments %}
<tr>
<td>{{ p.id }}</td>
<td>{{ p.invoice_number }}</td>
<td>{{ p.client_code }} - {{ p.company_name }}</td>
<td>{{ p.payment_method }}</td>
<td>{{ p.payment_currency }}</td>
<td>{{ p.payment_amount|money(p.payment_currency) }}</td>
<td>{{ p.cad_value_at_payment|money('CAD') }}</td>
<td><span class="status-badge status-{{ p.payment_status }}">{{ p.payment_status }}</span></td>
<td><span class="invoice-badge invoice-{{ p.invoice_status }}">{{ p.invoice_status }}</span></td>
<td>{{ p.received_at|localtime }}</td>
<td>
<a href="/payments/edit/{{ p.id }}">Edit</a>
{% if p.payment_status == 'confirmed' %}
|
<form method="post" action="/payments/void/{{ p.id }}" class="inline-form" onsubmit="return confirm('Void this payment? This will reverse it from invoice totals but keep the record for history.');">
<button type="submit" class="void-btn">Void</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>

1583
backup_logo_support_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

36
backup_logo_support_2026-03-09/dashboard.html.bak

@ -0,0 +1,36 @@
<!doctype html>
<html>
<head>
<title>OTB Billing Dashboard</title>
</head>
<body>
<h1>{{ app_settings.business_name or 'OTB Billing' }} Dashboard</h1>
<p><a href="/clients">Clients</a></p>
<p><a href="/services">Services</a></p>
<p><a href="/invoices">Invoices</a></p>
<p><a href="/payments">Payments</a></p>
<p><a href="/settings">Settings / Config</a></p>
<p><a href="/dbtest">DB Test</a></p>
<table border="1" cellpadding="10">
<tr>
<th>Total Clients</th>
<th>Active Services</th>
<th>Outstanding Invoices</th>
<th>Revenue Received (CAD)</th>
</tr>
<tr>
<td>{{ total_clients }}</td>
<td>{{ active_services }}</td>
<td>{{ outstanding_invoices }}</td>
<td>{{ revenue_received|money('CAD') }}</td>
</tr>
</table>
<p>Displayed times are shown in Eastern Time (Toronto).</p>
{% include "footer.html" %}
</body>
</html>

202
backup_logo_support_2026-03-09/invoice_view.html.bak

@ -0,0 +1,202 @@
<!doctype html>
<html>
<head>
<title>Invoice {{ invoice.invoice_number }}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 30px;
color: #111;
}
.top-links {
margin-bottom: 20px;
}
.top-links a {
margin-right: 15px;
}
.invoice-wrap {
max-width: 900px;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 25px;
}
.title-box h1 {
margin: 0 0 8px 0;
}
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-draft { background: #e5e7eb; color: #111827; }
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #e5e7eb; color: #4b5563; }
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 25px;
}
.info-card {
border: 1px solid #ccc;
padding: 15px;
}
.info-card h3 {
margin-top: 0;
}
.summary-table,
.total-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 25px;
}
.summary-table th,
.summary-table td,
.total-table th,
.total-table td {
border: 1px solid #ccc;
padding: 10px;
text-align: left;
}
.total-table {
max-width: 420px;
margin-left: auto;
}
.notes-box {
border: 1px solid #ccc;
padding: 15px;
white-space: pre-wrap;
margin-top: 20px;
}
.print-only {
display: none;
}
@media print {
.top-links,
.screen-only,
footer {
display: none !important;
}
.print-only {
display: block;
}
body {
margin: 0;
}
}
</style>
</head>
<body>
<div class="invoice-wrap">
<div class="top-links screen-only">
<a href="/">Home</a>
<a href="/invoices">Back to Invoices</a>
<a href="/invoices/edit/{{ invoice.id }}">Edit Invoice</a>
<a href="#" onclick="window.print(); return false;">Print</a>
<a href="/invoices/pdf/{{ invoice.id }}">PDF</a>
</div>
<div class="header-row">
<div class="title-box">
<h1>Invoice {{ invoice.invoice_number }}</h1>
<span class="status-badge status-{{ invoice.status }}">{{ invoice.status }}</span>
</div>
<div style="text-align:right;">
<strong>{{ settings.business_name or 'OTB Billing' }}</strong><br>
{{ settings.business_tagline or '' }}<br>
{% if settings.business_address %}{{ settings.business_address }}<br>{% endif %}
{% if settings.business_email %}{{ settings.business_email }}<br>{% endif %}
{% if settings.business_phone %}{{ settings.business_phone }}<br>{% endif %}
{% if settings.business_website %}{{ settings.business_website }}{% endif %}
</div>
</div>
<div class="info-grid">
<div class="info-card">
<h3>Bill To</h3>
<strong>{{ invoice.company_name }}</strong><br>
{% if invoice.contact_name %}{{ invoice.contact_name }}<br>{% endif %}
{% if invoice.email %}{{ invoice.email }}<br>{% endif %}
{% if invoice.phone %}{{ invoice.phone }}<br>{% endif %}
Client Code: {{ invoice.client_code }}
</div>
<div class="info-card">
<h3>Invoice Details</h3>
Invoice #: {{ invoice.invoice_number }}<br>
Issued: {{ invoice.issued_at|localtime }}<br>
Due: {{ invoice.due_at|localtime }}<br>
{% if invoice.paid_at %}Paid: {{ invoice.paid_at|localtime }}<br>{% endif %}
Currency: {{ invoice.currency_code }}<br>
{% if settings.tax_number %}{{ settings.tax_label or 'Tax' }} Number: {{ settings.tax_number }}<br>{% endif %}
{% if settings.business_number %}Business Number: {{ settings.business_number }}{% endif %}
</div>
</div>
<table class="summary-table">
<tr>
<th>Service Code</th>
<th>Service</th>
<th>Description</th>
<th>Total</th>
</tr>
<tr>
<td>{{ invoice.service_code or '-' }}</td>
<td>{{ invoice.service_name or '-' }}</td>
<td>{{ invoice.notes or '-' }}</td>
<td>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
<table class="total-table">
<tr>
<th>Subtotal</th>
<td>{{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>{{ settings.tax_label or 'Tax' }}</th>
<td>{{ invoice.tax_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Total</th>
<td><strong>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</strong></td>
</tr>
<tr>
<th>Paid</th>
<td>{{ invoice.amount_paid|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Remaining</th>
<td>{{ (invoice.total_amount - invoice.amount_paid)|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
{% if settings.payment_terms %}
<div class="notes-box">
<strong>Payment Terms</strong><br><br>
{{ settings.payment_terms }}
</div>
{% endif %}
{% if settings.invoice_footer %}
<div class="notes-box">
<strong>Footer</strong><br><br>
{{ settings.invoice_footer }}
</div>
{% endif %}
</div>
{% include "footer.html" %}
</body>
</html>

169
backup_logo_support_2026-03-09/settings.html.bak

@ -0,0 +1,169 @@
<!doctype html>
<html>
<head>
<title>Settings</title>
<style>
body { font-family: Arial, sans-serif; }
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
max-width: 1100px;
}
.card {
border: 1px solid #ccc;
padding: 16px;
}
.card h2 {
margin-top: 0;
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
textarea,
select {
width: 100%;
box-sizing: border-box;
margin-top: 4px;
margin-bottom: 12px;
padding: 8px;
}
textarea { min-height: 90px; }
.checkbox-row {
margin: 8px 0 14px 0;
}
.save-row {
margin-top: 18px;
}
</style>
</head>
<body>
<h1>Settings / Config</h1>
<p><a href="/">Home</a></p>
<form method="post">
<div class="form-grid">
<div class="card">
<h2>Business Identity</h2>
Business Name<br>
<input type="text" name="business_name" value="{{ settings.business_name }}"><br>
Slogan / Tagline<br>
<input type="text" name="business_tagline" value="{{ settings.business_tagline }}"><br>
Business Email<br>
<input type="email" name="business_email" value="{{ settings.business_email }}"><br>
Business Phone<br>
<input type="text" name="business_phone" value="{{ settings.business_phone }}"><br>
Business Address<br>
<textarea name="business_address">{{ settings.business_address }}</textarea><br>
Website<br>
<input type="text" name="business_website" value="{{ settings.business_website }}"><br>
Business Number / Registration Number<br>
<input type="text" name="business_number" value="{{ settings.business_number }}"><br>
Default Currency<br>
<select name="default_currency">
<option value="CAD" {% if settings.default_currency == 'CAD' %}selected{% endif %}>CAD</option>
<option value="USD" {% if settings.default_currency == 'USD' %}selected{% endif %}>USD</option>
<option value="ETHO" {% if settings.default_currency == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if settings.default_currency == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if settings.default_currency == 'ALT' %}selected{% endif %}>ALT</option>
</select>
</div>
<div class="card">
<h2>Tax Settings</h2>
Local Country<br>
<input type="text" name="local_country" value="{{ settings.local_country }}"><br>
Tax Label<br>
<input type="text" name="tax_label" value="{{ settings.tax_label }}"><br>
Tax Rate (%)<br>
<input type="number" step="0.01" name="tax_rate" value="{{ settings.tax_rate }}"><br>
Tax Number<br>
<input type="text" name="tax_number" value="{{ settings.tax_number }}"><br>
<div class="checkbox-row">
<label>
<input type="checkbox" name="apply_local_tax_only" value="1" {% if settings.apply_local_tax_only == '1' %}checked{% endif %}>
Apply tax only to local clients
</label>
</div>
Payment Terms<br>
<textarea name="payment_terms">{{ settings.payment_terms }}</textarea><br>
Invoice Footer<br>
<textarea name="invoice_footer">{{ settings.invoice_footer }}</textarea><br>
</div>
<div class="card">
<h2>Email / SMTP</h2>
SMTP Host<br>
<input type="text" name="smtp_host" value="{{ settings.smtp_host }}"><br>
SMTP Port<br>
<input type="number" name="smtp_port" value="{{ settings.smtp_port }}"><br>
SMTP Username<br>
<input type="text" name="smtp_user" value="{{ settings.smtp_user }}"><br>
SMTP Password<br>
<input type="password" name="smtp_pass" value="{{ settings.smtp_pass }}"><br>
From Email<br>
<input type="email" name="smtp_from_email" value="{{ settings.smtp_from_email }}"><br>
From Name<br>
<input type="text" name="smtp_from_name" value="{{ settings.smtp_from_name }}"><br>
<div class="checkbox-row">
<label>
<input type="checkbox" name="smtp_use_tls" value="1" {% if settings.smtp_use_tls == '1' %}checked{% endif %}>
Use TLS
</label>
</div>
<div class="checkbox-row">
<label>
<input type="checkbox" name="smtp_use_ssl" value="1" {% if settings.smtp_use_ssl == '1' %}checked{% endif %}>
Use SSL
</label>
</div>
</div>
<div class="card">
<h2>Notes</h2>
<p>
These settings become the identity and delivery configuration for this installation.
</p>
<p>
Email sending is not wired yet, but these SMTP settings are stored now so the next step can use them.
</p>
<p>
Tax settings are also stored now so invoice and automation logic can use them later.
</p>
</div>
</div>
<div class="save-row">
<button type="submit">Save Settings</button>
</div>
</form>
{% include "footer.html" %}
</body>
</html>

1872
backup_pre_batch_pdf_export_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

143
backup_pre_batch_pdf_export_2026-03-09/invoices_list.html.bak

@ -0,0 +1,143 @@
<!doctype html>
<html>
<head>
<title>Invoices</title>
<style>
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.status-draft { background: #e5e7eb; color: #111827; }
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #e5e7eb; color: #4b5563; }
.locked-note {
color: #92400e;
font-weight: bold;
}
.filter-box {
border: 1px solid #ccc;
padding: 12px;
margin: 14px 0;
max-width: 950px;
}
.filter-row {
display: flex;
gap: 14px;
align-items: end;
flex-wrap: wrap;
}
.filter-row div {
display: flex;
flex-direction: column;
}
input[type="date"],
select {
padding: 6px;
min-width: 150px;
}
</style>
</head>
<body>
<h1>Invoices</h1>
<p><a href="/">Home</a></p>
<p><a href="/invoices/new">Create Invoice</a></p>
<div class="filter-box">
<form method="get" action="/invoices">
<div class="filter-row">
<div>
<label for="start_date">Issued From</label>
<input type="date" id="start_date" name="start_date" value="{{ filters.start_date if filters is defined else '' }}">
</div>
<div>
<label for="end_date">Issued To</label>
<input type="date" id="end_date" name="end_date" value="{{ filters.end_date if filters is defined else '' }}">
</div>
<div>
<label for="status">Status</label>
<select id="status" name="status">
<option value="" {% if not filters.status %}selected{% endif %}>All</option>
<option value="draft" {% if filters.status == 'draft' %}selected{% endif %}>draft</option>
<option value="pending" {% if filters.status == 'pending' %}selected{% endif %}>pending</option>
<option value="partial" {% if filters.status == 'partial' %}selected{% endif %}>partial</option>
<option value="paid" {% if filters.status == 'paid' %}selected{% endif %}>paid</option>
<option value="overdue" {% if filters.status == 'overdue' %}selected{% endif %}>overdue</option>
<option value="cancelled" {% if filters.status == 'cancelled' %}selected{% endif %}>cancelled</option>
</select>
</div>
<div>
<button type="submit">Apply Filters</button>
</div>
<div>
<a href="/invoices">Clear Filters</a>
</div>
<div>
<a href="/invoices/export.csv?start_date={{ filters.start_date if filters is defined else '' }}&end_date={{ filters.end_date if filters is defined else '' }}&status={{ filters.status if filters is defined else '' }}">Export Filtered CSV</a>
</div>
</div>
</form>
</div>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Invoice</th>
<th>Client</th>
<th>Currency</th>
<th>Total</th>
<th>Paid</th>
<th>Remaining</th>
<th>Status</th>
<th>Issued</th>
<th>Due</th>
<th>Actions</th>
</tr>
{% for i in invoices %}
<tr>
<td>{{ i.id }}</td>
<td>{{ i.invoice_number }}</td>
<td>{{ i.client_code }} - {{ i.company_name }}</td>
<td>{{ i.currency_code }}</td>
<td>{{ i.total_amount|money(i.currency_code) }}</td>
<td>{{ i.amount_paid|money(i.currency_code) }}</td>
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td>
<td>
<span class="status-badge status-{{ i.status }}">{{ i.status }}</span>
</td>
<td>{{ i.issued_at|localtime }}</td>
<td>{{ i.due_at|localtime }}</td>
<td>
<a href="/invoices/view/{{ i.id }}">View</a> |
<a href="/invoices/pdf/{{ i.id }}">PDF</a> |
<a href="/invoices/edit/{{ i.id }}">Edit</a>
{% if i.payment_count > 0 %}
<span class="locked-note">(Locked)</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>

2168
backup_pre_batch_print_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

169
backup_pre_batch_print_2026-03-09/invoices_list.html.bak

@ -0,0 +1,169 @@
<!doctype html>
<html>
<head>
<title>Invoices</title>
<style>
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.status-draft { background: #e5e7eb; color: #111827; }
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #e5e7eb; color: #4b5563; }
.locked-note {
color: #92400e;
font-weight: bold;
}
.filter-box {
border: 1px solid #ccc;
padding: 12px;
margin: 14px 0;
max-width: 1100px;
}
.filter-row {
display: flex;
gap: 14px;
align-items: end;
flex-wrap: wrap;
}
.filter-row div {
display: flex;
flex-direction: column;
}
input[type="date"],
input[type="number"],
select {
padding: 6px;
min-width: 150px;
}
.action-links {
margin-top: 10px;
}
.action-links a {
margin-right: 18px;
}
</style>
</head>
<body>
<h1>Invoices</h1>
<p><a href="/">Home</a></p>
<p><a href="/invoices/new">Create Invoice</a></p>
<div class="filter-box">
<form method="get" action="/invoices">
<div class="filter-row">
<div>
<label for="start_date">Issued From</label>
<input type="date" id="start_date" name="start_date" value="{{ filters.start_date if filters is defined else '' }}">
</div>
<div>
<label for="end_date">Issued To</label>
<input type="date" id="end_date" name="end_date" value="{{ filters.end_date if filters is defined else '' }}">
</div>
<div>
<label for="status">Status</label>
<select id="status" name="status">
<option value="" {% if not filters.status %}selected{% endif %}>All</option>
<option value="draft" {% if filters.status == 'draft' %}selected{% endif %}>draft</option>
<option value="pending" {% if filters.status == 'pending' %}selected{% endif %}>pending</option>
<option value="partial" {% if filters.status == 'partial' %}selected{% endif %}>partial</option>
<option value="paid" {% if filters.status == 'paid' %}selected{% endif %}>paid</option>
<option value="overdue" {% if filters.status == 'overdue' %}selected{% endif %}>overdue</option>
<option value="cancelled" {% if filters.status == 'cancelled' %}selected{% endif %}>cancelled</option>
</select>
</div>
<div>
<label for="client_id">Client</label>
<select id="client_id" name="client_id">
<option value="" {% if not filters.client_id %}selected{% endif %}>All Clients</option>
{% for c in clients %}
<option value="{{ c.id }}" {% if filters.client_id == (c.id|string) %}selected{% endif %}>
{{ c.client_code }} - {{ c.company_name }}
</option>
{% endfor %}
</select>
</div>
<div>
<label for="limit">Limit</label>
<input type="number" id="limit" name="limit" min="1" step="1" value="{{ filters.limit if filters is defined else '' }}">
</div>
<div>
<button type="submit">Apply Filters</button>
</div>
<div>
<a href="/invoices">Clear Filters</a>
</div>
</div>
<div class="action-links">
<a href="/invoices/export.csv?start_date={{ filters.start_date if filters is defined else '' }}&end_date={{ filters.end_date if filters is defined else '' }}&status={{ filters.status if filters is defined else '' }}&client_id={{ filters.client_id if filters is defined else '' }}&limit={{ filters.limit if filters is defined else '' }}">Export Filtered CSV</a>
<a href="/invoices/export-pdf.zip?start_date={{ filters.start_date if filters is defined else '' }}&end_date={{ filters.end_date if filters is defined else '' }}&status={{ filters.status if filters is defined else '' }}&client_id={{ filters.client_id if filters is defined else '' }}&limit={{ filters.limit if filters is defined else '' }}">Export Filtered PDF ZIP</a>
</div>
</form>
</div>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Invoice</th>
<th>Client</th>
<th>Currency</th>
<th>Total</th>
<th>Paid</th>
<th>Remaining</th>
<th>Status</th>
<th>Issued</th>
<th>Due</th>
<th>Actions</th>
</tr>
{% for i in invoices %}
<tr>
<td>{{ i.id }}</td>
<td>{{ i.invoice_number }}</td>
<td>{{ i.client_code }} - {{ i.company_name }}</td>
<td>{{ i.currency_code }}</td>
<td>{{ i.total_amount|money(i.currency_code) }}</td>
<td>{{ i.amount_paid|money(i.currency_code) }}</td>
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td>
<td>
<span class="status-badge status-{{ i.status }}">{{ i.status }}</span>
</td>
<td>{{ i.issued_at|localtime }}</td>
<td>{{ i.due_at|localtime }}</td>
<td>
<a href="/invoices/view/{{ i.id }}">View</a> |
<a href="/invoices/pdf/{{ i.id }}">PDF</a> |
<a href="/invoices/edit/{{ i.id }}">Edit</a>
{% if i.payment_count > 0 %}
<span class="locked-note">(Locked)</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>

1593
backup_pre_csv_export_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

49
backup_pre_csv_export_2026-03-09/clients_list.html.bak

@ -0,0 +1,49 @@
<!doctype html>
<html>
<head>
<title>Clients</title>
</head>
<body>
<h1>Clients</h1>
<p><a href="/">Home</a></p>
<p><a href="/clients/new">Add Client</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Code</th>
<th>Company</th>
<th>Contact</th>
<th>Email</th>
<th>Phone</th>
<th>Status</th>
<th>Actions</th>
</tr>
{% for c in clients %}
<tr>
<td>{{ c.id }}</td>
<td>{{ c.client_code }}</td>
<td>{{ c.company_name }}</td>
<td>{{ c.contact_name }}</td>
<td>{{ c.email }}</td>
<td>{{ c.phone }}</td>
<td>{{ c.status }}</td>
<td>
<a href="/clients/edit/{{ c.id }}">Edit</a> |
<a href="/credits/{{ c.id }}"
style="{% if c.credit_balance > 0 %}color: green;{% elif c.credit_balance < 0 %}color: red;{% else %}color: blue;{% endif %}">
Ledger ({{ c.credit_balance|money('CAD') }})
</a>
</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>

80
backup_pre_csv_export_2026-03-09/invoices_list.html.bak

@ -0,0 +1,80 @@
<!doctype html>
<html>
<head>
<title>Invoices</title>
<style>
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.status-draft { background: #e5e7eb; color: #111827; }
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #e5e7eb; color: #4b5563; }
.locked-note {
color: #92400e;
font-weight: bold;
}
</style>
</head>
<body>
<h1>Invoices</h1>
<p><a href="/">Home</a></p>
<p><a href="/invoices/new">Create Invoice</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Invoice</th>
<th>Client</th>
<th>Currency</th>
<th>Total</th>
<th>Paid</th>
<th>Remaining</th>
<th>Status</th>
<th>Issued</th>
<th>Due</th>
<th>Actions</th>
</tr>
{% for i in invoices %}
<tr>
<td>{{ i.id }}</td>
<td>{{ i.invoice_number }}</td>
<td>{{ i.client_code }} - {{ i.company_name }}</td>
<td>{{ i.currency_code }}</td>
<td>{{ i.total_amount|money(i.currency_code) }}</td>
<td>{{ i.amount_paid|money(i.currency_code) }}</td>
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td>
<td>
<span class="status-badge status-{{ i.status }}">{{ i.status }}</span>
</td>
<td>{{ i.issued_at|localtime }}</td>
<td>{{ i.due_at|localtime }}</td>
<td>
<a href="/invoices/view/{{ i.id }}">View</a> |
<a href="/invoices/pdf/{{ i.id }}">PDF</a> |
<a href="/invoices/edit/{{ i.id }}">Edit</a>
{% if i.payment_count > 0 %}
<span class="locked-note">(Locked)</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>

102
backup_pre_csv_export_2026-03-09/payments_list.html.bak

@ -0,0 +1,102 @@
<!doctype html>
<html>
<head>
<title>Payments</title>
<style>
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.status-confirmed { background: #dcfce7; color: #166534; }
.status-reversed { background: #fee2e2; color: #991b1b; }
.invoice-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.invoice-draft { background: #e5e7eb; color: #111827; }
.invoice-pending { background: #dbeafe; color: #1d4ed8; }
.invoice-partial { background: #fef3c7; color: #92400e; }
.invoice-paid { background: #dcfce7; color: #166534; }
.invoice-overdue { background: #fee2e2; color: #991b1b; }
.invoice-cancelled { background: #e5e7eb; color: #4b5563; }
.inline-form {
display: inline;
margin: 0;
}
.void-btn {
background: #991b1b;
color: white;
border: 0;
padding: 4px 8px;
cursor: pointer;
}
.void-btn:hover {
opacity: 0.9;
}
</style>
</head>
<body>
<h1>Payments</h1>
<p><a href="/">Home</a></p>
<p><a href="/payments/new">Record Payment</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Invoice</th>
<th>Client</th>
<th>Method</th>
<th>Currency</th>
<th>Amount</th>
<th>CAD Value</th>
<th>Payment Status</th>
<th>Invoice Status</th>
<th>Remaining</th>
<th>Received</th>
<th>Actions</th>
</tr>
{% for p in payments %}
<tr>
<td>{{ p.id }}</td>
<td>{{ p.invoice_number }}</td>
<td>{{ p.client_code }} - {{ p.company_name }}</td>
<td>{{ p.payment_method }}</td>
<td>{{ p.payment_currency }}</td>
<td>{{ p.payment_amount|money(p.payment_currency) }}</td>
<td>{{ p.cad_value_at_payment|money('CAD') }}</td>
<td><span class="status-badge status-{{ p.payment_status }}">{{ p.payment_status }}</span></td>
<td><span class="invoice-badge invoice-{{ p.invoice_status }}">{{ p.invoice_status }}</span></td>
<td>{{ (p.total_amount - p.amount_paid)|money(p.invoice_currency_code) }}</td>
<td>{{ p.received_at|localtime }}</td>
<td>
<a href="/payments/edit/{{ p.id }}">Edit</a>
{% if p.payment_status == 'confirmed' %}
|
<form method="post" action="/payments/void/{{ p.id }}" class="inline-form" onsubmit="return confirm('Void this payment? This will reverse it from invoice totals but keep the record for history.');">
<button type="submit" class="void-btn">Void</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>

1437
backup_pre_invoice_numbering_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

81
backup_pre_invoice_numbering_2026-03-09/invoices_new.html.bak

@ -0,0 +1,81 @@
<!doctype html>
<html>
<head>
<title>New Invoice</title>
</head>
<body>
<h1>Create Invoice</h1>
{% if errors %}
<div style="border:1px solid red; padding:10px; margin-bottom:15px;">
<strong>Please fix the following:</strong>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post">
<p>
Client *<br>
<select name="client_id" required>
<option value="">Select client</option>
{% for c in clients %}
<option value="{{ c.id }}" {% if form_data.get('client_id') == (c.id|string) %}selected{% endif %}>
{{ c.client_code }} - {{ c.company_name }}
</option>
{% endfor %}
</select>
</p>
<p>
Service *<br>
<select name="service_id" required>
<option value="">Select service</option>
{% for s in services %}
<option value="{{ s.id }}" {% if form_data.get('service_id') == (s.id|string) %}selected{% endif %}>
{{ s.service_code }} - {{ s.service_name }}
</option>
{% endfor %}
</select>
</p>
<p>
Currency *<br>
<select name="currency_code" required>
<option value="CAD" {% if form_data.get('currency_code', 'CAD') == 'CAD' %}selected{% endif %}>CAD</option>
<option value="ETHO" {% if form_data.get('currency_code') == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if form_data.get('currency_code') == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if form_data.get('currency_code') == 'ALT' %}selected{% endif %}>ALT</option>
</select>
</p>
<p>
Total Amount *<br>
<input type="number" step="0.00000001" min="0.00000001" name="total_amount" value="{{ form_data.get('total_amount', '') }}" required>
</p>
<p>
Due Date *<br>
<input type="date" name="due_at" value="{{ form_data.get('due_at', '') }}" required>
</p>
<p>
Notes<br>
<textarea name="notes">{{ form_data.get('notes', '') }}</textarea>
</p>
<p>
<button type="submit">Create Invoice</button>
</p>
</form>
</body>
</html>
{% include "footer.html" %}

5
backup_pre_invoice_numbering_2026-03-09/requirements.txt.bak

@ -0,0 +1,5 @@
Flask
mysql-connector-python
reportlab
python-dateutil
pytz

1258
backup_pre_invoice_pdf_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

79
backup_pre_invoice_pdf_2026-03-09/invoices_list.html.bak

@ -0,0 +1,79 @@
<!doctype html>
<html>
<head>
<title>Invoices</title>
<style>
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.status-draft { background: #e5e7eb; color: #111827; }
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #e5e7eb; color: #4b5563; }
.locked-note {
color: #92400e;
font-weight: bold;
}
</style>
</head>
<body>
<h1>Invoices</h1>
<p><a href="/">Home</a></p>
<p><a href="/invoices/new">Create Invoice</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Invoice</th>
<th>Client</th>
<th>Currency</th>
<th>Total</th>
<th>Paid</th>
<th>Remaining</th>
<th>Status</th>
<th>Issued</th>
<th>Due</th>
<th>Actions</th>
</tr>
{% for i in invoices %}
<tr>
<td>{{ i.id }}</td>
<td>{{ i.invoice_number }}</td>
<td>{{ i.client_code }} - {{ i.company_name }}</td>
<td>{{ i.currency_code }}</td>
<td>{{ i.total_amount|money(i.currency_code) }}</td>
<td>{{ i.amount_paid|money(i.currency_code) }}</td>
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td>
<td>
<span class="status-badge status-{{ i.status }}">{{ i.status }}</span>
</td>
<td>{{ i.issued_at|localtime }}</td>
<td>{{ i.due_at|localtime }}</td>
<td>
<a href="/invoices/view/{{ i.id }}">View</a> |
<a href="/invoices/edit/{{ i.id }}">Edit</a>
{% if i.payment_count > 0 %}
<span class="locked-note">(Locked)</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>

187
backup_pre_invoice_pdf_2026-03-09/invoices_view.html.bak

@ -0,0 +1,187 @@
<!doctype html>
<html>
<head>
<title>Invoice {{ invoice.invoice_number }}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 30px;
color: #111;
}
.top-links {
margin-bottom: 20px;
}
.top-links a {
margin-right: 15px;
}
.invoice-wrap {
max-width: 900px;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 25px;
}
.title-box h1 {
margin: 0 0 8px 0;
}
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-draft { background: #e5e7eb; color: #111827; }
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #e5e7eb; color: #4b5563; }
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 25px;
}
.info-card {
border: 1px solid #ccc;
padding: 15px;
}
.info-card h3 {
margin-top: 0;
}
.summary-table,
.total-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 25px;
}
.summary-table th,
.summary-table td,
.total-table th,
.total-table td {
border: 1px solid #ccc;
padding: 10px;
text-align: left;
}
.total-table {
max-width: 420px;
margin-left: auto;
}
.notes-box {
border: 1px solid #ccc;
padding: 15px;
white-space: pre-wrap;
}
.print-only {
display: none;
}
@media print {
.top-links,
.screen-only,
footer {
display: none !important;
}
.print-only {
display: block;
}
body {
margin: 0;
}
}
</style>
</head>
<body>
<div class="invoice-wrap">
<div class="top-links screen-only">
<a href="/">Home</a>
<a href="/invoices">Back to Invoices</a>
<a href="/invoices/edit/{{ invoice.id }}">Edit Invoice</a>
<a href="#" onclick="window.print(); return false;">Print</a>
</div>
<div class="header-row">
<div class="title-box">
<h1>Invoice {{ invoice.invoice_number }}</h1>
<span class="status-badge status-{{ invoice.status }}">{{ invoice.status }}</span>
</div>
<div style="text-align:right;">
<strong>OTB Billing</strong><br>
By a contractor, for contractors
</div>
</div>
<div class="info-grid">
<div class="info-card">
<h3>Bill To</h3>
<strong>{{ invoice.company_name }}</strong><br>
{% if invoice.contact_name %}{{ invoice.contact_name }}<br>{% endif %}
{% if invoice.email %}{{ invoice.email }}<br>{% endif %}
{% if invoice.phone %}{{ invoice.phone }}<br>{% endif %}
Client Code: {{ invoice.client_code }}
</div>
<div class="info-card">
<h3>Invoice Details</h3>
Invoice #: {{ invoice.invoice_number }}<br>
Issued: {{ invoice.issued_at|localtime }}<br>
Due: {{ invoice.due_at|localtime }}<br>
{% if invoice.paid_at %}Paid: {{ invoice.paid_at|localtime }}<br>{% endif %}
Currency: {{ invoice.currency_code }}
</div>
</div>
<table class="summary-table">
<tr>
<th>Service Code</th>
<th>Service</th>
<th>Description</th>
<th>Total</th>
</tr>
<tr>
<td>{{ invoice.service_code or '-' }}</td>
<td>{{ invoice.service_name or '-' }}</td>
<td>{{ invoice.notes or '-' }}</td>
<td>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
<table class="total-table">
<tr>
<th>Subtotal</th>
<td>{{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Tax</th>
<td>{{ invoice.tax_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Total</th>
<td><strong>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</strong></td>
</tr>
<tr>
<th>Paid</th>
<td>{{ invoice.amount_paid|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Remaining</th>
<td>{{ (invoice.total_amount - invoice.amount_paid)|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
{% if invoice.notes %}
<div class="notes-box">
<strong>Notes</strong><br><br>
{{ invoice.notes }}
</div>
{% endif %}
</div>
{% include "footer.html" %}
</body>
</html>

1228
backup_pre_invoice_print_view_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

78
backup_pre_invoice_print_view_2026-03-09/invoices_list.html.bak

@ -0,0 +1,78 @@
<!doctype html>
<html>
<head>
<title>Invoices</title>
<style>
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.status-draft { background: #e5e7eb; color: #111827; }
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #e5e7eb; color: #4b5563; }
.locked-note {
color: #92400e;
font-weight: bold;
}
</style>
</head>
<body>
<h1>Invoices</h1>
<p><a href="/">Home</a></p>
<p><a href="/invoices/new">Create Invoice</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Invoice</th>
<th>Client</th>
<th>Currency</th>
<th>Total</th>
<th>Paid</th>
<th>Remaining</th>
<th>Status</th>
<th>Issued</th>
<th>Due</th>
<th>Actions</th>
</tr>
{% for i in invoices %}
<tr>
<td>{{ i.id }}</td>
<td>{{ i.invoice_number }}</td>
<td>{{ i.client_code }} - {{ i.company_name }}</td>
<td>{{ i.currency_code }}</td>
<td>{{ i.total_amount|money(i.currency_code) }}</td>
<td>{{ i.amount_paid|money(i.currency_code) }}</td>
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td>
<td>
<span class="status-badge status-{{ i.status }}">{{ i.status }}</span>
</td>
<td>{{ i.issued_at|localtime }}</td>
<td>{{ i.due_at|localtime }}</td>
<td>
<a href="/invoices/edit/{{ i.id }}">Edit</a>
{% if i.payment_count > 0 %}
<span class="locked-note">(Locked)</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>

1813
backup_pre_invoice_range_export_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

81
backup_pre_invoice_range_export_2026-03-09/invoices_list.html.bak

@ -0,0 +1,81 @@
<!doctype html>
<html>
<head>
<title>Invoices</title>
<style>
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.status-draft { background: #e5e7eb; color: #111827; }
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #e5e7eb; color: #4b5563; }
.locked-note {
color: #92400e;
font-weight: bold;
}
</style>
</head>
<body>
<h1>Invoices</h1>
<p><a href="/">Home</a></p>
<p><a href="/invoices/new">Create Invoice</a></p>
<p><a href="/invoices/export.csv">Export CSV</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Invoice</th>
<th>Client</th>
<th>Currency</th>
<th>Total</th>
<th>Paid</th>
<th>Remaining</th>
<th>Status</th>
<th>Issued</th>
<th>Due</th>
<th>Actions</th>
</tr>
{% for i in invoices %}
<tr>
<td>{{ i.id }}</td>
<td>{{ i.invoice_number }}</td>
<td>{{ i.client_code }} - {{ i.company_name }}</td>
<td>{{ i.currency_code }}</td>
<td>{{ i.total_amount|money(i.currency_code) }}</td>
<td>{{ i.amount_paid|money(i.currency_code) }}</td>
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td>
<td>
<span class="status-badge status-{{ i.status }}">{{ i.status }}</span>
</td>
<td>{{ i.issued_at|localtime }}</td>
<td>{{ i.due_at|localtime }}</td>
<td>
<a href="/invoices/view/{{ i.id }}">View</a> |
<a href="/invoices/pdf/{{ i.id }}">PDF</a> |
<a href="/invoices/edit/{{ i.id }}">Edit</a>
{% if i.payment_count > 0 %}
<span class="locked-note">(Locked)</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>

1149
backup_pre_new_payment_rebuild_2026-03-08/app.py.bak

File diff suppressed because it is too large Load Diff

1149
backup_pre_overpayment_guard_2026-03-08/app.py.bak

File diff suppressed because it is too large Load Diff

1149
backup_pre_payment_filter_2026-03-08/app.py.bak

File diff suppressed because it is too large Load Diff

103
backup_pre_payment_filter_2026-03-08/payments_new.html.bak

@ -0,0 +1,103 @@
<!doctype html>
<html>
<head>
<title>New Payment</title>
</head>
<body>
<h1>Record Payment</h1>
{% if errors %}
<div style="border:1px solid red; padding:10px; margin-bottom:15px;">
<strong>Please fix the following:</strong>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post">
<p>
Invoice *<br>
<select name="invoice_id" required>
<option value="">Select invoice</option>
{% for i in invoices %}
<option value="{{ i.id }}" {% if form_data.get('invoice_id') == (i.id|string) %}selected{% endif %}>
{{ i.invoice_number }} - {{ i.client_code }} - {{ i.company_name }} - Due {{ i.total_amount - i.amount_paid }} {{ i.currency_code }}
</option>
{% endfor %}
</select>
</p>
<p>
Payment Method *<br>
<select name="payment_method" required>
<option value="">Select method</option>
<option value="square" {% if form_data.get('payment_method') == 'square' %}selected{% endif %}>square</option>
<option value="etransfer" {% if form_data.get('payment_method') == 'etransfer' %}selected{% endif %}>etransfer</option>
<option value="crypto_etho" {% if form_data.get('payment_method') == 'crypto_etho' %}selected{% endif %}>crypto_etho</option>
<option value="crypto_egaz" {% if form_data.get('payment_method') == 'crypto_egaz' %}selected{% endif %}>crypto_egaz</option>
<option value="crypto_alt" {% if form_data.get('payment_method') == 'crypto_alt' %}selected{% endif %}>crypto_alt</option>
<option value="cash" {% if form_data.get('payment_method') == 'cash' %}selected{% endif %}>cash</option>
<option value="other" {% if form_data.get('payment_method') == 'other' %}selected{% endif %}>other</option>
</select>
</p>
<p>
Payment Currency *<br>
<select name="payment_currency" required>
<option value="">Select currency</option>
<option value="CAD" {% if form_data.get('payment_currency') == 'CAD' %}selected{% endif %}>CAD</option>
<option value="ETHO" {% if form_data.get('payment_currency') == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if form_data.get('payment_currency') == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if form_data.get('payment_currency') == 'ALT' %}selected{% endif %}>ALT</option>
</select>
</p>
<p>
Payment Amount *<br>
<input type="number" step="0.00000001" min="0.00000001" name="payment_amount" value="{{ form_data.get('payment_amount', '') }}" required>
</p>
<p>
CAD Value At Payment *<br>
<input type="number" step="0.00000001" min="0" name="cad_value_at_payment" value="{{ form_data.get('cad_value_at_payment', '') }}" required>
</p>
<p>
Reference<br>
<input name="reference" value="{{ form_data.get('reference', '') }}">
</p>
<p>
Sender Name<br>
<input name="sender_name" value="{{ form_data.get('sender_name', '') }}">
</p>
<p>
TXID<br>
<input name="txid" value="{{ form_data.get('txid', '') }}">
</p>
<p>
Wallet Address<br>
<input name="wallet_address" value="{{ form_data.get('wallet_address', '') }}">
</p>
<p>
Notes<br>
<textarea name="notes">{{ form_data.get('notes', '') }}</textarea>
</p>
<p>
<button type="submit">Record Payment</button>
</p>
</form>
</body>
</html>
{% include "footer.html" %}

1184
backup_pre_payment_policy_guard_2026-03-08/app.py.bak

File diff suppressed because it is too large Load Diff

107
backup_pre_payment_policy_guard_2026-03-08/payments_edit.html.bak

@ -0,0 +1,107 @@
<!doctype html>
<html>
<head>
<title>Edit Payment</title>
</head>
<body>
<h1>Edit Payment</h1>
<p><a href="/">Home</a></p>
<p><a href="/payments">Back to Payments</a></p>
{% if errors %}
<div style="border:1px solid red; padding:10px; margin-bottom:15px;">
<strong>Please fix the following:</strong>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post">
<p>
Payment ID<br>
<input value="{{ payment.id }}" readonly>
</p>
<p>
Invoice<br>
<input value="{{ payment.invoice_number }} - {{ payment.client_code }} - {{ payment.company_name }}" readonly>
</p>
<p>
Received<br>
<input value="{{ payment.received_at|localtime }}" readonly>
</p>
<p>
Payment Method *<br>
<select name="payment_method" required>
<option value="square" {% if payment.payment_method == 'square' %}selected{% endif %}>square</option>
<option value="etransfer" {% if payment.payment_method == 'etransfer' %}selected{% endif %}>etransfer</option>
<option value="crypto_etho" {% if payment.payment_method == 'crypto_etho' %}selected{% endif %}>crypto_etho</option>
<option value="crypto_egaz" {% if payment.payment_method == 'crypto_egaz' %}selected{% endif %}>crypto_egaz</option>
<option value="crypto_alt" {% if payment.payment_method == 'crypto_alt' %}selected{% endif %}>crypto_alt</option>
<option value="cash" {% if payment.payment_method == 'cash' %}selected{% endif %}>cash</option>
<option value="other" {% if payment.payment_method == 'other' %}selected{% endif %}>other</option>
</select>
</p>
<p>
Payment Currency *<br>
<select name="payment_currency" required>
<option value="CAD" {% if payment.payment_currency == 'CAD' %}selected{% endif %}>CAD</option>
<option value="ETHO" {% if payment.payment_currency == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if payment.payment_currency == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if payment.payment_currency == 'ALT' %}selected{% endif %}>ALT</option>
</select>
</p>
<p>
Payment Amount *<br>
<input type="number" step="0.00000001" min="0.00000001" name="payment_amount" value="{{ payment.payment_amount }}" required>
</p>
<p>
CAD Value At Payment *<br>
<input type="number" step="0.00000001" min="0" name="cad_value_at_payment" value="{{ payment.cad_value_at_payment }}" required>
</p>
<p>
Reference<br>
<input name="reference" value="{{ payment.reference or '' }}">
</p>
<p>
Sender Name<br>
<input name="sender_name" value="{{ payment.sender_name or '' }}">
</p>
<p>
TXID<br>
<input name="txid" value="{{ payment.txid or '' }}">
</p>
<p>
Wallet Address<br>
<input name="wallet_address" value="{{ payment.wallet_address or '' }}">
</p>
<p>
Notes<br>
<textarea name="notes" rows="5" cols="60">{{ payment.notes or '' }}</textarea>
</p>
<p>
<button type="submit">Save Payment</button>
</p>
</form>
{% include "footer.html" %}
</body>
</html>

139
backup_pre_payment_policy_guard_2026-03-08/payments_new.html.bak

@ -0,0 +1,139 @@
<!doctype html>
<html>
<head>
<title>New Payment</title>
<style>
.status-badge {
display: inline-block;
padding: 2px 7px;
border-radius: 999px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.info-box {
border: 1px solid #2563eb;
background: #eff6ff;
padding: 10px;
margin-bottom: 15px;
}
.error-box {
border: 1px solid red;
padding: 10px;
margin-bottom: 15px;
}
</style>
</head>
<body>
<h1>Record Payment</h1>
<p><a href="/">Home</a></p>
<p><a href="/payments">Back to Payments</a></p>
<div class="info-box">
Only invoices with an outstanding balance are shown here.<br>
Paid and cancelled invoices are excluded from payment entry.
</div>
{% if errors %}
<div class="error-box">
<strong>Please fix the following:</strong>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post">
<p>
Invoice *<br>
<select name="invoice_id" required>
<option value="">Select invoice</option>
{% for i in invoices %}
<option value="{{ i.id }}" {% if form_data.get('invoice_id') == (i.id|string) %}selected{% endif %}>
{{ i.invoice_number }} - {{ i.client_code }} - {{ i.company_name }} -
Remaining {{ (i.total_amount - i.amount_paid)|money(i.currency_code) }} {{ i.currency_code }} -
{{ i.status }}
</option>
{% endfor %}
</select>
</p>
<p>
Payment Method *<br>
<select name="payment_method" required>
<option value="">Select method</option>
<option value="square" {% if form_data.get('payment_method') == 'square' %}selected{% endif %}>square</option>
<option value="etransfer" {% if form_data.get('payment_method') == 'etransfer' %}selected{% endif %}>etransfer</option>
<option value="crypto_etho" {% if form_data.get('payment_method') == 'crypto_etho' %}selected{% endif %}>crypto_etho</option>
<option value="crypto_egaz" {% if form_data.get('payment_method') == 'crypto_egaz' %}selected{% endif %}>crypto_egaz</option>
<option value="crypto_alt" {% if form_data.get('payment_method') == 'crypto_alt' %}selected{% endif %}>crypto_alt</option>
<option value="cash" {% if form_data.get('payment_method') == 'cash' %}selected{% endif %}>cash</option>
<option value="other" {% if form_data.get('payment_method') == 'other' %}selected{% endif %}>other</option>
</select>
</p>
<p>
Payment Currency *<br>
<select name="payment_currency" required>
<option value="">Select currency</option>
<option value="CAD" {% if form_data.get('payment_currency') == 'CAD' %}selected{% endif %}>CAD</option>
<option value="ETHO" {% if form_data.get('payment_currency') == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if form_data.get('payment_currency') == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if form_data.get('payment_currency') == 'ALT' %}selected{% endif %}>ALT</option>
</select>
</p>
<p>
Payment Amount *<br>
<input type="number" step="0.00000001" min="0.00000001" name="payment_amount" value="{{ form_data.get('payment_amount', '') }}" required>
</p>
<p>
CAD Value At Payment *<br>
<input type="number" step="0.00000001" min="0" name="cad_value_at_payment" value="{{ form_data.get('cad_value_at_payment', '') }}" required>
</p>
<p>
Reference<br>
<input name="reference" value="{{ form_data.get('reference', '') }}">
</p>
<p>
Sender Name<br>
<input name="sender_name" value="{{ form_data.get('sender_name', '') }}">
</p>
<p>
TXID<br>
<input name="txid" value="{{ form_data.get('txid', '') }}">
</p>
<p>
Wallet Address<br>
<input name="wallet_address" value="{{ form_data.get('wallet_address', '') }}">
</p>
<p>
Notes<br>
<textarea name="notes">{{ form_data.get('notes', '') }}</textarea>
</p>
<p>
<button type="submit">Record Payment</button>
</p>
</form>
{% include "footer.html" %}
</body>
</html>

1184
backup_pre_payment_void_2026-03-08/app.py.bak

File diff suppressed because it is too large Load Diff

100
backup_pre_payment_void_2026-03-08/payments_list.html.bak

@ -0,0 +1,100 @@
<!doctype html>
<html>
<head>
<title>Payments</title>
<style>
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.status-confirmed { background: #dcfce7; color: #166534; }
.status-reversed { background: #fee2e2; color: #991b1b; }
.invoice-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.invoice-pending { background: #dbeafe; color: #1d4ed8; }
.invoice-partial { background: #fef3c7; color: #92400e; }
.invoice-paid { background: #dcfce7; color: #166534; }
.invoice-overdue { background: #fee2e2; color: #991b1b; }
.invoice-cancelled { background: #e5e7eb; color: #4b5563; }
.invoice-draft { background: #e5e7eb; color: #111827; }
.inline-form {
display: inline;
margin: 0;
}
.void-btn {
background: #991b1b;
color: white;
border: 0;
padding: 4px 8px;
cursor: pointer;
}
.void-btn:hover {
opacity: 0.9;
}
</style>
</head>
<body>
<h1>Payments</h1>
<p><a href="/">Home</a></p>
<p><a href="/payments/new">Record Payment</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Invoice</th>
<th>Client</th>
<th>Method</th>
<th>Currency</th>
<th>Amount</th>
<th>CAD Value</th>
<th>Payment Status</th>
<th>Invoice Status</th>
<th>Received</th>
<th>Actions</th>
</tr>
{% for p in payments %}
<tr>
<td>{{ p.id }}</td>
<td>{{ p.invoice_number }}</td>
<td>{{ p.client_code }} - {{ p.company_name }}</td>
<td>{{ p.payment_method }}</td>
<td>{{ p.payment_currency }}</td>
<td>{{ p.payment_amount|money(p.payment_currency) }}</td>
<td>{{ p.cad_value_at_payment|money('CAD') }}</td>
<td><span class="status-badge status-{{ p.payment_status }}">{{ p.payment_status }}</span></td>
<td><span class="invoice-badge invoice-{{ p.invoice_status }}">{{ p.invoice_status }}</span></td>
<td>{{ p.received_at|localtime }}</td>
<td>
<a href="/payments/edit/{{ p.id }}">Edit</a>
{% if p.payment_status == 'confirmed' %}
|
<form method="post" action="/payments/void/{{ p.id }}" class="inline-form" onsubmit="return confirm('Void this payment? This will reverse it from invoice totals but keep the record for history.');">
<button type="submit" class="void-btn">Void</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>

1224
backup_pre_payments_list_cleanup_2026-03-08/app.py.bak

File diff suppressed because it is too large Load Diff

100
backup_pre_payments_list_cleanup_2026-03-08/payments_list.html.bak

@ -0,0 +1,100 @@
<!doctype html>
<html>
<head>
<title>Payments</title>
<style>
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.status-confirmed { background: #dcfce7; color: #166534; }
.status-reversed { background: #fee2e2; color: #991b1b; }
.invoice-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.invoice-pending { background: #dbeafe; color: #1d4ed8; }
.invoice-partial { background: #fef3c7; color: #92400e; }
.invoice-paid { background: #dcfce7; color: #166534; }
.invoice-overdue { background: #fee2e2; color: #991b1b; }
.invoice-cancelled { background: #e5e7eb; color: #4b5563; }
.invoice-draft { background: #e5e7eb; color: #111827; }
.inline-form {
display: inline;
margin: 0;
}
.void-btn {
background: #991b1b;
color: white;
border: 0;
padding: 4px 8px;
cursor: pointer;
}
.void-btn:hover {
opacity: 0.9;
}
</style>
</head>
<body>
<h1>Payments</h1>
<p><a href="/">Home</a></p>
<p><a href="/payments/new">Record Payment</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Invoice</th>
<th>Client</th>
<th>Method</th>
<th>Currency</th>
<th>Amount</th>
<th>CAD Value</th>
<th>Payment Status</th>
<th>Invoice Status</th>
<th>Received</th>
<th>Actions</th>
</tr>
{% for p in payments %}
<tr>
<td>{{ p.id }}</td>
<td>{{ p.invoice_number }}</td>
<td>{{ p.client_code }} - {{ p.company_name }}</td>
<td>{{ p.payment_method }}</td>
<td>{{ p.payment_currency }}</td>
<td>{{ p.payment_amount|money(p.payment_currency) }}</td>
<td>{{ p.cad_value_at_payment|money('CAD') }}</td>
<td><span class="status-badge status-{{ p.payment_status }}">{{ p.payment_status }}</span></td>
<td><span class="invoice-badge invoice-{{ p.invoice_status }}">{{ p.invoice_status }}</span></td>
<td>{{ p.received_at|localtime }}</td>
<td>
<a href="/payments/edit/{{ p.id }}">Edit</a>
{% if p.payment_status == 'confirmed' %}
|
<form method="post" action="/payments/void/{{ p.id }}" class="inline-form" onsubmit="return confirm('Void this payment? This will reverse it from invoice totals but keep the record for history.');">
<button type="submit" class="void-btn">Void</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>

1584
backup_pre_pdf_logo_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

2242
backup_pre_revenue_report_json_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

42
backup_pre_revenue_report_json_2026-03-09/dashboard.html.bak

@ -0,0 +1,42 @@
<!doctype html>
<html>
<head>
<title>OTB Billing Dashboard</title>
</head>
<body>
{% if app_settings.business_logo_url %}
<div style="margin-bottom:15px;">
<img src="{{ app_settings.business_logo_url }}" style="height:60px;">
</div>
{% endif %}
<h1>{{ app_settings.business_name or 'OTB Billing' }} Dashboard</h1>
<p><a href="/clients">Clients</a></p>
<p><a href="/services">Services</a></p>
<p><a href="/invoices">Invoices</a></p>
<p><a href="/payments">Payments</a></p>
<p><a href="/settings">Settings / Config</a></p>
<p><a href="/dbtest">DB Test</a></p>
<table border="1" cellpadding="10">
<tr>
<th>Total Clients</th>
<th>Active Services</th>
<th>Outstanding Invoices</th>
<th>Revenue Received (CAD)</th>
</tr>
<tr>
<td>{{ total_clients }}</td>
<td>{{ active_services }}</td>
<td>{{ outstanding_invoices }}</td>
<td>{{ revenue_received|money('CAD') }}</td>
</tr>
</table>
<p>Displayed times are shown in Eastern Time (Toronto).</p>
{% include "footer.html" %}
</body>
</html>

192
backup_pre_revenue_report_json_2026-03-09/settings.html.bak

@ -0,0 +1,192 @@
<!doctype html>
<html>
<head>
<title>Settings</title>
<style>
body { font-family: Arial, sans-serif; }
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
max-width: 1100px;
}
.card {
border: 1px solid #ccc;
padding: 16px;
}
.card h2 {
margin-top: 0;
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
textarea,
select {
width: 100%;
box-sizing: border-box;
margin-top: 4px;
margin-bottom: 12px;
padding: 8px;
}
textarea { min-height: 90px; }
.checkbox-row {
margin: 8px 0 14px 0;
}
.save-row {
margin-top: 18px;
}
.logo-preview {
margin: 10px 0 14px 0;
}
.logo-preview img {
max-height: 70px;
max-width: 220px;
border: 1px solid #ccc;
padding: 6px;
background: #fff;
}
small {
color: #444;
}
</style>
</head>
<body>
<h1>Settings / Config</h1>
<p><a href="/">Home</a></p>
<form method="post">
<div class="form-grid">
<div class="card">
<h2>Business Identity</h2>
Business Name<br>
<input type="text" name="business_name" value="{{ settings.business_name }}"><br>
Business Logo URL<br>
<input type="text" name="business_logo_url" value="{{ settings.business_logo_url }}"><br>
<small>Example: /static/favicon.png or https://site.com/logo.png</small><br>
{% if settings.business_logo_url %}
<div class="logo-preview">
<img src="{{ settings.business_logo_url }}" alt="Business Logo Preview">
</div>
{% endif %}
Slogan / Tagline<br>
<input type="text" name="business_tagline" value="{{ settings.business_tagline }}"><br>
Business Email<br>
<input type="email" name="business_email" value="{{ settings.business_email }}"><br>
Business Phone<br>
<input type="text" name="business_phone" value="{{ settings.business_phone }}"><br>
Business Address<br>
<textarea name="business_address">{{ settings.business_address }}</textarea><br>
Website<br>
<input type="text" name="business_website" value="{{ settings.business_website }}"><br>
Business Number / Registration Number<br>
<input type="text" name="business_number" value="{{ settings.business_number }}"><br>
Default Currency<br>
<select name="default_currency">
<option value="CAD" {% if settings.default_currency == 'CAD' %}selected{% endif %}>CAD</option>
<option value="USD" {% if settings.default_currency == 'USD' %}selected{% endif %}>USD</option>
<option value="ETHO" {% if settings.default_currency == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if settings.default_currency == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if settings.default_currency == 'ALT' %}selected{% endif %}>ALT</option>
</select>
</div>
<div class="card">
<h2>Tax Settings</h2>
Local Country<br>
<input type="text" name="local_country" value="{{ settings.local_country }}"><br>
Tax Label<br>
<input type="text" name="tax_label" value="{{ settings.tax_label }}"><br>
Tax Rate (%)<br>
<input type="number" step="0.01" name="tax_rate" value="{{ settings.tax_rate }}"><br>
Tax Number<br>
<input type="text" name="tax_number" value="{{ settings.tax_number }}"><br>
<div class="checkbox-row">
<label>
<input type="checkbox" name="apply_local_tax_only" value="1" {% if settings.apply_local_tax_only == '1' %}checked{% endif %}>
Apply tax only to local clients
</label>
</div>
Payment Terms<br>
<textarea name="payment_terms">{{ settings.payment_terms }}</textarea><br>
Invoice Footer<br>
<textarea name="invoice_footer">{{ settings.invoice_footer }}</textarea><br>
</div>
<div class="card">
<h2>Advanced / Email / SMTP</h2>
SMTP Host<br>
<input type="text" name="smtp_host" value="{{ settings.smtp_host }}"><br>
SMTP Port<br>
<input type="number" name="smtp_port" value="{{ settings.smtp_port }}"><br>
SMTP Username<br>
<input type="text" name="smtp_user" value="{{ settings.smtp_user }}"><br>
SMTP Password<br>
<input type="password" name="smtp_pass" value="{{ settings.smtp_pass }}"><br>
From Email<br>
<input type="email" name="smtp_from_email" value="{{ settings.smtp_from_email }}"><br>
From Name<br>
<input type="text" name="smtp_from_name" value="{{ settings.smtp_from_name }}"><br>
<div class="checkbox-row">
<label>
<input type="checkbox" name="smtp_use_tls" value="1" {% if settings.smtp_use_tls == '1' %}checked{% endif %}>
Use TLS
</label>
</div>
<div class="checkbox-row">
<label>
<input type="checkbox" name="smtp_use_ssl" value="1" {% if settings.smtp_use_ssl == '1' %}checked{% endif %}>
Use SSL
</label>
</div>
</div>
<div class="card">
<h2>Notes</h2>
<p>
Branding, tax identity, and SMTP values are stored here for this installation.
</p>
<p>
Logo can be a local static path like <strong>/static/favicon.png</strong> or a full external/IPFS URL.
</p>
<p>
Email sending is not wired yet, but these SMTP settings are stored now so the next step can use them.
</p>
</div>
</div>
<div class="save-row">
<button type="submit">Save Settings</button>
</div>
</form>
{% include "footer.html" %}
</body>
</html>

1462
backup_pre_settings_config_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

35
backup_pre_settings_config_2026-03-09/dashboard.html.bak

@ -0,0 +1,35 @@
<!doctype html>
<html>
<head>
<title>OTB Billing Dashboard</title>
</head>
<body>
<h1>OTB Billing Dashboard</h1>
<p><a href="/clients">Clients</a></p>
<p><a href="/services">Services</a></p>
<p><a href="/invoices">Invoices</a></p>
<p><a href="/payments">Payments</a></p>
<p><a href="/dbtest">DB Test</a></p>
<table border="1" cellpadding="10">
<tr>
<th>Total Clients</th>
<th>Active Services</th>
<th>Outstanding Invoices</th>
<th>Revenue Received (CAD)</th>
</tr>
<tr>
<td>{{ total_clients }}</td>
<td>{{ active_services }}</td>
<td>{{ outstanding_invoices }}</td>
<td>{{ revenue_received|money('CAD') }}</td>
</tr>
</table>
<p>Displayed times are shown in Eastern Time (Toronto).</p>
{% include "footer.html" %}
</body>
</html>

188
backup_pre_settings_config_2026-03-09/invoices_view.html.bak

@ -0,0 +1,188 @@
<!doctype html>
<html>
<head>
<title>Invoice {{ invoice.invoice_number }}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 30px;
color: #111;
}
.top-links {
margin-bottom: 20px;
}
.top-links a {
margin-right: 15px;
}
.invoice-wrap {
max-width: 900px;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 25px;
}
.title-box h1 {
margin: 0 0 8px 0;
}
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-draft { background: #e5e7eb; color: #111827; }
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #e5e7eb; color: #4b5563; }
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 25px;
}
.info-card {
border: 1px solid #ccc;
padding: 15px;
}
.info-card h3 {
margin-top: 0;
}
.summary-table,
.total-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 25px;
}
.summary-table th,
.summary-table td,
.total-table th,
.total-table td {
border: 1px solid #ccc;
padding: 10px;
text-align: left;
}
.total-table {
max-width: 420px;
margin-left: auto;
}
.notes-box {
border: 1px solid #ccc;
padding: 15px;
white-space: pre-wrap;
}
.print-only {
display: none;
}
@media print {
.top-links,
.screen-only,
footer {
display: none !important;
}
.print-only {
display: block;
}
body {
margin: 0;
}
}
</style>
</head>
<body>
<div class="invoice-wrap">
<div class="top-links screen-only">
<a href="/">Home</a>
<a href="/invoices">Back to Invoices</a>
<a href="/invoices/edit/{{ invoice.id }}">Edit Invoice</a>
<a href="#" onclick="window.print(); return false;">Print</a>
<a href="/invoices/pdf/{{ invoice.id }}">PDF</a>
</div>
<div class="header-row">
<div class="title-box">
<h1>Invoice {{ invoice.invoice_number }}</h1>
<span class="status-badge status-{{ invoice.status }}">{{ invoice.status }}</span>
</div>
<div style="text-align:right;">
<strong>OTB Billing</strong><br>
By a contractor, for contractors
</div>
</div>
<div class="info-grid">
<div class="info-card">
<h3>Bill To</h3>
<strong>{{ invoice.company_name }}</strong><br>
{% if invoice.contact_name %}{{ invoice.contact_name }}<br>{% endif %}
{% if invoice.email %}{{ invoice.email }}<br>{% endif %}
{% if invoice.phone %}{{ invoice.phone }}<br>{% endif %}
Client Code: {{ invoice.client_code }}
</div>
<div class="info-card">
<h3>Invoice Details</h3>
Invoice #: {{ invoice.invoice_number }}<br>
Issued: {{ invoice.issued_at|localtime }}<br>
Due: {{ invoice.due_at|localtime }}<br>
{% if invoice.paid_at %}Paid: {{ invoice.paid_at|localtime }}<br>{% endif %}
Currency: {{ invoice.currency_code }}
</div>
</div>
<table class="summary-table">
<tr>
<th>Service Code</th>
<th>Service</th>
<th>Description</th>
<th>Total</th>
</tr>
<tr>
<td>{{ invoice.service_code or '-' }}</td>
<td>{{ invoice.service_name or '-' }}</td>
<td>{{ invoice.notes or '-' }}</td>
<td>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
<table class="total-table">
<tr>
<th>Subtotal</th>
<td>{{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Tax</th>
<td>{{ invoice.tax_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Total</th>
<td><strong>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</strong></td>
</tr>
<tr>
<th>Paid</th>
<td>{{ invoice.amount_paid|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Remaining</th>
<td>{{ (invoice.total_amount - invoice.amount_paid)|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
{% if invoice.notes %}
<div class="notes-box">
<strong>Notes</strong><br><br>
{{ invoice.notes }}
</div>
{% endif %}
</div>
{% include "footer.html" %}
</body>
</html>

1138
backup_pre_status_hardening_2026-03-08/app.py.bak

File diff suppressed because it is too large Load Diff

113
backup_pre_status_hardening_2026-03-08/invoices_edit.html.bak

@ -0,0 +1,113 @@
<!doctype html>
<html>
<head>
<title>Edit Invoice</title>
</head>
<body>
<h1>Edit Invoice</h1>
<p><a href="/">Home</a></p>
<p><a href="/invoices">Back to Invoices</a></p>
{% if errors %}
<div style="border:1px solid red; padding:10px; margin-bottom:15px;">
<strong>Please fix the following:</strong>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if locked %}
<div style="border:1px solid #aa6600; padding:10px; margin-bottom:15px; background:#fff4dd;">
<strong>This invoice is locked for core edits because payments exist.</strong><br>
Core accounting fields cannot be changed after payment activity begins.
</div>
{% endif %}
<form method="post">
<p>
Invoice Number<br>
<input value="{{ invoice.invoice_number }}" readonly>
</p>
{% if not locked %}
<p>
Client *<br>
<select name="client_id" required>
{% for c in clients %}
<option value="{{ c.id }}" {% if invoice.client_id == c.id %}selected{% endif %}>
{{ c.client_code }} - {{ c.company_name }}
</option>
{% endfor %}
</select>
</p>
<p>
Service *<br>
<select name="service_id" required>
{% for s in services %}
<option value="{{ s.id }}" {% if invoice.service_id == s.id %}selected{% endif %}>
{{ s.service_code }} - {{ s.service_name }}
</option>
{% endfor %}
</select>
</p>
<p>
Currency *<br>
<select name="currency_code" required>
<option value="CAD" {% if invoice.currency_code == 'CAD' %}selected{% endif %}>CAD</option>
<option value="ETHO" {% if invoice.currency_code == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if invoice.currency_code == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if invoice.currency_code == 'ALT' %}selected{% endif %}>ALT</option>
</select>
</p>
<p>
Total Amount *<br>
<input type="number" step="0.00000001" min="0" name="total_amount" value="{{ invoice.total_amount }}" required>
</p>
{% else %}
<p>Client<br><input value="{{ invoice.client_id }}" readonly></p>
<p>Service<br><input value="{{ invoice.service_id }}" readonly></p>
<p>Currency<br><input value="{{ invoice.currency_code }}" readonly></p>
<p>Total Amount<br><input value="{{ invoice.total_amount|money(invoice.currency_code) }}" readonly></p>
{% endif %}
<p>
Due Date *<br>
<input type="date" name="due_at" value="{{ invoice.due_at.strftime('%Y-%m-%d') if invoice.due_at else '' }}" required>
</p>
<p>
Status *<br>
<select name="status" required>
<option value="draft" {% if invoice.status == 'draft' %}selected{% endif %}>draft</option>
<option value="pending" {% if invoice.status == 'pending' %}selected{% endif %}>pending</option>
<option value="partial" {% if invoice.status == 'partial' %}selected{% endif %}>partial</option>
<option value="paid" {% if invoice.status == 'paid' %}selected{% endif %}>paid</option>
<option value="overdue" {% if invoice.status == 'overdue' %}selected{% endif %}>overdue</option>
<option value="cancelled" {% if invoice.status == 'cancelled' %}selected{% endif %}>cancelled</option>
</select>
</p>
<p>
Notes<br>
<textarea name="notes" rows="5" cols="60">{{ invoice.notes or '' }}</textarea>
</p>
<p>
<button type="submit">Save Invoice</button>
</p>
</form>
{% include "footer.html" %}
</body>
</html>

54
backup_pre_status_hardening_2026-03-08/invoices_list.html.bak

@ -0,0 +1,54 @@
<!doctype html>
<html>
<head>
<title>Invoices</title>
</head>
<body>
<h1>Invoices</h1>
<p><a href="/">Home</a></p>
<p><a href="/invoices/new">Create Invoice</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Invoice</th>
<th>Client</th>
<th>Currency</th>
<th>Total</th>
<th>Paid</th>
<th>Remaining</th>
<th>Status</th>
<th>Issued</th>
<th>Due</th>
<th>Actions</th>
</tr>
{% for i in invoices %}
<tr>
<td>{{ i.id }}</td>
<td>{{ i.invoice_number }}</td>
<td>{{ i.client_code }} - {{ i.company_name }}</td>
<td>{{ i.currency_code }}</td>
<td>{{ i.total_amount|money(i.currency_code) }}</td>
<td>{{ i.amount_paid|money(i.currency_code) }}</td>
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td>
<td>{{ i.status }}</td>
<td>{{ i.issued_at|localtime }}</td>
<td>{{ i.due_at|localtime }}</td>
<td>
<a href="/invoices/edit/{{ i.id }}">Edit</a>
{% if i.payment_count > 0 %}
(Locked)
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>

BIN
favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
static/favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

1
templates/base.html

@ -18,3 +18,4 @@
</body>
</html>
{% include "footer.html" %}

43
templates/clients/list.html

@ -3,42 +3,39 @@
<head>
<title>Clients</title>
</head>
<body>
<h1>Clients</h1>
<p><a href="/">Home</a></p>
<p><a href="/clients/new">Add Client</a></p>
<p><a href="/clients/export.csv">Export CSV</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Code</th>
<th>Company</th>
<th>Contact</th>
<th>Email</th>
<th>Phone</th>
<th>Status</th>
<th>Actions</th>
<th>ID</th>
<th>Code</th>
<th>Company</th>
<th>Contact</th>
<th>Email</th>
<th>Phone</th>
<th>Status</th>
<th>Actions</th>
</tr>
{% for c in clients %}
<tr>
<td>{{ c.id }}</td>
<td>{{ c.client_code }}</td>
<td>{{ c.company_name }}</td>
<td>{{ c.contact_name }}</td>
<td>{{ c.email }}</td>
<td>{{ c.phone }}</td>
<td>{{ c.status }}</td>
<td>
<a href="/clients/edit/{{ c.id }}">Edit</a> |
<a href="/credits/{{ c.id }}"
style="{% if c.credit_balance > 0 %}color: green;{% elif c.credit_balance < 0 %}color: red;{% else %}color: blue;{% endif %}">
Ledger ({{ c.credit_balance|money('CAD') }})
</a>
</td>
<td>{{ c.id }}</td>
<td>{{ c.client_code }}</td>
<td>{{ c.company_name }}</td>
<td>{{ c.contact_name }}</td>
<td>{{ c.email }}</td>
<td>{{ c.phone }}</td>
<td>{{ c.status }}</td>
<td>
<a href="/clients/edit/{{ c.id }}">Edit</a> |
<a href="/credits/{{ c.id }}">Ledger</a>
</td>
</tr>
{% endfor %}

1
templates/clients/new.html

@ -38,3 +38,4 @@ Phone<br>
</body>
</html>
{% include "footer.html" %}

7
templates/dashboard.html

@ -5,12 +5,19 @@
</head>
<body>
{% if app_settings.business_logo_url %}
<div style="margin-bottom:15px;">
<img src="{{ app_settings.business_logo_url }}" style="height:60px;">
</div>
{% endif %}
<h1>{{ app_settings.business_name or 'OTB Billing' }} Dashboard</h1>
<p><a href="/clients">Clients</a></p>
<p><a href="/services">Services</a></p>
<p><a href="/invoices">Invoices</a></p>
<p><a href="/payments">Payments</a></p>
<p><a href="/reports/revenue">Revenue Report</a></p>
<p><a href="/settings">Settings / Config</a></p>
<p><a href="/dbtest">DB Test</a></p>

86
templates/invoices/list.html

@ -23,6 +23,35 @@
color: #92400e;
font-weight: bold;
}
.filter-box {
border: 1px solid #ccc;
padding: 12px;
margin: 14px 0;
max-width: 1100px;
}
.filter-row {
display: flex;
gap: 14px;
align-items: end;
flex-wrap: wrap;
}
.filter-row div {
display: flex;
flex-direction: column;
}
input[type="date"],
input[type="number"],
select {
padding: 6px;
min-width: 150px;
}
.action-links {
margin-top: 12px;
display: flex;
gap: 18px;
flex-wrap: wrap;
}
</style>
</head>
@ -33,6 +62,63 @@
<p><a href="/">Home</a></p>
<p><a href="/invoices/new">Create Invoice</a></p>
<div class="filter-box">
<form method="get" action="/invoices">
<div class="filter-row">
<div>
<label for="start_date">Issued From</label>
<input type="date" id="start_date" name="start_date" value="{{ filters.start_date if filters is defined else '' }}">
</div>
<div>
<label for="end_date">Issued To</label>
<input type="date" id="end_date" name="end_date" value="{{ filters.end_date if filters is defined else '' }}">
</div>
<div>
<label for="status">Status</label>
<select id="status" name="status">
<option value="" {% if not filters.status %}selected{% endif %}>All</option>
<option value="draft" {% if filters.status == 'draft' %}selected{% endif %}>draft</option>
<option value="pending" {% if filters.status == 'pending' %}selected{% endif %}>pending</option>
<option value="partial" {% if filters.status == 'partial' %}selected{% endif %}>partial</option>
<option value="paid" {% if filters.status == 'paid' %}selected{% endif %}>paid</option>
<option value="overdue" {% if filters.status == 'overdue' %}selected{% endif %}>overdue</option>
<option value="cancelled" {% if filters.status == 'cancelled' %}selected{% endif %}>cancelled</option>
</select>
</div>
<div>
<label for="client_id">Client</label>
<select id="client_id" name="client_id">
<option value="" {% if not filters.client_id %}selected{% endif %}>All Clients</option>
{% for c in clients %}
<option value="{{ c.id }}" {% if filters.client_id == (c.id|string) %}selected{% endif %}>
{{ c.client_code }} - {{ c.company_name }}
</option>
{% endfor %}
</select>
</div>
<div>
<label for="limit">Limit</label>
<input type="number" id="limit" name="limit" min="1" step="1" value="{{ filters.limit if filters is defined else '' }}">
</div>
<div>
<button type="submit">Apply Filters</button>
</div>
</div>
<div class="action-links">
<a href="/invoices">Clear Filters</a>
<a href="/invoices/export.csv?start_date={{ filters.start_date if filters is defined else '' }}&end_date={{ filters.end_date if filters is defined else '' }}&status={{ filters.status if filters is defined else '' }}&client_id={{ filters.client_id if filters is defined else '' }}&limit={{ filters.limit if filters is defined else '' }}">Export Filtered CSV</a>
<a href="/invoices/export-pdf.zip?start_date={{ filters.start_date if filters is defined else '' }}&end_date={{ filters.end_date if filters is defined else '' }}&status={{ filters.status if filters is defined else '' }}&client_id={{ filters.client_id if filters is defined else '' }}&limit={{ filters.limit if filters is defined else '' }}">Export Filtered PDF ZIP</a>
<a href="/invoices/print?start_date={{ filters.start_date if filters is defined else '' }}&end_date={{ filters.end_date if filters is defined else '' }}&status={{ filters.status if filters is defined else '' }}&client_id={{ filters.client_id if filters is defined else '' }}&limit={{ filters.limit if filters is defined else '' }}">Batch Print</a>
</div>
</form>
</div>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>

222
templates/invoices/print_batch.html

@ -0,0 +1,222 @@
<!doctype html>
<html>
<head>
<title>Batch Print Invoices</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 24px;
color: #111;
}
.top-links {
margin-bottom: 20px;
}
.top-links a {
margin-right: 16px;
}
.batch-header {
margin-bottom: 24px;
}
.invoice-wrap {
page-break-after: always;
margin-bottom: 40px;
padding-bottom: 30px;
border-bottom: 2px solid #ddd;
}
.invoice-wrap:last-child {
page-break-after: auto;
border-bottom: none;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 25px;
}
.title-box h1 {
margin: 0 0 8px 0;
}
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-draft { background: #e5e7eb; color: #111827; }
.status-pending { background: #dbeafe; color: #1d4ed8; }
.status-partial { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #e5e7eb; color: #4b5563; }
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 25px;
}
.info-card {
border: 1px solid #ccc;
padding: 15px;
}
.info-card h3 {
margin-top: 0;
}
.summary-table,
.total-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 25px;
}
.summary-table th,
.summary-table td,
.total-table th,
.total-table td {
border: 1px solid #ccc;
padding: 10px;
text-align: left;
}
.total-table {
max-width: 420px;
margin-left: auto;
}
.notes-box {
border: 1px solid #ccc;
padding: 15px;
white-space: pre-wrap;
margin-top: 20px;
}
.logo {
max-height: 70px;
max-width: 120px;
margin-bottom: 10px;
}
@media print {
.top-links, footer {
display: none !important;
}
body {
margin: 0;
}
}
</style>
</head>
<body>
<div class="top-links">
<a href="/invoices">Back to Invoices</a>
<a href="#" onclick="window.print(); return false;">Print This Page</a>
</div>
<div class="batch-header">
<h1>Batch Invoice Print</h1>
<p>
Filters:
From={{ filters.start_date or 'Any' }},
To={{ filters.end_date or 'Any' }},
Status={{ filters.status or 'All' }},
Client={{ filters.client_id or 'All' }},
Limit={{ filters.limit or 'None' }}
</p>
</div>
{% for invoice in invoices %}
<div class="invoice-wrap">
<div class="header-row">
<div class="title-box">
{% if settings.business_logo_url %}
<img src="{{ settings.business_logo_url }}" class="logo" alt="Logo">
{% endif %}
<h1>Invoice {{ invoice.invoice_number }}</h1>
<span class="status-badge status-{{ invoice.status }}">{{ invoice.status }}</span>
</div>
<div style="text-align:right;">
<strong>{{ settings.business_name or 'OTB Billing' }}</strong><br>
{{ settings.business_tagline or '' }}<br>
{% if settings.business_address %}{{ settings.business_address }}<br>{% endif %}
{% if settings.business_email %}{{ settings.business_email }}<br>{% endif %}
{% if settings.business_phone %}{{ settings.business_phone }}<br>{% endif %}
{% if settings.business_website %}{{ settings.business_website }}{% endif %}
</div>
</div>
<div class="info-grid">
<div class="info-card">
<h3>Bill To</h3>
<strong>{{ invoice.company_name }}</strong><br>
{% if invoice.contact_name %}{{ invoice.contact_name }}<br>{% endif %}
{% if invoice.email %}{{ invoice.email }}<br>{% endif %}
{% if invoice.phone %}{{ invoice.phone }}<br>{% endif %}
Client Code: {{ invoice.client_code }}
</div>
<div class="info-card">
<h3>Invoice Details</h3>
Invoice #: {{ invoice.invoice_number }}<br>
Issued: {{ invoice.issued_at|localtime }}<br>
Due: {{ invoice.due_at|localtime }}<br>
{% if invoice.paid_at %}Paid: {{ invoice.paid_at|localtime }}<br>{% endif %}
Currency: {{ invoice.currency_code }}<br>
{% if settings.tax_number %}{{ settings.tax_label or 'Tax' }} Number: {{ settings.tax_number }}<br>{% endif %}
{% if settings.business_number %}Business Number: {{ settings.business_number }}{% endif %}
</div>
</div>
<table class="summary-table">
<tr>
<th>Service Code</th>
<th>Service</th>
<th>Description</th>
<th>Total</th>
</tr>
<tr>
<td>{{ invoice.service_code or '-' }}</td>
<td>{{ invoice.service_name or '-' }}</td>
<td>{{ invoice.notes or '-' }}</td>
<td>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
<table class="total-table">
<tr>
<th>Subtotal</th>
<td>{{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>{{ settings.tax_label or 'Tax' }}</th>
<td>{{ invoice.tax_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Total</th>
<td><strong>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</strong></td>
</tr>
<tr>
<th>Paid</th>
<td>{{ invoice.amount_paid|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
<tr>
<th>Remaining</th>
<td>{{ (invoice.total_amount - invoice.amount_paid)|money(invoice.currency_code) }} {{ invoice.currency_code }}</td>
</tr>
</table>
{% if settings.payment_terms %}
<div class="notes-box">
<strong>Payment Terms</strong><br><br>
{{ settings.payment_terms }}
</div>
{% endif %}
{% if settings.invoice_footer %}
<div class="notes-box">
<strong>Footer</strong><br><br>
{{ settings.invoice_footer }}
</div>
{% endif %}
</div>
{% endfor %}
{% include "footer.html" %}
</body>
</html>

5
templates/invoices/view.html

@ -108,6 +108,11 @@ body {
</div>
<div class="header-row">
{% if settings.business_logo_url %}
<img src="{{ settings.business_logo_url }}" style="height:70px;margin-bottom:10px;">
{% endif %}
<div class="title-box">
<h1>Invoice {{ invoice.invoice_number }}</h1>
<span class="status-badge status-{{ invoice.status }}">{{ invoice.status }}</span>

1
templates/payments/list.html

@ -53,6 +53,7 @@
<p><a href="/">Home</a></p>
<p><a href="/payments/new">Record Payment</a></p>
<p><a href="/payments/export.csv">Export CSV</a></p>
<table border="1" cellpadding="6">
<tr>

73
templates/reports/revenue.html

@ -0,0 +1,73 @@
<!doctype html>
<html>
<head>
<title>Revenue Report</title>
<style>
body { font-family: Arial, sans-serif; }
.report-grid {
display: grid;
grid-template-columns: repeat(2, minmax(260px, 1fr));
gap: 18px;
max-width: 900px;
}
.card {
border: 1px solid #ccc;
padding: 16px;
}
.card h2 {
margin-top: 0;
margin-bottom: 10px;
}
.value {
font-size: 28px;
font-weight: bold;
}
.action-links a {
margin-right: 16px;
}
</style>
</head>
<body>
<h1>Revenue Report</h1>
<p><a href="/">Home</a></p>
<div class="action-links">
<a href="/reports/revenue.json">Export JSON</a>
<a href="/reports/revenue/print">Print Report Now</a>
</div>
<p>
Frequency: <strong>{{ report.frequency }}</strong><br>
Period: <strong>{{ report.period_label }}</strong>
</p>
<div class="report-grid">
<div class="card">
<h2>Collected (CAD)</h2>
<div class="value">{{ report.collected_cad|money('CAD') }}</div>
</div>
<div class="card">
<h2>Invoices Issued</h2>
<div class="value">{{ report.invoice_count }}</div>
<div>{{ report.invoiced_total|money('CAD') }} CAD total</div>
</div>
<div class="card">
<h2>Outstanding Invoices</h2>
<div class="value">{{ report.outstanding_count }}</div>
<div>{{ report.outstanding_balance|money('CAD') }} CAD outstanding</div>
</div>
<div class="card">
<h2>Overdue Invoices</h2>
<div class="value">{{ report.overdue_count }}</div>
<div>{{ report.overdue_balance|money('CAD') }} CAD overdue</div>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

77
templates/reports/revenue_print.html

@ -0,0 +1,77 @@
<!doctype html>
<html>
<head>
<title>Print Revenue Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 24px; }
.top-links a { margin-right: 16px; }
table {
border-collapse: collapse;
width: 100%;
max-width: 700px;
margin-top: 20px;
}
th, td {
border: 1px solid #ccc;
padding: 10px;
text-align: left;
}
@media print {
.top-links, footer { display: none !important; }
body { margin: 0; }
}
</style>
</head>
<body>
<div class="top-links">
<a href="/reports/revenue">Back to Revenue Report</a>
<a href="#" onclick="window.print(); return false;">Print This Page</a>
</div>
<h1>Revenue Report</h1>
<p>
Frequency: <strong>{{ report.frequency }}</strong><br>
Period: <strong>{{ report.period_label }}</strong><br>
Start (UTC): {{ report.period_start }}<br>
End (UTC): {{ report.period_end }}
</p>
<table>
<tr>
<th>Metric</th>
<th>Value</th>
</tr>
<tr>
<td>Collected (CAD)</td>
<td>{{ report.collected_cad|money('CAD') }} CAD</td>
</tr>
<tr>
<td>Invoices Issued</td>
<td>{{ report.invoice_count }}</td>
</tr>
<tr>
<td>Invoiced Total</td>
<td>{{ report.invoiced_total|money('CAD') }} CAD</td>
</tr>
<tr>
<td>Outstanding Invoices</td>
<td>{{ report.outstanding_count }}</td>
</tr>
<tr>
<td>Outstanding Balance</td>
<td>{{ report.outstanding_balance|money('CAD') }} CAD</td>
</tr>
<tr>
<td>Overdue Invoices</td>
<td>{{ report.overdue_count }}</td>
</tr>
<tr>
<td>Overdue Balance</td>
<td>{{ report.overdue_balance|money('CAD') }} CAD</td>
</tr>
</table>
{% include "footer.html" %}
</body>
</html>

1
templates/services/new.html

@ -91,3 +91,4 @@ Description<br>
</body>
</html>
{% include "footer.html" %}

38
templates/settings.html

@ -36,6 +36,19 @@ textarea { min-height: 90px; }
.save-row {
margin-top: 18px;
}
.logo-preview {
margin: 10px 0 14px 0;
}
.logo-preview img {
max-height: 70px;
max-width: 220px;
border: 1px solid #ccc;
padding: 6px;
background: #fff;
}
small {
color: #444;
}
</style>
</head>
<body>
@ -52,6 +65,16 @@ textarea { min-height: 90px; }
Business Name<br>
<input type="text" name="business_name" value="{{ settings.business_name }}"><br>
Business Logo URL<br>
<input type="text" name="business_logo_url" value="{{ settings.business_logo_url }}"><br>
<small>Example: /static/favicon.png or https://site.com/logo.png</small><br>
{% if settings.business_logo_url %}
<div class="logo-preview">
<img src="{{ settings.business_logo_url }}" alt="Business Logo Preview">
</div>
{% endif %}
Slogan / Tagline<br>
<input type="text" name="business_tagline" value="{{ settings.business_tagline }}"><br>
@ -78,6 +101,13 @@ textarea { min-height: 90px; }
<option value="EGAZ" {% if settings.default_currency == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if settings.default_currency == 'ALT' %}selected{% endif %}>ALT</option>
</select>
Report Frequency<br>
<select name="report_frequency">
<option value="monthly" {% if settings.report_frequency == 'monthly' %}selected{% endif %}>monthly</option>
<option value="quarterly" {% if settings.report_frequency == 'quarterly' %}selected{% endif %}>quarterly</option>
<option value="yearly" {% if settings.report_frequency == 'yearly' %}selected{% endif %}>yearly</option>
</select>
</div>
<div class="card">
@ -110,7 +140,7 @@ textarea { min-height: 90px; }
</div>
<div class="card">
<h2>Email / SMTP</h2>
<h2>Advanced / Email / SMTP</h2>
SMTP Host<br>
<input type="text" name="smtp_host" value="{{ settings.smtp_host }}"><br>
@ -148,13 +178,13 @@ textarea { min-height: 90px; }
<div class="card">
<h2>Notes</h2>
<p>
These settings become the identity and delivery configuration for this installation.
Branding, tax identity, and SMTP values are stored here for this installation.
</p>
<p>
Email sending is not wired yet, but these SMTP settings are stored now so the next step can use them.
Logo can be a local static path like <strong>/static/favicon.png</strong> or a full external/IPFS URL.
</p>
<p>
Tax settings are also stored now so invoice and automation logic can use them later.
Email sending is not wired yet, but these SMTP settings are stored now so the next step can use them.
</p>
</div>
</div>

173
templates/settings.html.bak_logo_layout_fix

@ -0,0 +1,173 @@
<!doctype html>
<html>
<head>
<title>Settings</title>
<style>
body { font-family: Arial, sans-serif; }
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
max-width: 1100px;
}
.card {
border: 1px solid #ccc;
padding: 16px;
}
.card h2 {
margin-top: 0;
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
textarea,
select {
width: 100%;
box-sizing: border-box;
margin-top: 4px;
margin-bottom: 12px;
padding: 8px;
}
textarea { min-height: 90px; }
.checkbox-row {
margin: 8px 0 14px 0;
}
.save-row {
margin-top: 18px;
}
</style>
</head>
<body>
<h1>Settings / Config</h1>
<p><a href="/">Home</a></p>
<form method="post">
<div class="form-grid">
<div class="card">
<h2>Business Identity</h2>
Business Name<br>
Business Logo URL<br>
<input type="text" name="business_logo_url" value="{{ settings.business_logo_url }}"><br>
<small>Example: /static/logo.png or https://site.com/logo.png</small><br><br>
<input type="text" name="business_name" value="{{ settings.business_name }}"><br>
Slogan / Tagline<br>
<input type="text" name="business_tagline" value="{{ settings.business_tagline }}"><br>
Business Email<br>
<input type="email" name="business_email" value="{{ settings.business_email }}"><br>
Business Phone<br>
<input type="text" name="business_phone" value="{{ settings.business_phone }}"><br>
Business Address<br>
<textarea name="business_address">{{ settings.business_address }}</textarea><br>
Website<br>
<input type="text" name="business_website" value="{{ settings.business_website }}"><br>
Business Number / Registration Number<br>
<input type="text" name="business_number" value="{{ settings.business_number }}"><br>
Default Currency<br>
<select name="default_currency">
<option value="CAD" {% if settings.default_currency == 'CAD' %}selected{% endif %}>CAD</option>
<option value="USD" {% if settings.default_currency == 'USD' %}selected{% endif %}>USD</option>
<option value="ETHO" {% if settings.default_currency == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if settings.default_currency == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if settings.default_currency == 'ALT' %}selected{% endif %}>ALT</option>
</select>
</div>
<div class="card">
<h2>Tax Settings</h2>
Local Country<br>
<input type="text" name="local_country" value="{{ settings.local_country }}"><br>
Tax Label<br>
<input type="text" name="tax_label" value="{{ settings.tax_label }}"><br>
Tax Rate (%)<br>
<input type="number" step="0.01" name="tax_rate" value="{{ settings.tax_rate }}"><br>
Tax Number<br>
<input type="text" name="tax_number" value="{{ settings.tax_number }}"><br>
<div class="checkbox-row">
<label>
<input type="checkbox" name="apply_local_tax_only" value="1" {% if settings.apply_local_tax_only == '1' %}checked{% endif %}>
Apply tax only to local clients
</label>
</div>
Payment Terms<br>
<textarea name="payment_terms">{{ settings.payment_terms }}</textarea><br>
Invoice Footer<br>
<textarea name="invoice_footer">{{ settings.invoice_footer }}</textarea><br>
</div>
<div class="card">
<h2>Email / SMTP</h2>
SMTP Host<br>
<input type="text" name="smtp_host" value="{{ settings.smtp_host }}"><br>
SMTP Port<br>
<input type="number" name="smtp_port" value="{{ settings.smtp_port }}"><br>
SMTP Username<br>
<input type="text" name="smtp_user" value="{{ settings.smtp_user }}"><br>
SMTP Password<br>
<input type="password" name="smtp_pass" value="{{ settings.smtp_pass }}"><br>
From Email<br>
<input type="email" name="smtp_from_email" value="{{ settings.smtp_from_email }}"><br>
From Name<br>
<input type="text" name="smtp_from_name" value="{{ settings.smtp_from_name }}"><br>
<div class="checkbox-row">
<label>
<input type="checkbox" name="smtp_use_tls" value="1" {% if settings.smtp_use_tls == '1' %}checked{% endif %}>
Use TLS
</label>
</div>
<div class="checkbox-row">
<label>
<input type="checkbox" name="smtp_use_ssl" value="1" {% if settings.smtp_use_ssl == '1' %}checked{% endif %}>
Use SSL
</label>
</div>
</div>
<div class="card">
<h2>Notes</h2>
<p>
These settings become the identity and delivery configuration for this installation.
</p>
<p>
Email sending is not wired yet, but these SMTP settings are stored now so the next step can use them.
</p>
<p>
Tax settings are also stored now so invoice and automation logic can use them later.
</p>
</div>
</div>
<div class="save-row">
<button type="submit">Save Settings</button>
</div>
</form>
{% include "footer.html" %}
</body>
</html>

26
update_project_state.sh

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="/home/def/otb_billing"
STATE_FILE="$PROJECT_DIR/PROJECT_STATE.md"
if [ ! -f "$STATE_FILE" ]; then
echo "ERROR: $STATE_FILE not found"
exit 1
fi
VERSION="${1:-}"
if [ -z "$VERSION" ]; then
VERSION="$(grep -m1 '^Version:' "$STATE_FILE" | sed 's/^Version:[[:space:]]*//')"
fi
TODAY="$(date +%F)"
sed -i \
-e "s/^Last Updated: .*/Last Updated: $TODAY/" \
-e "s/^Version: .*/Version: $VERSION/" \
"$STATE_FILE"
echo "Updated $STATE_FILE"
echo "Last Updated: $TODAY"
echo "Version: $VERSION"
Loading…
Cancel
Save