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

25
README.md

@ -22,3 +22,28 @@ Infrastructure
- Settings system extended - Settings system extended
- Print layouts stabilized - 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 datetime import datetime, timezone
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from email.message import EmailMessage
from io import BytesIO, StringIO from io import BytesIO, StringIO
import csv import csv
import zipfile import zipfile
import smtplib
from reportlab.lib.pagesizes import letter from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader from reportlab.lib.utils import ImageReader
@ -213,6 +215,7 @@ APP_SETTINGS_DEFAULTS = {
"smtp_from_name": "", "smtp_from_name": "",
"smtp_use_tls": "1", "smtp_use_tls": "1",
"smtp_use_ssl": "0", "smtp_use_ssl": "0",
"report_delivery_email": "",
} }
def ensure_app_settings_table(): def ensure_app_settings_table():
@ -358,6 +361,55 @@ def get_revenue_report_data():
"outstanding_balance": str(to_decimal(outstanding_row["outstanding_balance"])), "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"]) @app.route("/settings", methods=["GET", "POST"])
def settings(): def settings():
ensure_app_settings_table() ensure_app_settings_table()
@ -385,6 +437,139 @@ def revenue_report_print():
report = get_revenue_report_data() report = get_revenue_report_data()
return render_template("reports/revenue_print.html", report=report) 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("/") @app.route("/")
def index(): def index():
refresh_overdue_invoices() 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> </div>
{% endif %} {% endif %}
<h1>{{ app_settings.business_name or 'OTB Billing' }} Dashboard</h1> <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="/clients">Clients</a></p>
<p><a href="/services">Services</a></p> <p><a href="/services">Services</a></p>
<p><a href="/invoices">Invoices</a></p> <p><a href="/invoices">Invoices</a></p>
<p><a href="/payments">Payments</a></p> <p><a href="/payments">Payments</a></p>
<p><a href="/reports/revenue">Revenue Report</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="/settings">Settings / Config</a></p>
<p><a href="/dbtest">DB Test</a></p> <p><a href="/dbtest">DB Test</a></p>

28
templates/invoices/view.html

@ -99,12 +99,27 @@ body {
<body> <body>
<div class="invoice-wrap"> <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"> <div class="top-links screen-only">
<a href="/">Home</a> <a href="/">Home</a>
<a href="/invoices">Back to Invoices</a> <a href="/invoices">Back to Invoices</a>
<a href="/invoices/edit/{{ invoice.id }}">Edit Invoice</a> <a href="/invoices/edit/{{ invoice.id }}">Edit Invoice</a>
<a href="#" onclick="window.print(); return false;">Print</a> <a href="#" onclick="window.print(); return false;">Print</a>
<a href="/invoices/pdf/{{ invoice.id }}">PDF</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>
<div class="header-row"> <div class="header-row">
@ -187,6 +202,19 @@ body {
</tr> </tr>
</table> </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 %} {% if settings.payment_terms %}
<div class="notes-box"> <div class="notes-box">
<strong>Payment Terms</strong><br><br> <strong>Payment Terms</strong><br><br>

18
templates/reports/revenue.html

@ -30,12 +30,30 @@ body { font-family: Arial, sans-serif; }
<body> <body>
<h1>Revenue Report</h1> <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> <p><a href="/">Home</a></p>
<div class="action-links"> <div class="action-links">
<a href="/reports/revenue.json">Export JSON</a> <a href="/reports/revenue.json">Export JSON</a>
<a href="/reports/revenue/print">Print Report Now</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> </div>
<p> <p>

3
templates/settings.html

@ -160,6 +160,9 @@ small {
From Name<br> From Name<br>
<input type="text" name="smtp_from_name" value="{{ settings.smtp_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"> <div class="checkbox-row">
<label> <label>
<input type="checkbox" name="smtp_use_tls" value="1" {% if settings.smtp_use_tls == '1' %}checked{% endif %}> <input type="checkbox" name="smtp_use_tls" value="1" {% if settings.smtp_use_tls == '1' %}checked{% endif %}>

Loading…
Cancel
Save