23 changed files with 12282 additions and 89 deletions
File diff suppressed because it is too large
Load Diff
@ -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> |
||||
@ -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. |
||||
File diff suppressed because it is too large
Load Diff
@ -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> |
||||
File diff suppressed because it is too large
Load Diff
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
File diff suppressed because it is too large
Load Diff
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
Loading…
Reference in new issue