122 changed files with 198049 additions and 37279 deletions
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,51 @@
|
||||
cd /home/def/otb_billing || exit 1 |
||||
set -e |
||||
|
||||
NEWVER="v0.5.1" |
||||
STAMP="$(date '+%Y-%m-%d %H:%M:%S')" |
||||
ZIPNAME="otb_billing-${NEWVER}.zip" |
||||
|
||||
echo "===== git status =====" |
||||
git status --short || true |
||||
|
||||
echo "===== update VERSION =====" |
||||
echo "${NEWVER}" > VERSION |
||||
|
||||
echo "===== update README.md =====" |
||||
cp README.md "README.md.bak.${NEWVER}" |
||||
|
||||
python3 <<'PY' |
||||
from pathlib import Path |
||||
from datetime import datetime |
||||
|
||||
p = Path("README.md") |
||||
text = p.read_text() |
||||
|
||||
entry = f"""## {datetime.now().strftime('%Y-%m-%d')} — v0.5.1 |
||||
|
||||
- Fixed crypto payment email auto-send failure |
||||
- Replaced internal PDF generator call with route-based PDF fetch |
||||
- Restored PDF attachments in payment emails |
||||
- Improved Payments Applied layout in invoice PDF (multi-line details + rate display) |
||||
- Stabilized send_payment_received_email() (removed debug raise, safe failure handling) |
||||
|
||||
""" |
||||
|
||||
p.write_text(entry + "\n" + text) |
||||
print("OK: README updated") |
||||
PY |
||||
|
||||
echo "===== git add =====" |
||||
git add . |
||||
|
||||
echo "===== git commit =====" |
||||
git commit -m "v0.5.1 - crypto email fix, PDF attachment fix, payment layout improvements" |
||||
|
||||
echo "===== git push =====" |
||||
git push |
||||
|
||||
echo "===== build zip =====" |
||||
cd /home/def || exit 1 |
||||
zip -r "${ZIPNAME}" otb_billing >/dev/null |
||||
|
||||
echo "ZIP CREATED: /home/def/${ZIPNAME}" |
||||
@ -1,43 +0,0 @@
|
||||
<!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> |
||||
File diff suppressed because it is too large
Load Diff
@ -1,143 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Invoices</title> |
||||
<style> |
||||
.status-badge { |
||||
display: inline-block; |
||||
padding: 3px 8px; |
||||
border-radius: 999px; |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
letter-spacing: 0.03em; |
||||
} |
||||
.status-draft { background: #e5e7eb; color: #111827; } |
||||
.status-pending { background: #dbeafe; color: #1d4ed8; } |
||||
.status-partial { background: #fef3c7; color: #92400e; } |
||||
.status-paid { background: #dcfce7; color: #166534; } |
||||
.status-overdue { background: #fee2e2; color: #991b1b; } |
||||
.status-cancelled { background: #e5e7eb; color: #4b5563; } |
||||
|
||||
.locked-note { |
||||
color: #92400e; |
||||
font-weight: bold; |
||||
} |
||||
|
||||
.filter-box { |
||||
border: 1px solid #ccc; |
||||
padding: 12px; |
||||
margin: 14px 0; |
||||
max-width: 950px; |
||||
} |
||||
.filter-row { |
||||
display: flex; |
||||
gap: 14px; |
||||
align-items: end; |
||||
flex-wrap: wrap; |
||||
} |
||||
.filter-row div { |
||||
display: flex; |
||||
flex-direction: column; |
||||
} |
||||
input[type="date"], |
||||
select { |
||||
padding: 6px; |
||||
min-width: 150px; |
||||
} |
||||
</style> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<h1>Invoices</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/invoices/new">Create Invoice</a></p> |
||||
|
||||
<div class="filter-box"> |
||||
<form method="get" action="/invoices"> |
||||
<div class="filter-row"> |
||||
<div> |
||||
<label for="start_date">Issued From</label> |
||||
<input type="date" id="start_date" name="start_date" value="{{ filters.start_date if filters is defined else '' }}"> |
||||
</div> |
||||
|
||||
<div> |
||||
<label for="end_date">Issued To</label> |
||||
<input type="date" id="end_date" name="end_date" value="{{ filters.end_date if filters is defined else '' }}"> |
||||
</div> |
||||
|
||||
<div> |
||||
<label for="status">Status</label> |
||||
<select id="status" name="status"> |
||||
<option value="" {% if not filters.status %}selected{% endif %}>All</option> |
||||
<option value="draft" {% if filters.status == 'draft' %}selected{% endif %}>draft</option> |
||||
<option value="pending" {% if filters.status == 'pending' %}selected{% endif %}>pending</option> |
||||
<option value="partial" {% if filters.status == 'partial' %}selected{% endif %}>partial</option> |
||||
<option value="paid" {% if filters.status == 'paid' %}selected{% endif %}>paid</option> |
||||
<option value="overdue" {% if filters.status == 'overdue' %}selected{% endif %}>overdue</option> |
||||
<option value="cancelled" {% if filters.status == 'cancelled' %}selected{% endif %}>cancelled</option> |
||||
</select> |
||||
</div> |
||||
|
||||
<div> |
||||
<button type="submit">Apply Filters</button> |
||||
</div> |
||||
|
||||
<div> |
||||
<a href="/invoices">Clear Filters</a> |
||||
</div> |
||||
|
||||
<div> |
||||
<a href="/invoices/export.csv?start_date={{ filters.start_date if filters is defined else '' }}&end_date={{ filters.end_date if filters is defined else '' }}&status={{ filters.status if filters is defined else '' }}">Export Filtered CSV</a> |
||||
</div> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
|
||||
<table border="1" cellpadding="6"> |
||||
<tr> |
||||
<th>ID</th> |
||||
<th>Invoice</th> |
||||
<th>Client</th> |
||||
<th>Currency</th> |
||||
<th>Total</th> |
||||
<th>Paid</th> |
||||
<th>Remaining</th> |
||||
<th>Status</th> |
||||
<th>Issued</th> |
||||
<th>Due</th> |
||||
<th>Actions</th> |
||||
</tr> |
||||
|
||||
{% for i in invoices %} |
||||
<tr> |
||||
<td>{{ i.id }}</td> |
||||
<td>{{ i.invoice_number }}</td> |
||||
<td>{{ i.client_code }} - {{ i.company_name }}</td> |
||||
<td>{{ i.currency_code }}</td> |
||||
<td>{{ i.total_amount|money(i.currency_code) }}</td> |
||||
<td>{{ i.amount_paid|money(i.currency_code) }}</td> |
||||
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td> |
||||
<td> |
||||
<span class="status-badge status-{{ i.status }}">{{ i.status }}</span> |
||||
</td> |
||||
<td>{{ i.issued_at|localtime }}</td> |
||||
<td>{{ i.due_at|localtime }}</td> |
||||
<td> |
||||
<a href="/invoices/view/{{ i.id }}">View</a> | |
||||
<a href="/invoices/pdf/{{ i.id }}">PDF</a> | |
||||
<a href="/invoices/edit/{{ i.id }}">Edit</a> |
||||
{% if i.payment_count > 0 %} |
||||
<span class="locked-note">(Locked)</span> |
||||
{% endif %} |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
|
||||
</table> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
File diff suppressed because it is too large
Load Diff
@ -1,169 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Invoices</title> |
||||
<style> |
||||
.status-badge { |
||||
display: inline-block; |
||||
padding: 3px 8px; |
||||
border-radius: 999px; |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
letter-spacing: 0.03em; |
||||
} |
||||
.status-draft { background: #e5e7eb; color: #111827; } |
||||
.status-pending { background: #dbeafe; color: #1d4ed8; } |
||||
.status-partial { background: #fef3c7; color: #92400e; } |
||||
.status-paid { background: #dcfce7; color: #166534; } |
||||
.status-overdue { background: #fee2e2; color: #991b1b; } |
||||
.status-cancelled { background: #e5e7eb; color: #4b5563; } |
||||
|
||||
.locked-note { |
||||
color: #92400e; |
||||
font-weight: bold; |
||||
} |
||||
|
||||
.filter-box { |
||||
border: 1px solid #ccc; |
||||
padding: 12px; |
||||
margin: 14px 0; |
||||
max-width: 1100px; |
||||
} |
||||
.filter-row { |
||||
display: flex; |
||||
gap: 14px; |
||||
align-items: end; |
||||
flex-wrap: wrap; |
||||
} |
||||
.filter-row div { |
||||
display: flex; |
||||
flex-direction: column; |
||||
} |
||||
input[type="date"], |
||||
input[type="number"], |
||||
select { |
||||
padding: 6px; |
||||
min-width: 150px; |
||||
} |
||||
.action-links { |
||||
margin-top: 10px; |
||||
} |
||||
.action-links a { |
||||
margin-right: 18px; |
||||
} |
||||
</style> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<h1>Invoices</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/invoices/new">Create Invoice</a></p> |
||||
|
||||
<div class="filter-box"> |
||||
<form method="get" action="/invoices"> |
||||
<div class="filter-row"> |
||||
<div> |
||||
<label for="start_date">Issued From</label> |
||||
<input type="date" id="start_date" name="start_date" value="{{ filters.start_date if filters is defined else '' }}"> |
||||
</div> |
||||
|
||||
<div> |
||||
<label for="end_date">Issued To</label> |
||||
<input type="date" id="end_date" name="end_date" value="{{ filters.end_date if filters is defined else '' }}"> |
||||
</div> |
||||
|
||||
<div> |
||||
<label for="status">Status</label> |
||||
<select id="status" name="status"> |
||||
<option value="" {% if not filters.status %}selected{% endif %}>All</option> |
||||
<option value="draft" {% if filters.status == 'draft' %}selected{% endif %}>draft</option> |
||||
<option value="pending" {% if filters.status == 'pending' %}selected{% endif %}>pending</option> |
||||
<option value="partial" {% if filters.status == 'partial' %}selected{% endif %}>partial</option> |
||||
<option value="paid" {% if filters.status == 'paid' %}selected{% endif %}>paid</option> |
||||
<option value="overdue" {% if filters.status == 'overdue' %}selected{% endif %}>overdue</option> |
||||
<option value="cancelled" {% if filters.status == 'cancelled' %}selected{% endif %}>cancelled</option> |
||||
</select> |
||||
</div> |
||||
|
||||
<div> |
||||
<label for="client_id">Client</label> |
||||
<select id="client_id" name="client_id"> |
||||
<option value="" {% if not filters.client_id %}selected{% endif %}>All Clients</option> |
||||
{% for c in clients %} |
||||
<option value="{{ c.id }}" {% if filters.client_id == (c.id|string) %}selected{% endif %}> |
||||
{{ c.client_code }} - {{ c.company_name }} |
||||
</option> |
||||
{% endfor %} |
||||
</select> |
||||
</div> |
||||
|
||||
<div> |
||||
<label for="limit">Limit</label> |
||||
<input type="number" id="limit" name="limit" min="1" step="1" value="{{ filters.limit if filters is defined else '' }}"> |
||||
</div> |
||||
|
||||
<div> |
||||
<button type="submit">Apply Filters</button> |
||||
</div> |
||||
|
||||
<div> |
||||
<a href="/invoices">Clear Filters</a> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="action-links"> |
||||
<a href="/invoices/export.csv?start_date={{ filters.start_date if filters is defined else '' }}&end_date={{ filters.end_date if filters is defined else '' }}&status={{ filters.status if filters is defined else '' }}&client_id={{ filters.client_id if filters is defined else '' }}&limit={{ filters.limit if filters is defined else '' }}">Export Filtered CSV</a> |
||||
|
||||
<a href="/invoices/export-pdf.zip?start_date={{ filters.start_date if filters is defined else '' }}&end_date={{ filters.end_date if filters is defined else '' }}&status={{ filters.status if filters is defined else '' }}&client_id={{ filters.client_id if filters is defined else '' }}&limit={{ filters.limit if filters is defined else '' }}">Export Filtered PDF ZIP</a> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
|
||||
<table border="1" cellpadding="6"> |
||||
<tr> |
||||
<th>ID</th> |
||||
<th>Invoice</th> |
||||
<th>Client</th> |
||||
<th>Currency</th> |
||||
<th>Total</th> |
||||
<th>Paid</th> |
||||
<th>Remaining</th> |
||||
<th>Status</th> |
||||
<th>Issued</th> |
||||
<th>Due</th> |
||||
<th>Actions</th> |
||||
</tr> |
||||
|
||||
{% for i in invoices %} |
||||
<tr> |
||||
<td>{{ i.id }}</td> |
||||
<td>{{ i.invoice_number }}</td> |
||||
<td>{{ i.client_code }} - {{ i.company_name }}</td> |
||||
<td>{{ i.currency_code }}</td> |
||||
<td>{{ i.total_amount|money(i.currency_code) }}</td> |
||||
<td>{{ i.amount_paid|money(i.currency_code) }}</td> |
||||
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td> |
||||
<td> |
||||
<span class="status-badge status-{{ i.status }}">{{ i.status }}</span> |
||||
</td> |
||||
<td>{{ i.issued_at|localtime }}</td> |
||||
<td>{{ i.due_at|localtime }}</td> |
||||
<td> |
||||
<a href="/invoices/view/{{ i.id }}">View</a> | |
||||
<a href="/invoices/pdf/{{ i.id }}">PDF</a> | |
||||
<a href="/invoices/edit/{{ i.id }}">Edit</a> |
||||
{% if i.payment_count > 0 %} |
||||
<span class="locked-note">(Locked)</span> |
||||
{% endif %} |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
|
||||
</table> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
File diff suppressed because it is too large
Load Diff
@ -1,49 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Clients</title> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<h1>Clients</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/clients/new">Add Client</a></p> |
||||
|
||||
<table border="1" cellpadding="6"> |
||||
<tr> |
||||
<th>ID</th> |
||||
<th>Code</th> |
||||
<th>Company</th> |
||||
<th>Contact</th> |
||||
<th>Email</th> |
||||
<th>Phone</th> |
||||
<th>Status</th> |
||||
<th>Actions</th> |
||||
</tr> |
||||
|
||||
{% for c in clients %} |
||||
<tr> |
||||
<td>{{ c.id }}</td> |
||||
<td>{{ c.client_code }}</td> |
||||
<td>{{ c.company_name }}</td> |
||||
<td>{{ c.contact_name }}</td> |
||||
<td>{{ c.email }}</td> |
||||
<td>{{ c.phone }}</td> |
||||
<td>{{ c.status }}</td> |
||||
<td> |
||||
<a href="/clients/edit/{{ c.id }}">Edit</a> | |
||||
<a href="/credits/{{ c.id }}" |
||||
style="{% if c.credit_balance > 0 %}color: green;{% elif c.credit_balance < 0 %}color: red;{% else %}color: blue;{% endif %}"> |
||||
Ledger ({{ c.credit_balance|money('CAD') }}) |
||||
</a> |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
|
||||
</table> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
@ -1,80 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Invoices</title> |
||||
<style> |
||||
.status-badge { |
||||
display: inline-block; |
||||
padding: 3px 8px; |
||||
border-radius: 999px; |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
letter-spacing: 0.03em; |
||||
} |
||||
.status-draft { background: #e5e7eb; color: #111827; } |
||||
.status-pending { background: #dbeafe; color: #1d4ed8; } |
||||
.status-partial { background: #fef3c7; color: #92400e; } |
||||
.status-paid { background: #dcfce7; color: #166534; } |
||||
.status-overdue { background: #fee2e2; color: #991b1b; } |
||||
.status-cancelled { background: #e5e7eb; color: #4b5563; } |
||||
|
||||
.locked-note { |
||||
color: #92400e; |
||||
font-weight: bold; |
||||
} |
||||
</style> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<h1>Invoices</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/invoices/new">Create Invoice</a></p> |
||||
|
||||
<table border="1" cellpadding="6"> |
||||
<tr> |
||||
<th>ID</th> |
||||
<th>Invoice</th> |
||||
<th>Client</th> |
||||
<th>Currency</th> |
||||
<th>Total</th> |
||||
<th>Paid</th> |
||||
<th>Remaining</th> |
||||
<th>Status</th> |
||||
<th>Issued</th> |
||||
<th>Due</th> |
||||
<th>Actions</th> |
||||
</tr> |
||||
|
||||
{% for i in invoices %} |
||||
<tr> |
||||
<td>{{ i.id }}</td> |
||||
<td>{{ i.invoice_number }}</td> |
||||
<td>{{ i.client_code }} - {{ i.company_name }}</td> |
||||
<td>{{ i.currency_code }}</td> |
||||
<td>{{ i.total_amount|money(i.currency_code) }}</td> |
||||
<td>{{ i.amount_paid|money(i.currency_code) }}</td> |
||||
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td> |
||||
<td> |
||||
<span class="status-badge status-{{ i.status }}">{{ i.status }}</span> |
||||
</td> |
||||
<td>{{ i.issued_at|localtime }}</td> |
||||
<td>{{ i.due_at|localtime }}</td> |
||||
<td> |
||||
<a href="/invoices/view/{{ i.id }}">View</a> | |
||||
<a href="/invoices/pdf/{{ i.id }}">PDF</a> | |
||||
<a href="/invoices/edit/{{ i.id }}">Edit</a> |
||||
{% if i.payment_count > 0 %} |
||||
<span class="locked-note">(Locked)</span> |
||||
{% endif %} |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
|
||||
</table> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
@ -1,102 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Payments</title> |
||||
<style> |
||||
.status-badge { |
||||
display: inline-block; |
||||
padding: 3px 8px; |
||||
border-radius: 999px; |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
letter-spacing: 0.03em; |
||||
} |
||||
.status-confirmed { background: #dcfce7; color: #166534; } |
||||
.status-reversed { background: #fee2e2; color: #991b1b; } |
||||
|
||||
.invoice-badge { |
||||
display: inline-block; |
||||
padding: 3px 8px; |
||||
border-radius: 999px; |
||||
font-size: 11px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
letter-spacing: 0.03em; |
||||
} |
||||
.invoice-draft { background: #e5e7eb; color: #111827; } |
||||
.invoice-pending { background: #dbeafe; color: #1d4ed8; } |
||||
.invoice-partial { background: #fef3c7; color: #92400e; } |
||||
.invoice-paid { background: #dcfce7; color: #166534; } |
||||
.invoice-overdue { background: #fee2e2; color: #991b1b; } |
||||
.invoice-cancelled { background: #e5e7eb; color: #4b5563; } |
||||
|
||||
.inline-form { |
||||
display: inline; |
||||
margin: 0; |
||||
} |
||||
.void-btn { |
||||
background: #991b1b; |
||||
color: white; |
||||
border: 0; |
||||
padding: 4px 8px; |
||||
cursor: pointer; |
||||
} |
||||
.void-btn:hover { |
||||
opacity: 0.9; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
|
||||
<h1>Payments</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/payments/new">Record Payment</a></p> |
||||
|
||||
<table border="1" cellpadding="6"> |
||||
<tr> |
||||
<th>ID</th> |
||||
<th>Invoice</th> |
||||
<th>Client</th> |
||||
<th>Method</th> |
||||
<th>Currency</th> |
||||
<th>Amount</th> |
||||
<th>CAD Value</th> |
||||
<th>Payment Status</th> |
||||
<th>Invoice Status</th> |
||||
<th>Remaining</th> |
||||
<th>Received</th> |
||||
<th>Actions</th> |
||||
</tr> |
||||
|
||||
{% for p in payments %} |
||||
<tr> |
||||
<td>{{ p.id }}</td> |
||||
<td>{{ p.invoice_number }}</td> |
||||
<td>{{ p.client_code }} - {{ p.company_name }}</td> |
||||
<td>{{ p.payment_method }}</td> |
||||
<td>{{ p.payment_currency }}</td> |
||||
<td>{{ p.payment_amount|money(p.payment_currency) }}</td> |
||||
<td>{{ p.cad_value_at_payment|money('CAD') }}</td> |
||||
<td><span class="status-badge status-{{ p.payment_status }}">{{ p.payment_status }}</span></td> |
||||
<td><span class="invoice-badge invoice-{{ p.invoice_status }}">{{ p.invoice_status }}</span></td> |
||||
<td>{{ (p.total_amount - p.amount_paid)|money(p.invoice_currency_code) }}</td> |
||||
<td>{{ p.received_at|localtime }}</td> |
||||
<td> |
||||
<a href="/payments/edit/{{ p.id }}">Edit</a> |
||||
{% if p.payment_status == 'confirmed' %} |
||||
| |
||||
<form method="post" action="/payments/void/{{ p.id }}" class="inline-form" onsubmit="return confirm('Void this payment? This will reverse it from invoice totals but keep the record for history.');"> |
||||
<button type="submit" class="void-btn">Void</button> |
||||
</form> |
||||
{% endif %} |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
|
||||
</table> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
@ -1,326 +0,0 @@
|
||||
# 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
File diff suppressed because it is too large
Load Diff
@ -1,44 +0,0 @@
|
||||
<!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> |
||||
@ -1,207 +0,0 @@
|
||||
<!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> |
||||
@ -1,73 +0,0 @@
|
||||
<!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> |
||||
@ -1,199 +0,0 @@
|
||||
<!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
@ -1,81 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>New Invoice</title> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<h1>Create Invoice</h1> |
||||
|
||||
{% if errors %} |
||||
<div style="border:1px solid red; padding:10px; margin-bottom:15px;"> |
||||
<strong>Please fix the following:</strong> |
||||
<ul> |
||||
{% for error in errors %} |
||||
<li>{{ error }}</li> |
||||
{% endfor %} |
||||
</ul> |
||||
</div> |
||||
{% endif %} |
||||
|
||||
<form method="post"> |
||||
|
||||
<p> |
||||
Client *<br> |
||||
<select name="client_id" required> |
||||
<option value="">Select client</option> |
||||
{% for c in clients %} |
||||
<option value="{{ c.id }}" {% if form_data.get('client_id') == (c.id|string) %}selected{% endif %}> |
||||
{{ c.client_code }} - {{ c.company_name }} |
||||
</option> |
||||
{% endfor %} |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Service *<br> |
||||
<select name="service_id" required> |
||||
<option value="">Select service</option> |
||||
{% for s in services %} |
||||
<option value="{{ s.id }}" {% if form_data.get('service_id') == (s.id|string) %}selected{% endif %}> |
||||
{{ s.service_code }} - {{ s.service_name }} |
||||
</option> |
||||
{% endfor %} |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Currency *<br> |
||||
<select name="currency_code" required> |
||||
<option value="CAD" {% if form_data.get('currency_code', 'CAD') == 'CAD' %}selected{% endif %}>CAD</option> |
||||
<option value="ETHO" {% if form_data.get('currency_code') == 'ETHO' %}selected{% endif %}>ETHO</option> |
||||
<option value="EGAZ" {% if form_data.get('currency_code') == 'EGAZ' %}selected{% endif %}>EGAZ</option> |
||||
<option value="ALT" {% if form_data.get('currency_code') == 'ALT' %}selected{% endif %}>ALT</option> |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Total Amount *<br> |
||||
<input type="number" step="0.00000001" min="0.00000001" name="total_amount" value="{{ form_data.get('total_amount', '') }}" required> |
||||
</p> |
||||
|
||||
<p> |
||||
Due Date *<br> |
||||
<input type="date" name="due_at" value="{{ form_data.get('due_at', '') }}" required> |
||||
</p> |
||||
|
||||
<p> |
||||
Notes<br> |
||||
<textarea name="notes">{{ form_data.get('notes', '') }}</textarea> |
||||
</p> |
||||
|
||||
<p> |
||||
<button type="submit">Create Invoice</button> |
||||
</p> |
||||
|
||||
</form> |
||||
|
||||
</body> |
||||
</html> |
||||
{% include "footer.html" %} |
||||
@ -1,5 +0,0 @@
|
||||
Flask |
||||
mysql-connector-python |
||||
reportlab |
||||
python-dateutil |
||||
pytz |
||||
File diff suppressed because it is too large
Load Diff
@ -1,79 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Invoices</title> |
||||
<style> |
||||
.status-badge { |
||||
display: inline-block; |
||||
padding: 3px 8px; |
||||
border-radius: 999px; |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
letter-spacing: 0.03em; |
||||
} |
||||
.status-draft { background: #e5e7eb; color: #111827; } |
||||
.status-pending { background: #dbeafe; color: #1d4ed8; } |
||||
.status-partial { background: #fef3c7; color: #92400e; } |
||||
.status-paid { background: #dcfce7; color: #166534; } |
||||
.status-overdue { background: #fee2e2; color: #991b1b; } |
||||
.status-cancelled { background: #e5e7eb; color: #4b5563; } |
||||
|
||||
.locked-note { |
||||
color: #92400e; |
||||
font-weight: bold; |
||||
} |
||||
</style> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<h1>Invoices</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/invoices/new">Create Invoice</a></p> |
||||
|
||||
<table border="1" cellpadding="6"> |
||||
<tr> |
||||
<th>ID</th> |
||||
<th>Invoice</th> |
||||
<th>Client</th> |
||||
<th>Currency</th> |
||||
<th>Total</th> |
||||
<th>Paid</th> |
||||
<th>Remaining</th> |
||||
<th>Status</th> |
||||
<th>Issued</th> |
||||
<th>Due</th> |
||||
<th>Actions</th> |
||||
</tr> |
||||
|
||||
{% for i in invoices %} |
||||
<tr> |
||||
<td>{{ i.id }}</td> |
||||
<td>{{ i.invoice_number }}</td> |
||||
<td>{{ i.client_code }} - {{ i.company_name }}</td> |
||||
<td>{{ i.currency_code }}</td> |
||||
<td>{{ i.total_amount|money(i.currency_code) }}</td> |
||||
<td>{{ i.amount_paid|money(i.currency_code) }}</td> |
||||
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td> |
||||
<td> |
||||
<span class="status-badge status-{{ i.status }}">{{ i.status }}</span> |
||||
</td> |
||||
<td>{{ i.issued_at|localtime }}</td> |
||||
<td>{{ i.due_at|localtime }}</td> |
||||
<td> |
||||
<a href="/invoices/view/{{ i.id }}">View</a> | |
||||
<a href="/invoices/edit/{{ i.id }}">Edit</a> |
||||
{% if i.payment_count > 0 %} |
||||
<span class="locked-note">(Locked)</span> |
||||
{% endif %} |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
|
||||
</table> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
@ -1,187 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Invoice {{ invoice.invoice_number }}</title> |
||||
<style> |
||||
body { |
||||
font-family: Arial, sans-serif; |
||||
margin: 30px; |
||||
color: #111; |
||||
} |
||||
.top-links { |
||||
margin-bottom: 20px; |
||||
} |
||||
.top-links a { |
||||
margin-right: 15px; |
||||
} |
||||
.invoice-wrap { |
||||
max-width: 900px; |
||||
} |
||||
.header-row { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: flex-start; |
||||
margin-bottom: 25px; |
||||
} |
||||
.title-box h1 { |
||||
margin: 0 0 8px 0; |
||||
} |
||||
.status-badge { |
||||
display: inline-block; |
||||
padding: 4px 10px; |
||||
border-radius: 999px; |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
} |
||||
.status-draft { background: #e5e7eb; color: #111827; } |
||||
.status-pending { background: #dbeafe; color: #1d4ed8; } |
||||
.status-partial { background: #fef3c7; color: #92400e; } |
||||
.status-paid { background: #dcfce7; color: #166534; } |
||||
.status-overdue { background: #fee2e2; color: #991b1b; } |
||||
.status-cancelled { background: #e5e7eb; color: #4b5563; } |
||||
|
||||
.info-grid { |
||||
display: grid; |
||||
grid-template-columns: 1fr 1fr; |
||||
gap: 30px; |
||||
margin-bottom: 25px; |
||||
} |
||||
.info-card { |
||||
border: 1px solid #ccc; |
||||
padding: 15px; |
||||
} |
||||
.info-card h3 { |
||||
margin-top: 0; |
||||
} |
||||
.summary-table, |
||||
.total-table { |
||||
width: 100%; |
||||
border-collapse: collapse; |
||||
margin-bottom: 25px; |
||||
} |
||||
.summary-table th, |
||||
.summary-table td, |
||||
.total-table th, |
||||
.total-table td { |
||||
border: 1px solid #ccc; |
||||
padding: 10px; |
||||
text-align: left; |
||||
} |
||||
.total-table { |
||||
max-width: 420px; |
||||
margin-left: auto; |
||||
} |
||||
.notes-box { |
||||
border: 1px solid #ccc; |
||||
padding: 15px; |
||||
white-space: pre-wrap; |
||||
} |
||||
.print-only { |
||||
display: none; |
||||
} |
||||
@media print { |
||||
.top-links, |
||||
.screen-only, |
||||
footer { |
||||
display: none !important; |
||||
} |
||||
.print-only { |
||||
display: block; |
||||
} |
||||
body { |
||||
margin: 0; |
||||
} |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
|
||||
<div class="invoice-wrap"> |
||||
<div class="top-links screen-only"> |
||||
<a href="/">Home</a> |
||||
<a href="/invoices">Back to Invoices</a> |
||||
<a href="/invoices/edit/{{ invoice.id }}">Edit Invoice</a> |
||||
<a href="#" onclick="window.print(); return false;">Print</a> |
||||
</div> |
||||
|
||||
<div class="header-row"> |
||||
<div class="title-box"> |
||||
<h1>Invoice {{ invoice.invoice_number }}</h1> |
||||
<span class="status-badge status-{{ invoice.status }}">{{ invoice.status }}</span> |
||||
</div> |
||||
<div style="text-align:right;"> |
||||
<strong>OTB Billing</strong><br> |
||||
By a contractor, for contractors |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="info-grid"> |
||||
<div class="info-card"> |
||||
<h3>Bill To</h3> |
||||
<strong>{{ invoice.company_name }}</strong><br> |
||||
{% if invoice.contact_name %}{{ invoice.contact_name }}<br>{% endif %} |
||||
{% if invoice.email %}{{ invoice.email }}<br>{% endif %} |
||||
{% if invoice.phone %}{{ invoice.phone }}<br>{% endif %} |
||||
Client Code: {{ invoice.client_code }} |
||||
</div> |
||||
|
||||
<div class="info-card"> |
||||
<h3>Invoice Details</h3> |
||||
Invoice #: {{ invoice.invoice_number }}<br> |
||||
Issued: {{ invoice.issued_at|localtime }}<br> |
||||
Due: {{ invoice.due_at|localtime }}<br> |
||||
{% if invoice.paid_at %}Paid: {{ invoice.paid_at|localtime }}<br>{% endif %} |
||||
Currency: {{ invoice.currency_code }} |
||||
</div> |
||||
</div> |
||||
|
||||
<table class="summary-table"> |
||||
<tr> |
||||
<th>Service Code</th> |
||||
<th>Service</th> |
||||
<th>Description</th> |
||||
<th>Total</th> |
||||
</tr> |
||||
<tr> |
||||
<td>{{ invoice.service_code or '-' }}</td> |
||||
<td>{{ invoice.service_name or '-' }}</td> |
||||
<td>{{ invoice.notes or '-' }}</td> |
||||
<td>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||
</tr> |
||||
</table> |
||||
|
||||
<table class="total-table"> |
||||
<tr> |
||||
<th>Subtotal</th> |
||||
<td>{{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||
</tr> |
||||
<tr> |
||||
<th>Tax</th> |
||||
<td>{{ invoice.tax_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||
</tr> |
||||
<tr> |
||||
<th>Total</th> |
||||
<td><strong>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</strong></td> |
||||
</tr> |
||||
<tr> |
||||
<th>Paid</th> |
||||
<td>{{ invoice.amount_paid|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||
</tr> |
||||
<tr> |
||||
<th>Remaining</th> |
||||
<td>{{ (invoice.total_amount - invoice.amount_paid)|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||
</tr> |
||||
</table> |
||||
|
||||
{% if invoice.notes %} |
||||
<div class="notes-box"> |
||||
<strong>Notes</strong><br><br> |
||||
{{ invoice.notes }} |
||||
</div> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
File diff suppressed because it is too large
Load Diff
@ -1,78 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Invoices</title> |
||||
<style> |
||||
.status-badge { |
||||
display: inline-block; |
||||
padding: 3px 8px; |
||||
border-radius: 999px; |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
letter-spacing: 0.03em; |
||||
} |
||||
.status-draft { background: #e5e7eb; color: #111827; } |
||||
.status-pending { background: #dbeafe; color: #1d4ed8; } |
||||
.status-partial { background: #fef3c7; color: #92400e; } |
||||
.status-paid { background: #dcfce7; color: #166534; } |
||||
.status-overdue { background: #fee2e2; color: #991b1b; } |
||||
.status-cancelled { background: #e5e7eb; color: #4b5563; } |
||||
|
||||
.locked-note { |
||||
color: #92400e; |
||||
font-weight: bold; |
||||
} |
||||
</style> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<h1>Invoices</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/invoices/new">Create Invoice</a></p> |
||||
|
||||
<table border="1" cellpadding="6"> |
||||
<tr> |
||||
<th>ID</th> |
||||
<th>Invoice</th> |
||||
<th>Client</th> |
||||
<th>Currency</th> |
||||
<th>Total</th> |
||||
<th>Paid</th> |
||||
<th>Remaining</th> |
||||
<th>Status</th> |
||||
<th>Issued</th> |
||||
<th>Due</th> |
||||
<th>Actions</th> |
||||
</tr> |
||||
|
||||
{% for i in invoices %} |
||||
<tr> |
||||
<td>{{ i.id }}</td> |
||||
<td>{{ i.invoice_number }}</td> |
||||
<td>{{ i.client_code }} - {{ i.company_name }}</td> |
||||
<td>{{ i.currency_code }}</td> |
||||
<td>{{ i.total_amount|money(i.currency_code) }}</td> |
||||
<td>{{ i.amount_paid|money(i.currency_code) }}</td> |
||||
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td> |
||||
<td> |
||||
<span class="status-badge status-{{ i.status }}">{{ i.status }}</span> |
||||
</td> |
||||
<td>{{ i.issued_at|localtime }}</td> |
||||
<td>{{ i.due_at|localtime }}</td> |
||||
<td> |
||||
<a href="/invoices/edit/{{ i.id }}">Edit</a> |
||||
{% if i.payment_count > 0 %} |
||||
<span class="locked-note">(Locked)</span> |
||||
{% endif %} |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
|
||||
</table> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
File diff suppressed because it is too large
Load Diff
@ -1,81 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Invoices</title> |
||||
<style> |
||||
.status-badge { |
||||
display: inline-block; |
||||
padding: 3px 8px; |
||||
border-radius: 999px; |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
letter-spacing: 0.03em; |
||||
} |
||||
.status-draft { background: #e5e7eb; color: #111827; } |
||||
.status-pending { background: #dbeafe; color: #1d4ed8; } |
||||
.status-partial { background: #fef3c7; color: #92400e; } |
||||
.status-paid { background: #dcfce7; color: #166534; } |
||||
.status-overdue { background: #fee2e2; color: #991b1b; } |
||||
.status-cancelled { background: #e5e7eb; color: #4b5563; } |
||||
|
||||
.locked-note { |
||||
color: #92400e; |
||||
font-weight: bold; |
||||
} |
||||
</style> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<h1>Invoices</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/invoices/new">Create Invoice</a></p> |
||||
<p><a href="/invoices/export.csv">Export CSV</a></p> |
||||
|
||||
<table border="1" cellpadding="6"> |
||||
<tr> |
||||
<th>ID</th> |
||||
<th>Invoice</th> |
||||
<th>Client</th> |
||||
<th>Currency</th> |
||||
<th>Total</th> |
||||
<th>Paid</th> |
||||
<th>Remaining</th> |
||||
<th>Status</th> |
||||
<th>Issued</th> |
||||
<th>Due</th> |
||||
<th>Actions</th> |
||||
</tr> |
||||
|
||||
{% for i in invoices %} |
||||
<tr> |
||||
<td>{{ i.id }}</td> |
||||
<td>{{ i.invoice_number }}</td> |
||||
<td>{{ i.client_code }} - {{ i.company_name }}</td> |
||||
<td>{{ i.currency_code }}</td> |
||||
<td>{{ i.total_amount|money(i.currency_code) }}</td> |
||||
<td>{{ i.amount_paid|money(i.currency_code) }}</td> |
||||
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td> |
||||
<td> |
||||
<span class="status-badge status-{{ i.status }}">{{ i.status }}</span> |
||||
</td> |
||||
<td>{{ i.issued_at|localtime }}</td> |
||||
<td>{{ i.due_at|localtime }}</td> |
||||
<td> |
||||
<a href="/invoices/view/{{ i.id }}">View</a> | |
||||
<a href="/invoices/pdf/{{ i.id }}">PDF</a> | |
||||
<a href="/invoices/edit/{{ i.id }}">Edit</a> |
||||
{% if i.payment_count > 0 %} |
||||
<span class="locked-note">(Locked)</span> |
||||
{% endif %} |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
|
||||
</table> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,103 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>New Payment</title> |
||||
</head> |
||||
<body> |
||||
|
||||
<h1>Record Payment</h1> |
||||
|
||||
{% if errors %} |
||||
<div style="border:1px solid red; padding:10px; margin-bottom:15px;"> |
||||
<strong>Please fix the following:</strong> |
||||
<ul> |
||||
{% for error in errors %} |
||||
<li>{{ error }}</li> |
||||
{% endfor %} |
||||
</ul> |
||||
</div> |
||||
{% endif %} |
||||
|
||||
<form method="post"> |
||||
|
||||
<p> |
||||
Invoice *<br> |
||||
<select name="invoice_id" required> |
||||
<option value="">Select invoice</option> |
||||
{% for i in invoices %} |
||||
<option value="{{ i.id }}" {% if form_data.get('invoice_id') == (i.id|string) %}selected{% endif %}> |
||||
{{ i.invoice_number }} - {{ i.client_code }} - {{ i.company_name }} - Due {{ i.total_amount - i.amount_paid }} {{ i.currency_code }} |
||||
</option> |
||||
{% endfor %} |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Payment Method *<br> |
||||
<select name="payment_method" required> |
||||
<option value="">Select method</option> |
||||
<option value="square" {% if form_data.get('payment_method') == 'square' %}selected{% endif %}>square</option> |
||||
<option value="etransfer" {% if form_data.get('payment_method') == 'etransfer' %}selected{% endif %}>etransfer</option> |
||||
<option value="crypto_etho" {% if form_data.get('payment_method') == 'crypto_etho' %}selected{% endif %}>crypto_etho</option> |
||||
<option value="crypto_egaz" {% if form_data.get('payment_method') == 'crypto_egaz' %}selected{% endif %}>crypto_egaz</option> |
||||
<option value="crypto_alt" {% if form_data.get('payment_method') == 'crypto_alt' %}selected{% endif %}>crypto_alt</option> |
||||
<option value="cash" {% if form_data.get('payment_method') == 'cash' %}selected{% endif %}>cash</option> |
||||
<option value="other" {% if form_data.get('payment_method') == 'other' %}selected{% endif %}>other</option> |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Payment Currency *<br> |
||||
<select name="payment_currency" required> |
||||
<option value="">Select currency</option> |
||||
<option value="CAD" {% if form_data.get('payment_currency') == 'CAD' %}selected{% endif %}>CAD</option> |
||||
<option value="ETHO" {% if form_data.get('payment_currency') == 'ETHO' %}selected{% endif %}>ETHO</option> |
||||
<option value="EGAZ" {% if form_data.get('payment_currency') == 'EGAZ' %}selected{% endif %}>EGAZ</option> |
||||
<option value="ALT" {% if form_data.get('payment_currency') == 'ALT' %}selected{% endif %}>ALT</option> |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Payment Amount *<br> |
||||
<input type="number" step="0.00000001" min="0.00000001" name="payment_amount" value="{{ form_data.get('payment_amount', '') }}" required> |
||||
</p> |
||||
|
||||
<p> |
||||
CAD Value At Payment *<br> |
||||
<input type="number" step="0.00000001" min="0" name="cad_value_at_payment" value="{{ form_data.get('cad_value_at_payment', '') }}" required> |
||||
</p> |
||||
|
||||
<p> |
||||
Reference<br> |
||||
<input name="reference" value="{{ form_data.get('reference', '') }}"> |
||||
</p> |
||||
|
||||
<p> |
||||
Sender Name<br> |
||||
<input name="sender_name" value="{{ form_data.get('sender_name', '') }}"> |
||||
</p> |
||||
|
||||
<p> |
||||
TXID<br> |
||||
<input name="txid" value="{{ form_data.get('txid', '') }}"> |
||||
</p> |
||||
|
||||
<p> |
||||
Wallet Address<br> |
||||
<input name="wallet_address" value="{{ form_data.get('wallet_address', '') }}"> |
||||
</p> |
||||
|
||||
<p> |
||||
Notes<br> |
||||
<textarea name="notes">{{ form_data.get('notes', '') }}</textarea> |
||||
</p> |
||||
|
||||
<p> |
||||
<button type="submit">Record Payment</button> |
||||
</p> |
||||
|
||||
</form> |
||||
|
||||
</body> |
||||
</html> |
||||
{% include "footer.html" %} |
||||
File diff suppressed because it is too large
Load Diff
@ -1,107 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Edit Payment</title> |
||||
</head> |
||||
<body> |
||||
|
||||
<h1>Edit Payment</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/payments">Back to Payments</a></p> |
||||
|
||||
{% if errors %} |
||||
<div style="border:1px solid red; padding:10px; margin-bottom:15px;"> |
||||
<strong>Please fix the following:</strong> |
||||
<ul> |
||||
{% for error in errors %} |
||||
<li>{{ error }}</li> |
||||
{% endfor %} |
||||
</ul> |
||||
</div> |
||||
{% endif %} |
||||
|
||||
<form method="post"> |
||||
|
||||
<p> |
||||
Payment ID<br> |
||||
<input value="{{ payment.id }}" readonly> |
||||
</p> |
||||
|
||||
<p> |
||||
Invoice<br> |
||||
<input value="{{ payment.invoice_number }} - {{ payment.client_code }} - {{ payment.company_name }}" readonly> |
||||
</p> |
||||
|
||||
<p> |
||||
Received<br> |
||||
<input value="{{ payment.received_at|localtime }}" readonly> |
||||
</p> |
||||
|
||||
<p> |
||||
Payment Method *<br> |
||||
<select name="payment_method" required> |
||||
<option value="square" {% if payment.payment_method == 'square' %}selected{% endif %}>square</option> |
||||
<option value="etransfer" {% if payment.payment_method == 'etransfer' %}selected{% endif %}>etransfer</option> |
||||
<option value="crypto_etho" {% if payment.payment_method == 'crypto_etho' %}selected{% endif %}>crypto_etho</option> |
||||
<option value="crypto_egaz" {% if payment.payment_method == 'crypto_egaz' %}selected{% endif %}>crypto_egaz</option> |
||||
<option value="crypto_alt" {% if payment.payment_method == 'crypto_alt' %}selected{% endif %}>crypto_alt</option> |
||||
<option value="cash" {% if payment.payment_method == 'cash' %}selected{% endif %}>cash</option> |
||||
<option value="other" {% if payment.payment_method == 'other' %}selected{% endif %}>other</option> |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Payment Currency *<br> |
||||
<select name="payment_currency" required> |
||||
<option value="CAD" {% if payment.payment_currency == 'CAD' %}selected{% endif %}>CAD</option> |
||||
<option value="ETHO" {% if payment.payment_currency == 'ETHO' %}selected{% endif %}>ETHO</option> |
||||
<option value="EGAZ" {% if payment.payment_currency == 'EGAZ' %}selected{% endif %}>EGAZ</option> |
||||
<option value="ALT" {% if payment.payment_currency == 'ALT' %}selected{% endif %}>ALT</option> |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Payment Amount *<br> |
||||
<input type="number" step="0.00000001" min="0.00000001" name="payment_amount" value="{{ payment.payment_amount }}" required> |
||||
</p> |
||||
|
||||
<p> |
||||
CAD Value At Payment *<br> |
||||
<input type="number" step="0.00000001" min="0" name="cad_value_at_payment" value="{{ payment.cad_value_at_payment }}" required> |
||||
</p> |
||||
|
||||
<p> |
||||
Reference<br> |
||||
<input name="reference" value="{{ payment.reference or '' }}"> |
||||
</p> |
||||
|
||||
<p> |
||||
Sender Name<br> |
||||
<input name="sender_name" value="{{ payment.sender_name or '' }}"> |
||||
</p> |
||||
|
||||
<p> |
||||
TXID<br> |
||||
<input name="txid" value="{{ payment.txid or '' }}"> |
||||
</p> |
||||
|
||||
<p> |
||||
Wallet Address<br> |
||||
<input name="wallet_address" value="{{ payment.wallet_address or '' }}"> |
||||
</p> |
||||
|
||||
<p> |
||||
Notes<br> |
||||
<textarea name="notes" rows="5" cols="60">{{ payment.notes or '' }}</textarea> |
||||
</p> |
||||
|
||||
<p> |
||||
<button type="submit">Save Payment</button> |
||||
</p> |
||||
|
||||
</form> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
@ -1,139 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>New Payment</title> |
||||
<style> |
||||
.status-badge { |
||||
display: inline-block; |
||||
padding: 2px 7px; |
||||
border-radius: 999px; |
||||
font-size: 11px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
letter-spacing: 0.03em; |
||||
} |
||||
.status-pending { background: #dbeafe; color: #1d4ed8; } |
||||
.status-partial { background: #fef3c7; color: #92400e; } |
||||
.status-overdue { background: #fee2e2; color: #991b1b; } |
||||
|
||||
.info-box { |
||||
border: 1px solid #2563eb; |
||||
background: #eff6ff; |
||||
padding: 10px; |
||||
margin-bottom: 15px; |
||||
} |
||||
.error-box { |
||||
border: 1px solid red; |
||||
padding: 10px; |
||||
margin-bottom: 15px; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
|
||||
<h1>Record Payment</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/payments">Back to Payments</a></p> |
||||
|
||||
<div class="info-box"> |
||||
Only invoices with an outstanding balance are shown here.<br> |
||||
Paid and cancelled invoices are excluded from payment entry. |
||||
</div> |
||||
|
||||
{% if errors %} |
||||
<div class="error-box"> |
||||
<strong>Please fix the following:</strong> |
||||
<ul> |
||||
{% for error in errors %} |
||||
<li>{{ error }}</li> |
||||
{% endfor %} |
||||
</ul> |
||||
</div> |
||||
{% endif %} |
||||
|
||||
<form method="post"> |
||||
|
||||
<p> |
||||
Invoice *<br> |
||||
<select name="invoice_id" required> |
||||
<option value="">Select invoice</option> |
||||
{% for i in invoices %} |
||||
<option value="{{ i.id }}" {% if form_data.get('invoice_id') == (i.id|string) %}selected{% endif %}> |
||||
{{ i.invoice_number }} - {{ i.client_code }} - {{ i.company_name }} - |
||||
Remaining {{ (i.total_amount - i.amount_paid)|money(i.currency_code) }} {{ i.currency_code }} - |
||||
{{ i.status }} |
||||
</option> |
||||
{% endfor %} |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Payment Method *<br> |
||||
<select name="payment_method" required> |
||||
<option value="">Select method</option> |
||||
<option value="square" {% if form_data.get('payment_method') == 'square' %}selected{% endif %}>square</option> |
||||
<option value="etransfer" {% if form_data.get('payment_method') == 'etransfer' %}selected{% endif %}>etransfer</option> |
||||
<option value="crypto_etho" {% if form_data.get('payment_method') == 'crypto_etho' %}selected{% endif %}>crypto_etho</option> |
||||
<option value="crypto_egaz" {% if form_data.get('payment_method') == 'crypto_egaz' %}selected{% endif %}>crypto_egaz</option> |
||||
<option value="crypto_alt" {% if form_data.get('payment_method') == 'crypto_alt' %}selected{% endif %}>crypto_alt</option> |
||||
<option value="cash" {% if form_data.get('payment_method') == 'cash' %}selected{% endif %}>cash</option> |
||||
<option value="other" {% if form_data.get('payment_method') == 'other' %}selected{% endif %}>other</option> |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Payment Currency *<br> |
||||
<select name="payment_currency" required> |
||||
<option value="">Select currency</option> |
||||
<option value="CAD" {% if form_data.get('payment_currency') == 'CAD' %}selected{% endif %}>CAD</option> |
||||
<option value="ETHO" {% if form_data.get('payment_currency') == 'ETHO' %}selected{% endif %}>ETHO</option> |
||||
<option value="EGAZ" {% if form_data.get('payment_currency') == 'EGAZ' %}selected{% endif %}>EGAZ</option> |
||||
<option value="ALT" {% if form_data.get('payment_currency') == 'ALT' %}selected{% endif %}>ALT</option> |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Payment Amount *<br> |
||||
<input type="number" step="0.00000001" min="0.00000001" name="payment_amount" value="{{ form_data.get('payment_amount', '') }}" required> |
||||
</p> |
||||
|
||||
<p> |
||||
CAD Value At Payment *<br> |
||||
<input type="number" step="0.00000001" min="0" name="cad_value_at_payment" value="{{ form_data.get('cad_value_at_payment', '') }}" required> |
||||
</p> |
||||
|
||||
<p> |
||||
Reference<br> |
||||
<input name="reference" value="{{ form_data.get('reference', '') }}"> |
||||
</p> |
||||
|
||||
<p> |
||||
Sender Name<br> |
||||
<input name="sender_name" value="{{ form_data.get('sender_name', '') }}"> |
||||
</p> |
||||
|
||||
<p> |
||||
TXID<br> |
||||
<input name="txid" value="{{ form_data.get('txid', '') }}"> |
||||
</p> |
||||
|
||||
<p> |
||||
Wallet Address<br> |
||||
<input name="wallet_address" value="{{ form_data.get('wallet_address', '') }}"> |
||||
</p> |
||||
|
||||
<p> |
||||
Notes<br> |
||||
<textarea name="notes">{{ form_data.get('notes', '') }}</textarea> |
||||
</p> |
||||
|
||||
<p> |
||||
<button type="submit">Record Payment</button> |
||||
</p> |
||||
|
||||
</form> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
File diff suppressed because it is too large
Load Diff
@ -1,100 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Payments</title> |
||||
<style> |
||||
.status-badge { |
||||
display: inline-block; |
||||
padding: 3px 8px; |
||||
border-radius: 999px; |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
letter-spacing: 0.03em; |
||||
} |
||||
.status-confirmed { background: #dcfce7; color: #166534; } |
||||
.status-reversed { background: #fee2e2; color: #991b1b; } |
||||
|
||||
.invoice-badge { |
||||
display: inline-block; |
||||
padding: 3px 8px; |
||||
border-radius: 999px; |
||||
font-size: 11px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
letter-spacing: 0.03em; |
||||
} |
||||
.invoice-pending { background: #dbeafe; color: #1d4ed8; } |
||||
.invoice-partial { background: #fef3c7; color: #92400e; } |
||||
.invoice-paid { background: #dcfce7; color: #166534; } |
||||
.invoice-overdue { background: #fee2e2; color: #991b1b; } |
||||
.invoice-cancelled { background: #e5e7eb; color: #4b5563; } |
||||
.invoice-draft { background: #e5e7eb; color: #111827; } |
||||
|
||||
.inline-form { |
||||
display: inline; |
||||
margin: 0; |
||||
} |
||||
.void-btn { |
||||
background: #991b1b; |
||||
color: white; |
||||
border: 0; |
||||
padding: 4px 8px; |
||||
cursor: pointer; |
||||
} |
||||
.void-btn:hover { |
||||
opacity: 0.9; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
|
||||
<h1>Payments</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/payments/new">Record Payment</a></p> |
||||
|
||||
<table border="1" cellpadding="6"> |
||||
<tr> |
||||
<th>ID</th> |
||||
<th>Invoice</th> |
||||
<th>Client</th> |
||||
<th>Method</th> |
||||
<th>Currency</th> |
||||
<th>Amount</th> |
||||
<th>CAD Value</th> |
||||
<th>Payment Status</th> |
||||
<th>Invoice Status</th> |
||||
<th>Received</th> |
||||
<th>Actions</th> |
||||
</tr> |
||||
|
||||
{% for p in payments %} |
||||
<tr> |
||||
<td>{{ p.id }}</td> |
||||
<td>{{ p.invoice_number }}</td> |
||||
<td>{{ p.client_code }} - {{ p.company_name }}</td> |
||||
<td>{{ p.payment_method }}</td> |
||||
<td>{{ p.payment_currency }}</td> |
||||
<td>{{ p.payment_amount|money(p.payment_currency) }}</td> |
||||
<td>{{ p.cad_value_at_payment|money('CAD') }}</td> |
||||
<td><span class="status-badge status-{{ p.payment_status }}">{{ p.payment_status }}</span></td> |
||||
<td><span class="invoice-badge invoice-{{ p.invoice_status }}">{{ p.invoice_status }}</span></td> |
||||
<td>{{ p.received_at|localtime }}</td> |
||||
<td> |
||||
<a href="/payments/edit/{{ p.id }}">Edit</a> |
||||
{% if p.payment_status == 'confirmed' %} |
||||
| |
||||
<form method="post" action="/payments/void/{{ p.id }}" class="inline-form" onsubmit="return confirm('Void this payment? This will reverse it from invoice totals but keep the record for history.');"> |
||||
<button type="submit" class="void-btn">Void</button> |
||||
</form> |
||||
{% endif %} |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
|
||||
</table> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
File diff suppressed because it is too large
Load Diff
@ -1,100 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Payments</title> |
||||
<style> |
||||
.status-badge { |
||||
display: inline-block; |
||||
padding: 3px 8px; |
||||
border-radius: 999px; |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
letter-spacing: 0.03em; |
||||
} |
||||
.status-confirmed { background: #dcfce7; color: #166534; } |
||||
.status-reversed { background: #fee2e2; color: #991b1b; } |
||||
|
||||
.invoice-badge { |
||||
display: inline-block; |
||||
padding: 3px 8px; |
||||
border-radius: 999px; |
||||
font-size: 11px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
letter-spacing: 0.03em; |
||||
} |
||||
.invoice-pending { background: #dbeafe; color: #1d4ed8; } |
||||
.invoice-partial { background: #fef3c7; color: #92400e; } |
||||
.invoice-paid { background: #dcfce7; color: #166534; } |
||||
.invoice-overdue { background: #fee2e2; color: #991b1b; } |
||||
.invoice-cancelled { background: #e5e7eb; color: #4b5563; } |
||||
.invoice-draft { background: #e5e7eb; color: #111827; } |
||||
|
||||
.inline-form { |
||||
display: inline; |
||||
margin: 0; |
||||
} |
||||
.void-btn { |
||||
background: #991b1b; |
||||
color: white; |
||||
border: 0; |
||||
padding: 4px 8px; |
||||
cursor: pointer; |
||||
} |
||||
.void-btn:hover { |
||||
opacity: 0.9; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
|
||||
<h1>Payments</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/payments/new">Record Payment</a></p> |
||||
|
||||
<table border="1" cellpadding="6"> |
||||
<tr> |
||||
<th>ID</th> |
||||
<th>Invoice</th> |
||||
<th>Client</th> |
||||
<th>Method</th> |
||||
<th>Currency</th> |
||||
<th>Amount</th> |
||||
<th>CAD Value</th> |
||||
<th>Payment Status</th> |
||||
<th>Invoice Status</th> |
||||
<th>Received</th> |
||||
<th>Actions</th> |
||||
</tr> |
||||
|
||||
{% for p in payments %} |
||||
<tr> |
||||
<td>{{ p.id }}</td> |
||||
<td>{{ p.invoice_number }}</td> |
||||
<td>{{ p.client_code }} - {{ p.company_name }}</td> |
||||
<td>{{ p.payment_method }}</td> |
||||
<td>{{ p.payment_currency }}</td> |
||||
<td>{{ p.payment_amount|money(p.payment_currency) }}</td> |
||||
<td>{{ p.cad_value_at_payment|money('CAD') }}</td> |
||||
<td><span class="status-badge status-{{ p.payment_status }}">{{ p.payment_status }}</span></td> |
||||
<td><span class="invoice-badge invoice-{{ p.invoice_status }}">{{ p.invoice_status }}</span></td> |
||||
<td>{{ p.received_at|localtime }}</td> |
||||
<td> |
||||
<a href="/payments/edit/{{ p.id }}">Edit</a> |
||||
{% if p.payment_status == 'confirmed' %} |
||||
| |
||||
<form method="post" action="/payments/void/{{ p.id }}" class="inline-form" onsubmit="return confirm('Void this payment? This will reverse it from invoice totals but keep the record for history.');"> |
||||
<button type="submit" class="void-btn">Void</button> |
||||
</form> |
||||
{% endif %} |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
|
||||
</table> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,42 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>OTB Billing Dashboard</title> |
||||
</head> |
||||
<body> |
||||
|
||||
|
||||
{% if app_settings.business_logo_url %} |
||||
<div style="margin-bottom:15px;"> |
||||
<img src="{{ app_settings.business_logo_url }}" style="height:60px;"> |
||||
</div> |
||||
{% endif %} |
||||
<h1>{{ app_settings.business_name or 'OTB Billing' }} Dashboard</h1> |
||||
|
||||
<p><a href="/clients">Clients</a></p> |
||||
<p><a href="/services">Services</a></p> |
||||
<p><a href="/invoices">Invoices</a></p> |
||||
<p><a href="/payments">Payments</a></p> |
||||
<p><a href="/settings">Settings / Config</a></p> |
||||
<p><a href="/dbtest">DB Test</a></p> |
||||
|
||||
<table border="1" cellpadding="10"> |
||||
<tr> |
||||
<th>Total Clients</th> |
||||
<th>Active Services</th> |
||||
<th>Outstanding Invoices</th> |
||||
<th>Revenue Received (CAD)</th> |
||||
</tr> |
||||
<tr> |
||||
<td>{{ total_clients }}</td> |
||||
<td>{{ active_services }}</td> |
||||
<td>{{ outstanding_invoices }}</td> |
||||
<td>{{ revenue_received|money('CAD') }}</td> |
||||
</tr> |
||||
</table> |
||||
|
||||
<p>Displayed times are shown in Eastern Time (Toronto).</p> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
@ -1,192 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Settings</title> |
||||
<style> |
||||
body { font-family: Arial, sans-serif; } |
||||
.form-grid { |
||||
display: grid; |
||||
grid-template-columns: 1fr 1fr; |
||||
gap: 24px; |
||||
max-width: 1100px; |
||||
} |
||||
.card { |
||||
border: 1px solid #ccc; |
||||
padding: 16px; |
||||
} |
||||
.card h2 { |
||||
margin-top: 0; |
||||
} |
||||
input[type="text"], |
||||
input[type="email"], |
||||
input[type="password"], |
||||
input[type="number"], |
||||
textarea, |
||||
select { |
||||
width: 100%; |
||||
box-sizing: border-box; |
||||
margin-top: 4px; |
||||
margin-bottom: 12px; |
||||
padding: 8px; |
||||
} |
||||
textarea { min-height: 90px; } |
||||
.checkbox-row { |
||||
margin: 8px 0 14px 0; |
||||
} |
||||
.save-row { |
||||
margin-top: 18px; |
||||
} |
||||
.logo-preview { |
||||
margin: 10px 0 14px 0; |
||||
} |
||||
.logo-preview img { |
||||
max-height: 70px; |
||||
max-width: 220px; |
||||
border: 1px solid #ccc; |
||||
padding: 6px; |
||||
background: #fff; |
||||
} |
||||
small { |
||||
color: #444; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
|
||||
<h1>Settings / Config</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
|
||||
<form method="post"> |
||||
<div class="form-grid"> |
||||
<div class="card"> |
||||
<h2>Business Identity</h2> |
||||
|
||||
Business Name<br> |
||||
<input type="text" name="business_name" value="{{ settings.business_name }}"><br> |
||||
|
||||
Business Logo URL<br> |
||||
<input type="text" name="business_logo_url" value="{{ settings.business_logo_url }}"><br> |
||||
<small>Example: /static/favicon.png or https://site.com/logo.png</small><br> |
||||
|
||||
{% if settings.business_logo_url %} |
||||
<div class="logo-preview"> |
||||
<img src="{{ settings.business_logo_url }}" alt="Business Logo Preview"> |
||||
</div> |
||||
{% endif %} |
||||
|
||||
Slogan / Tagline<br> |
||||
<input type="text" name="business_tagline" value="{{ settings.business_tagline }}"><br> |
||||
|
||||
Business Email<br> |
||||
<input type="email" name="business_email" value="{{ settings.business_email }}"><br> |
||||
|
||||
Business Phone<br> |
||||
<input type="text" name="business_phone" value="{{ settings.business_phone }}"><br> |
||||
|
||||
Business Address<br> |
||||
<textarea name="business_address">{{ settings.business_address }}</textarea><br> |
||||
|
||||
Website<br> |
||||
<input type="text" name="business_website" value="{{ settings.business_website }}"><br> |
||||
|
||||
Business Number / Registration Number<br> |
||||
<input type="text" name="business_number" value="{{ settings.business_number }}"><br> |
||||
|
||||
Default Currency<br> |
||||
<select name="default_currency"> |
||||
<option value="CAD" {% if settings.default_currency == 'CAD' %}selected{% endif %}>CAD</option> |
||||
<option value="USD" {% if settings.default_currency == 'USD' %}selected{% endif %}>USD</option> |
||||
<option value="ETHO" {% if settings.default_currency == 'ETHO' %}selected{% endif %}>ETHO</option> |
||||
<option value="EGAZ" {% if settings.default_currency == 'EGAZ' %}selected{% endif %}>EGAZ</option> |
||||
<option value="ALT" {% if settings.default_currency == 'ALT' %}selected{% endif %}>ALT</option> |
||||
</select> |
||||
</div> |
||||
|
||||
<div class="card"> |
||||
<h2>Tax Settings</h2> |
||||
|
||||
Local Country<br> |
||||
<input type="text" name="local_country" value="{{ settings.local_country }}"><br> |
||||
|
||||
Tax Label<br> |
||||
<input type="text" name="tax_label" value="{{ settings.tax_label }}"><br> |
||||
|
||||
Tax Rate (%)<br> |
||||
<input type="number" step="0.01" name="tax_rate" value="{{ settings.tax_rate }}"><br> |
||||
|
||||
Tax Number<br> |
||||
<input type="text" name="tax_number" value="{{ settings.tax_number }}"><br> |
||||
|
||||
<div class="checkbox-row"> |
||||
<label> |
||||
<input type="checkbox" name="apply_local_tax_only" value="1" {% if settings.apply_local_tax_only == '1' %}checked{% endif %}> |
||||
Apply tax only to local clients |
||||
</label> |
||||
</div> |
||||
|
||||
Payment Terms<br> |
||||
<textarea name="payment_terms">{{ settings.payment_terms }}</textarea><br> |
||||
|
||||
Invoice Footer<br> |
||||
<textarea name="invoice_footer">{{ settings.invoice_footer }}</textarea><br> |
||||
</div> |
||||
|
||||
<div class="card"> |
||||
<h2>Advanced / Email / SMTP</h2> |
||||
|
||||
SMTP Host<br> |
||||
<input type="text" name="smtp_host" value="{{ settings.smtp_host }}"><br> |
||||
|
||||
SMTP Port<br> |
||||
<input type="number" name="smtp_port" value="{{ settings.smtp_port }}"><br> |
||||
|
||||
SMTP Username<br> |
||||
<input type="text" name="smtp_user" value="{{ settings.smtp_user }}"><br> |
||||
|
||||
SMTP Password<br> |
||||
<input type="password" name="smtp_pass" value="{{ settings.smtp_pass }}"><br> |
||||
|
||||
From Email<br> |
||||
<input type="email" name="smtp_from_email" value="{{ settings.smtp_from_email }}"><br> |
||||
|
||||
From Name<br> |
||||
<input type="text" name="smtp_from_name" value="{{ settings.smtp_from_name }}"><br> |
||||
|
||||
<div class="checkbox-row"> |
||||
<label> |
||||
<input type="checkbox" name="smtp_use_tls" value="1" {% if settings.smtp_use_tls == '1' %}checked{% endif %}> |
||||
Use TLS |
||||
</label> |
||||
</div> |
||||
|
||||
<div class="checkbox-row"> |
||||
<label> |
||||
<input type="checkbox" name="smtp_use_ssl" value="1" {% if settings.smtp_use_ssl == '1' %}checked{% endif %}> |
||||
Use SSL |
||||
</label> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="card"> |
||||
<h2>Notes</h2> |
||||
<p> |
||||
Branding, tax identity, and SMTP values are stored here for this installation. |
||||
</p> |
||||
<p> |
||||
Logo can be a local static path like <strong>/static/favicon.png</strong> or a full external/IPFS URL. |
||||
</p> |
||||
<p> |
||||
Email sending is not wired yet, but these SMTP settings are stored now so the next step can use them. |
||||
</p> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="save-row"> |
||||
<button type="submit">Save Settings</button> |
||||
</div> |
||||
</form> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
File diff suppressed because it is too large
Load Diff
@ -1,35 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>OTB Billing Dashboard</title> |
||||
</head> |
||||
<body> |
||||
|
||||
<h1>OTB Billing Dashboard</h1> |
||||
|
||||
<p><a href="/clients">Clients</a></p> |
||||
<p><a href="/services">Services</a></p> |
||||
<p><a href="/invoices">Invoices</a></p> |
||||
<p><a href="/payments">Payments</a></p> |
||||
<p><a href="/dbtest">DB Test</a></p> |
||||
|
||||
<table border="1" cellpadding="10"> |
||||
<tr> |
||||
<th>Total Clients</th> |
||||
<th>Active Services</th> |
||||
<th>Outstanding Invoices</th> |
||||
<th>Revenue Received (CAD)</th> |
||||
</tr> |
||||
<tr> |
||||
<td>{{ total_clients }}</td> |
||||
<td>{{ active_services }}</td> |
||||
<td>{{ outstanding_invoices }}</td> |
||||
<td>{{ revenue_received|money('CAD') }}</td> |
||||
</tr> |
||||
</table> |
||||
|
||||
<p>Displayed times are shown in Eastern Time (Toronto).</p> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
@ -1,188 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Invoice {{ invoice.invoice_number }}</title> |
||||
<style> |
||||
body { |
||||
font-family: Arial, sans-serif; |
||||
margin: 30px; |
||||
color: #111; |
||||
} |
||||
.top-links { |
||||
margin-bottom: 20px; |
||||
} |
||||
.top-links a { |
||||
margin-right: 15px; |
||||
} |
||||
.invoice-wrap { |
||||
max-width: 900px; |
||||
} |
||||
.header-row { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: flex-start; |
||||
margin-bottom: 25px; |
||||
} |
||||
.title-box h1 { |
||||
margin: 0 0 8px 0; |
||||
} |
||||
.status-badge { |
||||
display: inline-block; |
||||
padding: 4px 10px; |
||||
border-radius: 999px; |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
text-transform: uppercase; |
||||
} |
||||
.status-draft { background: #e5e7eb; color: #111827; } |
||||
.status-pending { background: #dbeafe; color: #1d4ed8; } |
||||
.status-partial { background: #fef3c7; color: #92400e; } |
||||
.status-paid { background: #dcfce7; color: #166534; } |
||||
.status-overdue { background: #fee2e2; color: #991b1b; } |
||||
.status-cancelled { background: #e5e7eb; color: #4b5563; } |
||||
|
||||
.info-grid { |
||||
display: grid; |
||||
grid-template-columns: 1fr 1fr; |
||||
gap: 30px; |
||||
margin-bottom: 25px; |
||||
} |
||||
.info-card { |
||||
border: 1px solid #ccc; |
||||
padding: 15px; |
||||
} |
||||
.info-card h3 { |
||||
margin-top: 0; |
||||
} |
||||
.summary-table, |
||||
.total-table { |
||||
width: 100%; |
||||
border-collapse: collapse; |
||||
margin-bottom: 25px; |
||||
} |
||||
.summary-table th, |
||||
.summary-table td, |
||||
.total-table th, |
||||
.total-table td { |
||||
border: 1px solid #ccc; |
||||
padding: 10px; |
||||
text-align: left; |
||||
} |
||||
.total-table { |
||||
max-width: 420px; |
||||
margin-left: auto; |
||||
} |
||||
.notes-box { |
||||
border: 1px solid #ccc; |
||||
padding: 15px; |
||||
white-space: pre-wrap; |
||||
} |
||||
.print-only { |
||||
display: none; |
||||
} |
||||
@media print { |
||||
.top-links, |
||||
.screen-only, |
||||
footer { |
||||
display: none !important; |
||||
} |
||||
.print-only { |
||||
display: block; |
||||
} |
||||
body { |
||||
margin: 0; |
||||
} |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
|
||||
<div class="invoice-wrap"> |
||||
<div class="top-links screen-only"> |
||||
<a href="/">Home</a> |
||||
<a href="/invoices">Back to Invoices</a> |
||||
<a href="/invoices/edit/{{ invoice.id }}">Edit Invoice</a> |
||||
<a href="#" onclick="window.print(); return false;">Print</a> |
||||
<a href="/invoices/pdf/{{ invoice.id }}">PDF</a> |
||||
</div> |
||||
|
||||
<div class="header-row"> |
||||
<div class="title-box"> |
||||
<h1>Invoice {{ invoice.invoice_number }}</h1> |
||||
<span class="status-badge status-{{ invoice.status }}">{{ invoice.status }}</span> |
||||
</div> |
||||
<div style="text-align:right;"> |
||||
<strong>OTB Billing</strong><br> |
||||
By a contractor, for contractors |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="info-grid"> |
||||
<div class="info-card"> |
||||
<h3>Bill To</h3> |
||||
<strong>{{ invoice.company_name }}</strong><br> |
||||
{% if invoice.contact_name %}{{ invoice.contact_name }}<br>{% endif %} |
||||
{% if invoice.email %}{{ invoice.email }}<br>{% endif %} |
||||
{% if invoice.phone %}{{ invoice.phone }}<br>{% endif %} |
||||
Client Code: {{ invoice.client_code }} |
||||
</div> |
||||
|
||||
<div class="info-card"> |
||||
<h3>Invoice Details</h3> |
||||
Invoice #: {{ invoice.invoice_number }}<br> |
||||
Issued: {{ invoice.issued_at|localtime }}<br> |
||||
Due: {{ invoice.due_at|localtime }}<br> |
||||
{% if invoice.paid_at %}Paid: {{ invoice.paid_at|localtime }}<br>{% endif %} |
||||
Currency: {{ invoice.currency_code }} |
||||
</div> |
||||
</div> |
||||
|
||||
<table class="summary-table"> |
||||
<tr> |
||||
<th>Service Code</th> |
||||
<th>Service</th> |
||||
<th>Description</th> |
||||
<th>Total</th> |
||||
</tr> |
||||
<tr> |
||||
<td>{{ invoice.service_code or '-' }}</td> |
||||
<td>{{ invoice.service_name or '-' }}</td> |
||||
<td>{{ invoice.notes or '-' }}</td> |
||||
<td>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||
</tr> |
||||
</table> |
||||
|
||||
<table class="total-table"> |
||||
<tr> |
||||
<th>Subtotal</th> |
||||
<td>{{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||
</tr> |
||||
<tr> |
||||
<th>Tax</th> |
||||
<td>{{ invoice.tax_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||
</tr> |
||||
<tr> |
||||
<th>Total</th> |
||||
<td><strong>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</strong></td> |
||||
</tr> |
||||
<tr> |
||||
<th>Paid</th> |
||||
<td>{{ invoice.amount_paid|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||
</tr> |
||||
<tr> |
||||
<th>Remaining</th> |
||||
<td>{{ (invoice.total_amount - invoice.amount_paid)|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||
</tr> |
||||
</table> |
||||
|
||||
{% if invoice.notes %} |
||||
<div class="notes-box"> |
||||
<strong>Notes</strong><br><br> |
||||
{{ invoice.notes }} |
||||
</div> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
File diff suppressed because it is too large
Load Diff
@ -1,113 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Edit Invoice</title> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<h1>Edit Invoice</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/invoices">Back to Invoices</a></p> |
||||
|
||||
{% if errors %} |
||||
<div style="border:1px solid red; padding:10px; margin-bottom:15px;"> |
||||
<strong>Please fix the following:</strong> |
||||
<ul> |
||||
{% for error in errors %} |
||||
<li>{{ error }}</li> |
||||
{% endfor %} |
||||
</ul> |
||||
</div> |
||||
{% endif %} |
||||
|
||||
{% if locked %} |
||||
<div style="border:1px solid #aa6600; padding:10px; margin-bottom:15px; background:#fff4dd;"> |
||||
<strong>This invoice is locked for core edits because payments exist.</strong><br> |
||||
Core accounting fields cannot be changed after payment activity begins. |
||||
</div> |
||||
{% endif %} |
||||
|
||||
<form method="post"> |
||||
|
||||
<p> |
||||
Invoice Number<br> |
||||
<input value="{{ invoice.invoice_number }}" readonly> |
||||
</p> |
||||
|
||||
{% if not locked %} |
||||
<p> |
||||
Client *<br> |
||||
<select name="client_id" required> |
||||
{% for c in clients %} |
||||
<option value="{{ c.id }}" {% if invoice.client_id == c.id %}selected{% endif %}> |
||||
{{ c.client_code }} - {{ c.company_name }} |
||||
</option> |
||||
{% endfor %} |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Service *<br> |
||||
<select name="service_id" required> |
||||
{% for s in services %} |
||||
<option value="{{ s.id }}" {% if invoice.service_id == s.id %}selected{% endif %}> |
||||
{{ s.service_code }} - {{ s.service_name }} |
||||
</option> |
||||
{% endfor %} |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Currency *<br> |
||||
<select name="currency_code" required> |
||||
<option value="CAD" {% if invoice.currency_code == 'CAD' %}selected{% endif %}>CAD</option> |
||||
<option value="ETHO" {% if invoice.currency_code == 'ETHO' %}selected{% endif %}>ETHO</option> |
||||
<option value="EGAZ" {% if invoice.currency_code == 'EGAZ' %}selected{% endif %}>EGAZ</option> |
||||
<option value="ALT" {% if invoice.currency_code == 'ALT' %}selected{% endif %}>ALT</option> |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Total Amount *<br> |
||||
<input type="number" step="0.00000001" min="0" name="total_amount" value="{{ invoice.total_amount }}" required> |
||||
</p> |
||||
{% else %} |
||||
<p>Client<br><input value="{{ invoice.client_id }}" readonly></p> |
||||
<p>Service<br><input value="{{ invoice.service_id }}" readonly></p> |
||||
<p>Currency<br><input value="{{ invoice.currency_code }}" readonly></p> |
||||
<p>Total Amount<br><input value="{{ invoice.total_amount|money(invoice.currency_code) }}" readonly></p> |
||||
{% endif %} |
||||
|
||||
<p> |
||||
Due Date *<br> |
||||
<input type="date" name="due_at" value="{{ invoice.due_at.strftime('%Y-%m-%d') if invoice.due_at else '' }}" required> |
||||
</p> |
||||
|
||||
<p> |
||||
Status *<br> |
||||
<select name="status" required> |
||||
<option value="draft" {% if invoice.status == 'draft' %}selected{% endif %}>draft</option> |
||||
<option value="pending" {% if invoice.status == 'pending' %}selected{% endif %}>pending</option> |
||||
<option value="partial" {% if invoice.status == 'partial' %}selected{% endif %}>partial</option> |
||||
<option value="paid" {% if invoice.status == 'paid' %}selected{% endif %}>paid</option> |
||||
<option value="overdue" {% if invoice.status == 'overdue' %}selected{% endif %}>overdue</option> |
||||
<option value="cancelled" {% if invoice.status == 'cancelled' %}selected{% endif %}>cancelled</option> |
||||
</select> |
||||
</p> |
||||
|
||||
<p> |
||||
Notes<br> |
||||
<textarea name="notes" rows="5" cols="60">{{ invoice.notes or '' }}</textarea> |
||||
</p> |
||||
|
||||
<p> |
||||
<button type="submit">Save Invoice</button> |
||||
</p> |
||||
|
||||
</form> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
@ -1,54 +0,0 @@
|
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<title>Invoices</title> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<h1>Invoices</h1> |
||||
|
||||
<p><a href="/">Home</a></p> |
||||
<p><a href="/invoices/new">Create Invoice</a></p> |
||||
|
||||
<table border="1" cellpadding="6"> |
||||
<tr> |
||||
<th>ID</th> |
||||
<th>Invoice</th> |
||||
<th>Client</th> |
||||
<th>Currency</th> |
||||
<th>Total</th> |
||||
<th>Paid</th> |
||||
<th>Remaining</th> |
||||
<th>Status</th> |
||||
<th>Issued</th> |
||||
<th>Due</th> |
||||
<th>Actions</th> |
||||
</tr> |
||||
|
||||
{% for i in invoices %} |
||||
<tr> |
||||
<td>{{ i.id }}</td> |
||||
<td>{{ i.invoice_number }}</td> |
||||
<td>{{ i.client_code }} - {{ i.company_name }}</td> |
||||
<td>{{ i.currency_code }}</td> |
||||
<td>{{ i.total_amount|money(i.currency_code) }}</td> |
||||
<td>{{ i.amount_paid|money(i.currency_code) }}</td> |
||||
<td>{{ (i.total_amount - i.amount_paid)|money(i.currency_code) }}</td> |
||||
<td>{{ i.status }}</td> |
||||
<td>{{ i.issued_at|localtime }}</td> |
||||
<td>{{ i.due_at|localtime }}</td> |
||||
<td> |
||||
<a href="/invoices/edit/{{ i.id }}">Edit</a> |
||||
{% if i.payment_count > 0 %} |
||||
(Locked) |
||||
{% endif %} |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
|
||||
</table> |
||||
|
||||
{% include "footer.html" %} |
||||
</body> |
||||
</html> |
||||
File diff suppressed because it is too large
Load Diff
@ -1,235 +0,0 @@
|
||||
<!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,45 @@
|
||||
cd /home/def/otb_billing/backend || exit 1 |
||||
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)" |
||||
cp app.py "app.py.email-attachment.${STAMP}.bak" |
||||
|
||||
python3 <<'PY' |
||||
from pathlib import Path |
||||
|
||||
p = Path("app.py") |
||||
t = p.read_text() |
||||
|
||||
# Find the payment confirmation email block |
||||
old = "attachments=None," |
||||
|
||||
new = """attachments=[{ |
||||
"filename": f"invoice_{invoice.get('invoice_number')}.pdf", |
||||
"content": generate_invoice_pdf_bytes(invoice_id), |
||||
"mime": "application/pdf" |
||||
}],""" |
||||
|
||||
if old not in t: |
||||
raise SystemExit("FAILED: attachments=None not found") |
||||
|
||||
t = t.replace(old, new, 1) |
||||
|
||||
# Add helper function if missing |
||||
if "def generate_invoice_pdf_bytes" not in t: |
||||
t += """ |
||||
|
||||
def generate_invoice_pdf_bytes(invoice_id): |
||||
from io import BytesIO |
||||
buffer = BytesIO() |
||||
c = canvas.Canvas(buffer, pagesize=letter) |
||||
c.drawString(100, 750, f"Invoice #{invoice_id}") |
||||
c.drawString(100, 730, "Generated by OTB Billing") |
||||
c.save() |
||||
buffer.seek(0) |
||||
return buffer.read() |
||||
""" |
||||
|
||||
p.write_text(t) |
||||
print("OK: email attachment patch applied") |
||||
PY |
||||
|
||||
sudo systemctl restart otb_billing.service |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue