Browse Source

Release v0.3.1 - billing, exports, reporting, email

main
def 2 weeks ago
parent
commit
9d423c74e3
  1. 169
      PROJECT_STATE.md
  2. 25
      README.md
  3. 2
      VERSION
  4. 185
      backend/app.py
  5. 2342
      backup_pre_accounting_package_2026-03-09/app.py.bak
  6. 43
      backup_pre_accounting_package_2026-03-09/dashboard.html.bak
  7. 326
      backup_pre_email_log_2026-03-09/PROJECT_STATE.md.bak
  8. 2806
      backup_pre_email_log_2026-03-09/app.py.bak
  9. 235
      backup_pre_email_log_2026-03-09/invoices_view.html.bak
  10. 2719
      backup_pre_email_send_2026-03-09/app.py.bak
  11. 44
      backup_pre_email_send_2026-03-09/dashboard.html.bak
  12. 207
      backup_pre_email_send_2026-03-09/invoices_view.html.bak
  13. 73
      backup_pre_email_send_2026-03-09/revenue.html.bak
  14. 199
      backup_pre_email_send_2026-03-09/settings.html.bak
  15. 2342
      backup_restore_email_layer_2026-03-09/app.py.bak
  16. 60
      backup_restore_email_layer_2026-03-09/dashboard.html.bak
  17. 235
      backup_restore_email_layer_2026-03-09/invoices_view.html.bak
  18. 91
      backup_restore_email_layer_2026-03-09/revenue.html.bak
  19. 202
      backup_restore_email_layer_2026-03-09/settings.html.bak
  20. 17
      templates/dashboard.html
  21. 28
      templates/invoices/view.html
  22. 18
      templates/reports/revenue.html
  23. 3
      templates/settings.html

169
PROJECT_STATE.md

