12 changed files with 1273 additions and 193 deletions
@ -0,0 +1,5 @@ |
|||||||
|
Flask |
||||||
|
mysql-connector-python |
||||||
|
reportlab |
||||||
|
python-dateutil |
||||||
|
pytz |
||||||
@ -0,0 +1,202 @@ |
|||||||
|
<!doctype html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>Invoice {{ invoice.invoice_number }}</title> |
||||||
|
<style> |
||||||
|
body { |
||||||
|
font-family: Arial, sans-serif; |
||||||
|
margin: 30px; |
||||||
|
color: #111; |
||||||
|
} |
||||||
|
.top-links { |
||||||
|
margin-bottom: 20px; |
||||||
|
} |
||||||
|
.top-links a { |
||||||
|
margin-right: 15px; |
||||||
|
} |
||||||
|
.invoice-wrap { |
||||||
|
max-width: 900px; |
||||||
|
} |
||||||
|
.header-row { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: flex-start; |
||||||
|
margin-bottom: 25px; |
||||||
|
} |
||||||
|
.title-box h1 { |
||||||
|
margin: 0 0 8px 0; |
||||||
|
} |
||||||
|
.status-badge { |
||||||
|
display: inline-block; |
||||||
|
padding: 4px 10px; |
||||||
|
border-radius: 999px; |
||||||
|
font-size: 12px; |
||||||
|
font-weight: bold; |
||||||
|
text-transform: uppercase; |
||||||
|
} |
||||||
|
.status-draft { background: #e5e7eb; color: #111827; } |
||||||
|
.status-pending { background: #dbeafe; color: #1d4ed8; } |
||||||
|
.status-partial { background: #fef3c7; color: #92400e; } |
||||||
|
.status-paid { background: #dcfce7; color: #166534; } |
||||||
|
.status-overdue { background: #fee2e2; color: #991b1b; } |
||||||
|
.status-cancelled { background: #e5e7eb; color: #4b5563; } |
||||||
|
|
||||||
|
.info-grid { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: 1fr 1fr; |
||||||
|
gap: 30px; |
||||||
|
margin-bottom: 25px; |
||||||
|
} |
||||||
|
.info-card { |
||||||
|
border: 1px solid #ccc; |
||||||
|
padding: 15px; |
||||||
|
} |
||||||
|
.info-card h3 { |
||||||
|
margin-top: 0; |
||||||
|
} |
||||||
|
.summary-table, |
||||||
|
.total-table { |
||||||
|
width: 100%; |
||||||
|
border-collapse: collapse; |
||||||
|
margin-bottom: 25px; |
||||||
|
} |
||||||
|
.summary-table th, |
||||||
|
.summary-table td, |
||||||
|
.total-table th, |
||||||
|
.total-table td { |
||||||
|
border: 1px solid #ccc; |
||||||
|
padding: 10px; |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
.total-table { |
||||||
|
max-width: 420px; |
||||||
|
margin-left: auto; |
||||||
|
} |
||||||
|
.notes-box { |
||||||
|
border: 1px solid #ccc; |
||||||
|
padding: 15px; |
||||||
|
white-space: pre-wrap; |
||||||
|
margin-top: 20px; |
||||||
|
} |
||||||
|
.print-only { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
@media print { |
||||||
|
.top-links, |
||||||
|
.screen-only, |
||||||
|
footer { |
||||||
|
display: none !important; |
||||||
|
} |
||||||
|
.print-only { |
||||||
|
display: block; |
||||||
|
} |
||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
|
||||||
|
<div class="invoice-wrap"> |
||||||
|
<div class="top-links screen-only"> |
||||||
|
<a href="/">Home</a> |
||||||
|
<a href="/invoices">Back to Invoices</a> |
||||||
|
<a href="/invoices/edit/{{ invoice.id }}">Edit Invoice</a> |
||||||
|
<a href="#" onclick="window.print(); return false;">Print</a> |
||||||
|
<a href="/invoices/pdf/{{ invoice.id }}">PDF</a> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="header-row"> |
||||||
|
<div class="title-box"> |
||||||
|
<h1>Invoice {{ invoice.invoice_number }}</h1> |
||||||
|
<span class="status-badge status-{{ invoice.status }}">{{ invoice.status }}</span> |
||||||
|
</div> |
||||||
|
<div style="text-align:right;"> |
||||||
|
<strong>{{ settings.business_name or 'OTB Billing' }}</strong><br> |
||||||
|
{{ settings.business_tagline or '' }}<br> |
||||||
|
{% if settings.business_address %}{{ settings.business_address }}<br>{% endif %} |
||||||
|
{% if settings.business_email %}{{ settings.business_email }}<br>{% endif %} |
||||||
|
{% if settings.business_phone %}{{ settings.business_phone }}<br>{% endif %} |
||||||
|
{% if settings.business_website %}{{ settings.business_website }}{% endif %} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="info-grid"> |
||||||
|
<div class="info-card"> |
||||||
|
<h3>Bill To</h3> |
||||||
|
<strong>{{ invoice.company_name }}</strong><br> |
||||||
|
{% if invoice.contact_name %}{{ invoice.contact_name }}<br>{% endif %} |
||||||
|
{% if invoice.email %}{{ invoice.email }}<br>{% endif %} |
||||||
|
{% if invoice.phone %}{{ invoice.phone }}<br>{% endif %} |
||||||
|
Client Code: {{ invoice.client_code }} |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="info-card"> |
||||||
|
<h3>Invoice Details</h3> |
||||||
|
Invoice #: {{ invoice.invoice_number }}<br> |
||||||
|
Issued: {{ invoice.issued_at|localtime }}<br> |
||||||
|
Due: {{ invoice.due_at|localtime }}<br> |
||||||
|
{% if invoice.paid_at %}Paid: {{ invoice.paid_at|localtime }}<br>{% endif %} |
||||||
|
Currency: {{ invoice.currency_code }}<br> |
||||||
|
{% if settings.tax_number %}{{ settings.tax_label or 'Tax' }} Number: {{ settings.tax_number }}<br>{% endif %} |
||||||
|
{% if settings.business_number %}Business Number: {{ settings.business_number }}{% endif %} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<table class="summary-table"> |
||||||
|
<tr> |
||||||
|
<th>Service Code</th> |
||||||
|
<th>Service</th> |
||||||
|
<th>Description</th> |
||||||
|
<th>Total</th> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<td>{{ invoice.service_code or '-' }}</td> |
||||||
|
<td>{{ invoice.service_name or '-' }}</td> |
||||||
|
<td>{{ invoice.notes or '-' }}</td> |
||||||
|
<td>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
|
||||||
|
<table class="total-table"> |
||||||
|
<tr> |
||||||
|
<th>Subtotal</th> |
||||||
|
<td>{{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th>{{ settings.tax_label or 'Tax' }}</th> |
||||||
|
<td>{{ invoice.tax_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th>Total</th> |
||||||
|
<td><strong>{{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }}</strong></td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th>Paid</th> |
||||||
|
<td>{{ invoice.amount_paid|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th>Remaining</th> |
||||||
|
<td>{{ (invoice.total_amount - invoice.amount_paid)|money(invoice.currency_code) }} {{ invoice.currency_code }}</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
|
||||||
|
{% if settings.payment_terms %} |
||||||
|
<div class="notes-box"> |
||||||
|
<strong>Payment Terms</strong><br><br> |
||||||
|
{{ settings.payment_terms }} |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
|
||||||
|
{% if settings.invoice_footer %} |
||||||
|
<div class="notes-box"> |
||||||
|
<strong>Footer</strong><br><br> |
||||||
|
{{ settings.invoice_footer }} |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
|
||||||
|
{% include "footer.html" %} |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,169 @@ |
|||||||
|
<!doctype html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>Settings</title> |
||||||
|
<style> |
||||||
|
body { font-family: Arial, sans-serif; } |
||||||
|
.form-grid { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: 1fr 1fr; |
||||||
|
gap: 24px; |
||||||
|
max-width: 1100px; |
||||||
|
} |
||||||
|
.card { |
||||||
|
border: 1px solid #ccc; |
||||||
|
padding: 16px; |
||||||
|
} |
||||||
|
.card h2 { |
||||||
|
margin-top: 0; |
||||||
|
} |
||||||
|
input[type="text"], |
||||||
|
input[type="email"], |
||||||
|
input[type="password"], |
||||||
|
input[type="number"], |
||||||
|
textarea, |
||||||
|
select { |
||||||
|
width: 100%; |
||||||
|
box-sizing: border-box; |
||||||
|
margin-top: 4px; |
||||||
|
margin-bottom: 12px; |
||||||
|
padding: 8px; |
||||||
|
} |
||||||
|
textarea { min-height: 90px; } |
||||||
|
.checkbox-row { |
||||||
|
margin: 8px 0 14px 0; |
||||||
|
} |
||||||
|
.save-row { |
||||||
|
margin-top: 18px; |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
|
||||||
|
<h1>Settings / Config</h1> |
||||||
|
|
||||||
|
<p><a href="/">Home</a></p> |
||||||
|
|
||||||
|
<form method="post"> |
||||||
|
<div class="form-grid"> |
||||||
|
<div class="card"> |
||||||
|
<h2>Business Identity</h2> |
||||||
|
|
||||||
|
Business Name<br> |
||||||
|
<input type="text" name="business_name" value="{{ settings.business_name }}"><br> |
||||||
|
|
||||||
|
Slogan / Tagline<br> |
||||||
|
<input type="text" name="business_tagline" value="{{ settings.business_tagline }}"><br> |
||||||
|
|
||||||
|
Business Email<br> |
||||||
|
<input type="email" name="business_email" value="{{ settings.business_email }}"><br> |
||||||
|
|
||||||
|
Business Phone<br> |
||||||
|
<input type="text" name="business_phone" value="{{ settings.business_phone }}"><br> |
||||||
|
|
||||||
|
Business Address<br> |
||||||
|
<textarea name="business_address">{{ settings.business_address }}</textarea><br> |
||||||
|
|
||||||
|
Website<br> |
||||||
|
<input type="text" name="business_website" value="{{ settings.business_website }}"><br> |
||||||
|
|
||||||
|
Business Number / Registration Number<br> |
||||||
|
<input type="text" name="business_number" value="{{ settings.business_number }}"><br> |
||||||
|
|
||||||
|
Default Currency<br> |
||||||
|
<select name="default_currency"> |
||||||
|
<option value="CAD" {% if settings.default_currency == 'CAD' %}selected{% endif %}>CAD</option> |
||||||
|
<option value="USD" {% if settings.default_currency == 'USD' %}selected{% endif %}>USD</option> |
||||||
|
<option value="ETHO" {% if settings.default_currency == 'ETHO' %}selected{% endif %}>ETHO</option> |
||||||
|
<option value="EGAZ" {% if settings.default_currency == 'EGAZ' %}selected{% endif %}>EGAZ</option> |
||||||
|
<option value="ALT" {% if settings.default_currency == 'ALT' %}selected{% endif %}>ALT</option> |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="card"> |
||||||
|
<h2>Tax Settings</h2> |
||||||
|
|
||||||
|
Local Country<br> |
||||||
|
<input type="text" name="local_country" value="{{ settings.local_country }}"><br> |
||||||
|
|
||||||
|
Tax Label<br> |
||||||
|
<input type="text" name="tax_label" value="{{ settings.tax_label }}"><br> |
||||||
|
|
||||||
|
Tax Rate (%)<br> |
||||||
|
<input type="number" step="0.01" name="tax_rate" value="{{ settings.tax_rate }}"><br> |
||||||
|
|
||||||
|
Tax Number<br> |
||||||
|
<input type="text" name="tax_number" value="{{ settings.tax_number }}"><br> |
||||||
|
|
||||||
|
<div class="checkbox-row"> |
||||||
|
<label> |
||||||
|
<input type="checkbox" name="apply_local_tax_only" value="1" {% if settings.apply_local_tax_only == '1' %}checked{% endif %}> |
||||||
|
Apply tax only to local clients |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
|
||||||
|
Payment Terms<br> |
||||||
|
<textarea name="payment_terms">{{ settings.payment_terms }}</textarea><br> |
||||||
|
|
||||||
|
Invoice Footer<br> |
||||||
|
<textarea name="invoice_footer">{{ settings.invoice_footer }}</textarea><br> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="card"> |
||||||
|
<h2>Email / SMTP</h2> |
||||||
|
|
||||||
|
SMTP Host<br> |
||||||
|
<input type="text" name="smtp_host" value="{{ settings.smtp_host }}"><br> |
||||||
|
|
||||||
|
SMTP Port<br> |
||||||
|
<input type="number" name="smtp_port" value="{{ settings.smtp_port }}"><br> |
||||||
|
|
||||||
|
SMTP Username<br> |
||||||
|
<input type="text" name="smtp_user" value="{{ settings.smtp_user }}"><br> |
||||||
|
|
||||||
|
SMTP Password<br> |
||||||
|
<input type="password" name="smtp_pass" value="{{ settings.smtp_pass }}"><br> |
||||||
|
|
||||||
|
From Email<br> |
||||||
|
<input type="email" name="smtp_from_email" value="{{ settings.smtp_from_email }}"><br> |
||||||
|
|
||||||
|
From Name<br> |
||||||
|
<input type="text" name="smtp_from_name" value="{{ settings.smtp_from_name }}"><br> |
||||||
|
|
||||||
|
<div class="checkbox-row"> |
||||||
|
<label> |
||||||
|
<input type="checkbox" name="smtp_use_tls" value="1" {% if settings.smtp_use_tls == '1' %}checked{% endif %}> |
||||||
|
Use TLS |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="checkbox-row"> |
||||||
|
<label> |
||||||
|
<input type="checkbox" name="smtp_use_ssl" value="1" {% if settings.smtp_use_ssl == '1' %}checked{% endif %}> |
||||||
|
Use SSL |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="card"> |
||||||
|
<h2>Notes</h2> |
||||||
|
<p> |
||||||
|
These settings become the identity and delivery configuration for this installation. |
||||||
|
</p> |
||||||
|
<p> |
||||||
|
Email sending is not wired yet, but these SMTP settings are stored now so the next step can use them. |
||||||
|
</p> |
||||||
|
<p> |
||||||
|
Tax settings are also stored now so invoice and automation logic can use them later. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="save-row"> |
||||||
|
<button type="submit">Save Settings</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
|
||||||
|
{% include "footer.html" %} |
||||||
|
</body> |
||||||
|
</html> |
||||||
Loading…
Reference in new issue