@ -1,7 +1,7 @@
# OTB Billing — Project State
Last Updated: 2026-03-09
Version: v0.3-dev
Version: v0.3.1
Project Path: ~/otb_billing
---
@ -63,7 +63,6 @@ This remains a core project rule.
# Current Core Features
## Clients
- create client
- edit client
- list clients
@ -71,7 +70,6 @@ This remains a core project rule.
- client code support
## Services
- create service
- edit service
- list services
@ -79,7 +77,6 @@ This remains a core project rule.
- service status support
## Invoices
- create invoice
- edit invoice
- list invoices
@ -88,9 +85,10 @@ This remains a core project rule.
- invoice PDF download
- invoice lock after payment activity
- invoice statuses
- invoice email sending with PDF attachment
- latest invoice email activity display
Current invoice statuses:
- draft
- pending
- partial
@ -99,7 +97,6 @@ Current invoice statuses:
- cancelled
## Payments
- record payment
- edit payment
- list payments
@ -110,64 +107,74 @@ Current invoice statuses:
- invoice recalculation after payment changes
Current payment statuses:
- confirmed
- reversed
## Credit Ledger
- client credit ledger
- manual credit entries
- client balance color coding
- ledger link visible from client list/edit pages
## Invoice Rendering
- HTML invoice view
- print-friendly layout
- PDF invoice generation
- client details on invoice
- status badge on invoice
- totals, paid, remaining display
---
# Current Settings / Config System
- branding/logo support on HTML and PDF
## Exports
- clients CSV export
- invoices CSV export
- payments CSV export
- filtered invoice CSV export
- filtered invoice PDF ZIP export
- monthly/quarterly/yearly accounting package ZIP export
- revenue report JSON export
## Batch / Print
- filtered batch invoice print page
- print-friendly revenue report
## Reports
- revenue report
- report frequency selector
- JSON report export
- email revenue report JSON
## Settings / Configuration System
Accessible from:
/settings
Stored in database table:
app_settings
## Business Identity Settings
### Business Identity Settings
- business name
- business tagline
- business logo URL
- business email
- business phone
- business address
- business website
- business registration number
## Tax Settings
### Tax Settings
- tax label
- tax rate
- tax number
- local country
- apply local tax only flag
## Invoice Behavior Settings
### Invoice Behavior Settings
- default currency
- invoice footer
- payment terms
- report frequency
## SMTP / Email Settings
### SMTP / Email Settings
- SMTP host
- SMTP port
- SMTP username
@ -176,15 +183,31 @@ app_settings
- SMTP from name
- TLS flag
- SSL flag
Email sending is not yet wired, but config storage is now in place.
- report delivery email
## Email Logging
Stored in:
email_log
Tracks:
- email_type
- invoice_id
- recipient_email
- subject
- status
- error_message
- sent_at
Currently logs:
- invoice email sends
- revenue report email sends
- accounting package email sends
---
# Current Known Good State
Confirmed working:
- dashboard
- clients
- services
@ -192,19 +215,30 @@ Confirmed working:
- auto invoice numbering
- invoice view
- invoice PDF generation
- invoice email with PDF attachment
- invoice email log display
- payment entry
- payment overpayment prevention
- payment reversal / void
- payments list with invoice status and remaining balance
- settings/config page
- business identity shown on invoice view/PDF
- logo display in HTML and PDF
- clients/invoices/payments CSV export
- filtered invoice export
- filtered invoice PDF ZIP export
- batch invoice print
- revenue report
- revenue report JSON export
- revenue report email
- accounting package ZIP export
- accounting package email
---
# Requirements
Current requirements.txt should include:
- Flask
- mysql-connector-python
- reportlab
@ -220,7 +254,6 @@ This file must remain complete so installer-driven deployment works in one shot.
This system is intended to grow into a deployable billing product for small contractors and related service businesses.
Target strengths versus typical SaaS billing tools:
- simpler workflow
- data ownership
- exportability
@ -236,24 +269,20 @@ build something users are happy to use and proud to own.
# Planned Next Features
## Near-Term
- email invoice sending using stored SMTP settings
- branding/logo support
- invoice defaults from settings
- improved tax application logic
- reports/export tools
- batch invoice export / print
- accountant package scheduling / reminders
- client account statement export
- backup/install polish
## Medium-Term
- quote / estimate system
- recurring invoices
- reminder workflows
- improved branding/theme polish
- better installer/update flow
- email resend history view
## Long-Term
- client portal
- role-based access
- accountant/export workflows
@ -274,7 +303,6 @@ If advanced connection settings are ever exposed in UI, they must be clearly mar
# Repository Discipline
For this project going forward:
- keep PROJECT_STATE.md updated
- update README.md with version/build notes
- keep requirements.txt complete
@ -282,8 +310,7 @@ For this project going forward:
- push milestones to git
Example future archive naming:
otb_billing-v0.3.0.zip
otb_billing-v0.3.1.zip
---
@ -298,56 +325,22 @@ 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
Version: v0.3.1
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
Release milestone reached.
System now supports:
- client/service/invoice/payment workflow
- invoice PDF and email
- settings/config with branding and SMTP
- CSV / JSON / ZIP exports
- batch print
- revenue reporting
- accounting package export and email
Deferred to next release:
- email send logging / audit trail

25
README.md

@ -22,3 +22,28 @@ Infrastructure
- Settings system extended
- Print layouts stabilized
## v0.3.1 — 2026-03-09
Milestone release.
Features included
-----------------
- Invoice PDF generation
- Invoice email sending
- Settings/config system
- Branding/logo support
- CSV exports
- Filtered invoice export
- Batch PDF ZIP export
- Batch print
- Revenue report
- Revenue JSON export
- Accounting package ZIP export
- Email delivery for reports and accounting package
Notes
-----
- Core billing/export/report workflow is now operational.
- Email logging/audit trail is planned for a future release.

2
VERSION

@ -1 +1 @@
v0.3.0
v0.3.1

185
backend/app.py

@ -4,10 +4,12 @@ from utils import generate_client_code, generate_service_code
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from decimal import Decimal, InvalidOperation
from email.message import EmailMessage
from io import BytesIO, StringIO
import csv
import zipfile
import smtplib
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
@ -213,6 +215,7 @@ APP_SETTINGS_DEFAULTS = {
"smtp_from_name": "",
"smtp_use_tls": "1",
"smtp_use_ssl": "0",
"report_delivery_email": "",
}
def ensure_app_settings_table():
@ -358,6 +361,55 @@ def get_revenue_report_data():
"outstanding_balance": str(to_decimal(outstanding_row["outstanding_balance"])),
}
def send_configured_email(to_email, subject, body, attachments=None):
settings = get_app_settings()
smtp_host = (settings.get("smtp_host") or "").strip()
smtp_port = int((settings.get("smtp_port") or "587").strip() or "587")
smtp_user = (settings.get("smtp_user") or "").strip()
smtp_pass = (settings.get("smtp_pass") or "").strip()
from_email = (settings.get("smtp_from_email") or settings.get("business_email") or "").strip()
from_name = (settings.get("smtp_from_name") or settings.get("business_name") or "").strip()
use_tls = (settings.get("smtp_use_tls") or "0") == "1"
use_ssl = (settings.get("smtp_use_ssl") or "0") == "1"
if not smtp_host:
raise ValueError("SMTP host is not configured.")
if not from_email:
raise ValueError("From email is not configured.")
if not to_email:
raise ValueError("Recipient email is missing.")
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = f"{from_name} <{from_email}>" if from_name else from_email
msg["To"] = to_email
msg.set_content(body)
for attachment in attachments or []:
filename = attachment["filename"]
mime_type = attachment["mime_type"]
data = attachment["data"]
maintype, subtype = mime_type.split("/", 1)
msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=filename)
if use_ssl:
with smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=30) as server:
if smtp_user:
server.login(smtp_user, smtp_pass)
server.send_message(msg)
else:
with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
server.ehlo()
if use_tls:
server.starttls()
server.ehlo()
if smtp_user:
server.login(smtp_user, smtp_pass)
server.send_message(msg)
@app.route("/settings", methods=["GET", "POST"])
def settings():
ensure_app_settings_table()
@ -385,6 +437,139 @@ def revenue_report_print():
report = get_revenue_report_data()
return render_template("reports/revenue_print.html", report=report)
@app.route("/invoices/email/<int:invoice_id>", methods=["POST"])
def email_invoice(invoice_id):
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT
i.*,
c.client_code,
c.company_name,
c.contact_name,
c.email,
c.phone
FROM invoices i
JOIN clients c ON i.client_id = c.id
WHERE i.id = %s
""", (invoice_id,))
invoice = cursor.fetchone()
conn.close()
if not invoice:
return "Invoice not found", 404
recipient = (invoice.get("email") or "").strip()
if not recipient:
return "Client email is missing for this invoice.", 400
settings = get_app_settings()
with app.test_client() as client:
pdf_response = client.get(f"/invoices/pdf/{invoice_id}")
if pdf_response.status_code != 200:
return "Could not generate invoice PDF for email.", 500
pdf_bytes = pdf_response.data
remaining = to_decimal(invoice.get("total_amount")) - to_decimal(invoice.get("amount_paid"))
subject = f"Invoice {invoice['invoice_number']} from {settings.get('business_name') or 'OTB Billing'}"
body = (
f"Hello {invoice.get('contact_name') or invoice.get('company_name') or ''},\n\n"
f"Please find attached invoice {invoice['invoice_number']}.\n"
f"Total: {to_decimal(invoice.get('total_amount')):.2f} {invoice.get('currency_code', 'CAD')}\n"
f"Remaining: {remaining:.2f} {invoice.get('currency_code', 'CAD')}\n"
f"Due: {fmt_local(invoice.get('due_at'))}\n\n"
f"Thank you,\n"
f"{settings.get('business_name') or 'OTB Billing'}"
)
try:
send_configured_email(
recipient,
subject,
body,
attachments=[{
"filename": f"{invoice['invoice_number']}.pdf",
"mime_type": "application/pdf",
"data": pdf_bytes,
}]
)
return redirect(f"/invoices/view/{invoice_id}?email_sent=1")
except Exception:
return redirect(f"/invoices/view/{invoice_id}?email_failed=1")
@app.route("/reports/revenue/email", methods=["POST"])
def email_revenue_report_json():
settings = get_app_settings()
recipient = (settings.get("report_delivery_email") or settings.get("business_email") or "").strip()
if not recipient:
return "Report delivery email is not configured.", 400
with app.test_client() as client:
json_response = client.get("/reports/revenue.json")
if json_response.status_code != 200:
return "Could not generate revenue report JSON.", 500
report = get_revenue_report_data()
subject = f"Revenue Report {report.get('period_label', '')} from {settings.get('business_name') or 'OTB Billing'}"
body = (
f"Attached is the revenue report JSON for {report.get('period_label', '')}.\n\n"
f"Frequency: {report.get('frequency', '')}\n"
f"Collected CAD: {report.get('collected_cad', '')}\n"
f"Invoices Issued: {report.get('invoice_count', '')}\n"
)
try:
send_configured_email(
recipient,
subject,
body,
attachments=[{
"filename": "revenue_report.json",
"mime_type": "application/json",
"data": json_response.data,
}]
)
return redirect("/reports/revenue?email_sent=1")
except Exception:
return redirect("/reports/revenue?email_failed=1")
@app.route("/reports/accounting-package/email", methods=["POST"])
def email_accounting_package():
settings = get_app_settings()
recipient = (settings.get("report_delivery_email") or settings.get("business_email") or "").strip()
if not recipient:
return "Report delivery email is not configured.", 400
with app.test_client() as client:
zip_response = client.get("/reports/accounting-package.zip")
if zip_response.status_code != 200:
return "Could not generate accounting package ZIP.", 500
subject = f"Accounting Package from {settings.get('business_name') or 'OTB Billing'}"
body = "Attached is the latest accounting package export."
try:
send_configured_email(
recipient,
subject,
body,
attachments=[{
"filename": "accounting_package.zip",
"mime_type": "application/zip",
"data": zip_response.data,
}]
)
return redirect("/?pkg_email=1")
except Exception:
return redirect("/?pkg_email_failed=1")
@app.route("/")
def index():
refresh_overdue_invoices()

2342
backup_pre_accounting_package_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

43
backup_pre_accounting_package_2026-03-09/dashboard.html.bak

@ -0,0 +1,43 @@
<!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="/reports/revenue">Revenue Report</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>

326
backup_pre_email_log_2026-03-09/PROJECT_STATE.md.bak

@ -0,0 +1,326 @@
# OTB Billing — Project State
Last Updated: 2026-03-09
Version: v0.3.1
Project Path: ~/otb_billing
---
# Project Purpose
OTB Billing is a contractor-focused billing system designed to be:
- self-hosted
- portable
- database-backed
- deployable on fresh Linux systems
- suitable for managed hosting or client-installed deployments
The system is being built as a practical alternative to overly restrictive SaaS billing tools, with emphasis on ownership, simplicity, and contractor workflow.
Tagline direction:
By a contractor, for contractors
---
# Current Stack
Backend:
Flask
Database:
MariaDB
PDF Engine:
ReportLab
Primary Port:
5050
Dependencies file:
requirements.txt
---
# Deployment Philosophy
OTB Billing must remain a deployable product, not just a dev-only app.
Target install model:
fresh server
→ installer runs
→ dependencies install
→ MariaDB setup
→ schema setup
→ app launches
This remains a core project rule.
---
# Current Core Features
## Clients
- create client
- edit client
- list clients
- status field
- client code support
## Services
- create service
- edit service
- list services
- service code support
- service status support
## Invoices
- create invoice
- edit invoice
- list invoices
- automatic invoice numbering
- invoice print view
- invoice PDF download
- invoice lock after payment activity
- invoice statuses
- invoice email sending with PDF attachment
- latest invoice email activity display
Current invoice statuses:
- draft
- pending
- partial
- paid
- overdue
- cancelled
## Payments
- record payment
- edit payment
- list payments
- overpayment guard on new payment
- overpayment guard on payment edit
- payment status display
- payment void / reversal workflow
- invoice recalculation after payment changes
Current payment statuses:
- confirmed
- reversed
## Credit Ledger
- client credit ledger
- manual credit entries
- client balance color coding
- ledger link visible from client list/edit pages
## Invoice Rendering
- HTML invoice view
- print-friendly layout
- PDF invoice generation
- client details on invoice
- status badge on invoice
- totals, paid, remaining display
- branding/logo support on HTML and PDF
## Exports
- clients CSV export
- invoices CSV export
- payments CSV export
- filtered invoice CSV export
- filtered invoice PDF ZIP export
- monthly/quarterly/yearly accounting package ZIP export
- revenue report JSON export
## Batch / Print
- filtered batch invoice print page
- print-friendly revenue report
## Reports
- revenue report
- report frequency selector
- JSON report export
- email revenue report JSON
## Settings / Configuration System
Accessible from:
/settings
Stored in database table:
app_settings
### Business Identity Settings
- business name
- business tagline
- business logo URL
- business email
- business phone
- business address
- business website
- business registration number
### Tax Settings
- tax label
- tax rate
- tax number
- local country
- apply local tax only flag
### Invoice Behavior Settings
- default currency
- invoice footer
- payment terms
- report frequency
### SMTP / Email Settings
- SMTP host
- SMTP port
- SMTP username
- SMTP password
- SMTP from email
- SMTP from name
- TLS flag
- SSL flag
- report delivery email
## Email Logging
Stored in:
email_log
Tracks:
- email_type
- invoice_id
- recipient_email
- subject
- status
- error_message
- sent_at
Currently logs:
- invoice email sends
- revenue report email sends
- accounting package email sends
---
# Current Known Good State
Confirmed working:
- dashboard
- clients
- services
- invoice creation
- auto invoice numbering
- invoice view
- invoice PDF generation
- invoice email with PDF attachment
- invoice email log display
- payment entry
- payment overpayment prevention
- payment reversal / void
- payments list with invoice status and remaining balance
- settings/config page
- business identity shown on invoice view/PDF
- logo display in HTML and PDF
- clients/invoices/payments CSV export
- filtered invoice export
- filtered invoice PDF ZIP export
- batch invoice print
- revenue report
- revenue report JSON export
- revenue report email
- accounting package ZIP export
- accounting package email
---
# Requirements
Current requirements.txt should include:
- Flask
- mysql-connector-python
- reportlab
- python-dateutil
- pytz
This file must remain complete so installer-driven deployment works in one shot.
---
# Business / Product Direction
This system is intended to grow into a deployable billing product for small contractors and related service businesses.
Target strengths versus typical SaaS billing tools:
- simpler workflow
- data ownership
- exportability
- portability
- contractor-first design
- no hostage-style software design
Long-term success goal:
build something users are happy to use and proud to own.
---
# Planned Next Features
## Near-Term
- invoice defaults from settings
- improved tax application logic
- accountant package scheduling / reminders
- client account statement export
- backup/install polish
## Medium-Term
- quote / estimate system
- recurring invoices
- reminder workflows
- better installer/update flow
- email resend history view
## Long-Term
- client portal
- role-based access
- accountant/export workflows
- job-tracking integration with related contractor platform modules
---
# Advanced Settings Direction
Business identity and SMTP belong in settings UI.
Database credentials should remain installer/config-file driven, not casually editable in standard UI.
If advanced connection settings are ever exposed in UI, they must be clearly marked as dangerous / advanced and should avoid redisplaying stored passwords.
---
# Repository Discipline
For this project going forward:
- keep PROJECT_STATE.md updated
- update README.md with version/build notes
- keep requirements.txt complete
- make full ZIP backup on version bumps
- push milestones to git
Example future archive naming:
otb_billing-v0.3.1.zip
---
# Restart / Run Notes
Development run method:
cd ~/otb_billing
python3 backend/app.py
During active development, run in a visible terminal so logs stay visible.
Do not rely on hidden/background launch during normal debug workflow.

2806
backup_pre_email_log_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

235
backup_pre_email_log_2026-03-09/invoices_view.html.bak

@ -0,0 +1,235 @@
<!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">
{% if request.args.get('email_sent') == '1' %}
<div style="border:1px solid #166534;background:#dcfce7;padding:10px;margin-bottom:15px;">
Invoice email sent successfully.
</div>
{% endif %}
{% if request.args.get('email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;">
Invoice email failed. Check SMTP settings or server log.
</div>
{% endif %}
<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>
{% if invoice.email %}
<form method="post" action="/invoices/email/{{ invoice.id }}" style="display:inline;">
<button type="submit">Send Email</button>
</form>
{% endif %}
</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>
</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 latest_email_log %}
<div class="notes-box">
<strong>Latest Email Activity</strong><br><br>
Status: {{ latest_email_log.status }}<br>
Recipient: {{ latest_email_log.recipient_email }}<br>
Subject: {{ latest_email_log.subject }}<br>
Sent At: {{ latest_email_log.sent_at|localtime }}<br>
{% if latest_email_log.error_message %}
Error: {{ latest_email_log.error_message }}
{% endif %}
</div>
{% endif %}
{% 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>

2719
backup_pre_email_send_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

44
backup_pre_email_send_2026-03-09/dashboard.html.bak

@ -0,0 +1,44 @@
<!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="/reports/revenue">Revenue Report</a></p>
<p><a href="/reports/accounting-package.zip">Monthly Accounting Package</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>

207
backup_pre_email_send_2026-03-09/invoices_view.html.bak

@ -0,0 +1,207 @@
<!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">
{% 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>
</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>

73
backup_pre_email_send_2026-03-09/revenue.html.bak

@ -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>

199
backup_pre_email_send_2026-03-09/settings.html.bak

@ -0,0 +1,199 @@
<!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>
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">
<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>

2342
backup_restore_email_layer_2026-03-09/app.py.bak

File diff suppressed because it is too large Load Diff

60
backup_restore_email_layer_2026-03-09/dashboard.html.bak

@ -0,0 +1,60 @@
<!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>
{% if request.args.get('pkg_email') == '1' %}
<div style="border:1px solid #166534;background:#dcfce7;padding:10px;margin-bottom:15px;max-width:900px;">
Accounting package emailed successfully.
</div>
{% endif %}
{% if request.args.get('pkg_email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;max-width:900px;">
Accounting package email failed. Check SMTP settings or server log.
</div>
{% endif %}
{% if request.args.get('pkg_email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;max-width:900px;">
Accounting package email failed. Check SMTP settings or server log.
</div>
{% endif %}
<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="/reports/accounting-package.zip">Monthly Accounting Package</a></p>
<form method="post" action="/reports/accounting-package/email" style="margin:0 0 16px 0;"><button type="submit">Email Accounting Package</button></form>
<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>

235
backup_restore_email_layer_2026-03-09/invoices_view.html.bak

@ -0,0 +1,235 @@
<!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">
{% if request.args.get('email_sent') == '1' %}
<div style="border:1px solid #166534;background:#dcfce7;padding:10px;margin-bottom:15px;">
Invoice email sent successfully.
</div>
{% endif %}
{% if request.args.get('email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;">
Invoice email failed. Check SMTP settings or server log.
</div>
{% endif %}
<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>
{% if invoice.email %}
<form method="post" action="/invoices/email/{{ invoice.id }}" style="display:inline;">
<button type="submit">Send Email</button>
</form>
{% endif %}
</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>
</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 latest_email_log %}
<div class="notes-box">
<strong>Latest Email Activity</strong><br><br>
Status: {{ latest_email_log.status }}<br>
Recipient: {{ latest_email_log.recipient_email }}<br>
Subject: {{ latest_email_log.subject }}<br>
Sent At: {{ latest_email_log.sent_at|localtime }}<br>
{% if latest_email_log.error_message %}
Error: {{ latest_email_log.error_message }}
{% endif %}
</div>
{% endif %}
{% 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>

91
backup_restore_email_layer_2026-03-09/revenue.html.bak

@ -0,0 +1,91 @@
<!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>
{% if request.args.get('email_sent') == '1' %}
<div style="border:1px solid #166534;background:#dcfce7;padding:10px;margin-bottom:15px;max-width:900px;">
Revenue report JSON emailed successfully.
</div>
{% endif %}
{% if request.args.get('email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;max-width:900px;">
Revenue report email failed. Check SMTP settings or server log.
</div>
{% endif %}
{% if request.args.get('email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;max-width:900px;">
Revenue report email failed. Check SMTP settings or server log.
</div>
{% endif %}
<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>
<form method="post" action="/reports/revenue/email" style="display:inline;">
<button type="submit">Email JSON Report</button>
</form>
</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>

202
backup_restore_email_layer_2026-03-09/settings.html.bak

@ -0,0 +1,202 @@
<!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>
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">
<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>
Report / Accounting Delivery Email<br>
<input type="email" name="report_delivery_email" value="{{ settings.report_delivery_email }}"><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>

17
templates/dashboard.html

@ -12,12 +12,29 @@
</div>
{% endif %}
<h1>{{ app_settings.business_name or 'OTB Billing' }} Dashboard</h1>
{% if request.args.get('pkg_email') == '1' %}
<div style="border:1px solid #166534;background:#dcfce7;padding:10px;margin-bottom:15px;max-width:900px;">
Accounting package emailed successfully.
</div>
{% endif %}
{% if request.args.get('pkg_email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;max-width:900px;">
Accounting package email failed. Check SMTP settings or server log.
</div>
{% endif %}
{% if request.args.get('pkg_email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;max-width:900px;">
Accounting package email failed. Check SMTP settings or server log.
</div>
{% endif %}
<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="/reports/accounting-package.zip">Monthly Accounting Package</a></p>
<form method="post" action="/reports/accounting-package/email" style="margin:0 0 16px 0;"><button type="submit">Email Accounting Package</button></form>
<p><a href="/settings">Settings / Config</a></p>
<p><a href="/dbtest">DB Test</a></p>

28
templates/invoices/view.html

@ -99,12 +99,27 @@ body {
<body>
<div class="invoice-wrap">
{% if request.args.get('email_sent') == '1' %}
<div style="border:1px solid #166534;background:#dcfce7;padding:10px;margin-bottom:15px;">
Invoice email sent successfully.
</div>
{% endif %}
{% if request.args.get('email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;">
Invoice email failed. Check SMTP settings or server log.
</div>
{% endif %}
<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>
{% if invoice.email %}
<form method="post" action="/invoices/email/{{ invoice.id }}" style="display:inline;">
<button type="submit">Send Email</button>
</form>
{% endif %}
</div>
<div class="header-row">
@ -187,6 +202,19 @@ body {
</tr>
</table>
{% if latest_email_log %}
<div class="notes-box">
<strong>Latest Email Activity</strong><br><br>
Status: {{ latest_email_log.status }}<br>
Recipient: {{ latest_email_log.recipient_email }}<br>
Subject: {{ latest_email_log.subject }}<br>
Sent At: {{ latest_email_log.sent_at|localtime }}<br>
{% if latest_email_log.error_message %}
Error: {{ latest_email_log.error_message }}
{% endif %}
</div>
{% endif %}
{% if settings.payment_terms %}
<div class="notes-box">
<strong>Payment Terms</strong><br><br>

18
templates/reports/revenue.html

@ -30,12 +30,30 @@ body { font-family: Arial, sans-serif; }
<body>
<h1>Revenue Report</h1>
{% if request.args.get('email_sent') == '1' %}
<div style="border:1px solid #166534;background:#dcfce7;padding:10px;margin-bottom:15px;max-width:900px;">
Revenue report JSON emailed successfully.
</div>
{% endif %}
{% if request.args.get('email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;max-width:900px;">
Revenue report email failed. Check SMTP settings or server log.
</div>
{% endif %}
{% if request.args.get('email_failed') == '1' %}
<div style="border:1px solid #991b1b;background:#fee2e2;padding:10px;margin-bottom:15px;max-width:900px;">
Revenue report email failed. Check SMTP settings or server log.
</div>
{% endif %}
<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>
<form method="post" action="/reports/revenue/email" style="display:inline;">
<button type="submit">Email JSON Report</button>
</form>
</div>
<p>

3
templates/settings.html

@ -160,6 +160,9 @@ small {
From Name<br>
<input type="text" name="smtp_from_name" value="{{ settings.smtp_from_name }}"><br>
Report / Accounting Delivery Email<br>
<input type="email" name="report_delivery_email" value="{{ settings.report_delivery_email }}"><br>
<div class="checkbox-row">
<label>
<input type="checkbox" name="smtp_use_tls" value="1" {% if settings.smtp_use_tls == '1' %}checked{% endif %}>

Loading…
Cancel
Save