You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
218 lines
7.7 KiB
218 lines
7.7 KiB
<!doctype html> |
|
<html> |
|
<head> |
|
<title>New Invoice</title> |
|
<link rel="icon" type="image/png" href="/static/favicon.png"> |
|
<style> |
|
body { font-family: Arial, sans-serif; margin: 30px; } |
|
.invoice-lines { width: 100%; border-collapse: collapse; margin: 15px 0; } |
|
.invoice-lines th, .invoice-lines td { border: 1px solid #ccc; padding: 8px; vertical-align: top; } |
|
.invoice-lines th { background: #f3f4f6; color: #111827; text-align: left; } |
|
.invoice-lines textarea { width: 100%; min-height: 70px; } |
|
.invoice-lines input[type="number"] { width: 120px; } |
|
.invoice-lines input, .invoice-lines textarea { background: #ffffff; color: #111827; } |
|
.summary-box { border:1px solid #ccc; padding:10px; max-width:420px; margin:15px 0; } |
|
.summary-box table { width: 100%; } |
|
.summary-box th { text-align:left; padding-right:20px; } |
|
.remove-line { color: #991b1b; } |
|
</style> |
|
</head> |
|
|
|
<body> |
|
|
|
<h1>Create Invoice</h1> |
|
<p> |
|
<strong>Invoice Number</strong><br> |
|
This invoice number will be generated automatically when the invoice is created. |
|
</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> |
|
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" id="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> |
|
|
|
<h2>Invoice Lines</h2> |
|
<table class="invoice-lines"> |
|
<thead> |
|
<tr> |
|
<th style="width:50%;">Description *</th> |
|
<th>Qty *</th> |
|
<th>Unit Cost *</th> |
|
<th>HST</th> |
|
<th>Line Total</th> |
|
<th></th> |
|
</tr> |
|
</thead> |
|
<tbody id="invoice-lines-body"> |
|
{% set line_items = form_data.get('line_items') or [{'description':'', 'quantity':'1', 'unit_amount':'', 'taxable':'0'}] %} |
|
{% for item in line_items %} |
|
<tr class="invoice-line"> |
|
<td><textarea name="item_description[]" required>{{ item.description }}</textarea></td> |
|
<td><input type="number" step="0.0001" min="0.0001" name="item_quantity[]" class="line-qty" value="{{ item.quantity or '1' }}" required></td> |
|
<td><input type="number" step="0.00000001" min="0.00000001" name="item_unit_amount[]" class="line-unit" value="{{ item.unit_amount or '' }}" required></td> |
|
<td> |
|
<input type="hidden" name="item_taxable[]" class="line-taxable-hidden" value="{{ item.taxable or '0' }}"> |
|
<input type="checkbox" class="line-taxable" value="1" {% if item.taxable == '1' %}checked{% endif %}> |
|
</td> |
|
<td class="line-total">0.00 CAD</td> |
|
<td><button type="button" class="remove-line">Remove</button></td> |
|
</tr> |
|
{% endfor %} |
|
</tbody> |
|
</table> |
|
|
|
<p><button type="button" id="add-line">+ Add Line</button></p> |
|
|
|
<div class="summary-box"> |
|
<strong>Invoice Summary</strong><br><br> |
|
<table> |
|
<tr><th>Subtotal</th><td id="summary_subtotal">0.00 CAD</td></tr> |
|
<tr id="summary_tax_row" style="display:none;"><th>HST 13%</th><td id="summary_tax">0.00 CAD</td></tr> |
|
<tr><th>Total Amount</th><td><strong id="summary_total">0.00 CAD</strong></td></tr> |
|
</table> |
|
</div> |
|
|
|
<p> |
|
Due Date *<br> |
|
<input type="date" name="due_at" value="{{ form_data.get('due_at', '') }}" required> |
|
</p> |
|
|
|
<p><button type="submit">Create Invoice</button></p> |
|
|
|
</form> |
|
|
|
<script> |
|
(function () { |
|
const body = document.getElementById("invoice-lines-body"); |
|
const addBtn = document.getElementById("add-line"); |
|
const currencySelect = document.getElementById("currency_code"); |
|
const subtotalEl = document.getElementById("summary_subtotal"); |
|
const taxRow = document.getElementById("summary_tax_row"); |
|
const taxEl = document.getElementById("summary_tax"); |
|
const totalEl = document.getElementById("summary_total"); |
|
|
|
function decimals() { |
|
return (currencySelect.value || "CAD") === "CAD" ? 2 : 8; |
|
} |
|
|
|
function money(value) { |
|
const n = Number(value || 0); |
|
return n.toFixed(decimals()) + " " + (currencySelect.value || "CAD"); |
|
} |
|
|
|
function bindRow(row) { |
|
const taxableBox = row.querySelector(".line-taxable"); |
|
const taxableHidden = row.querySelector(".line-taxable-hidden"); |
|
|
|
taxableBox.addEventListener("change", function () { |
|
taxableHidden.value = taxableBox.checked ? "1" : "0"; |
|
updateSummary(); |
|
}); |
|
|
|
row.querySelectorAll("input, textarea").forEach(function (el) { |
|
el.addEventListener("input", updateSummary); |
|
el.addEventListener("change", updateSummary); |
|
}); |
|
|
|
row.querySelector(".remove-line").addEventListener("click", function () { |
|
if (body.querySelectorAll(".invoice-line").length > 1) { |
|
row.remove(); |
|
updateSummary(); |
|
} |
|
}); |
|
} |
|
|
|
function updateSummary() { |
|
let subtotal = 0; |
|
let tax = 0; |
|
|
|
body.querySelectorAll(".invoice-line").forEach(function (row) { |
|
const qty = Number(row.querySelector(".line-qty").value || 0); |
|
const unit = Number(row.querySelector(".line-unit").value || 0); |
|
const lineTotal = qty * unit; |
|
const taxable = row.querySelector(".line-taxable").checked; |
|
|
|
subtotal += lineTotal; |
|
if (taxable) { |
|
tax += lineTotal * 0.13; |
|
} |
|
|
|
row.querySelector(".line-total").textContent = money(lineTotal); |
|
row.querySelector(".line-taxable-hidden").value = taxable ? "1" : "0"; |
|
}); |
|
|
|
subtotalEl.textContent = money(subtotal); |
|
taxEl.textContent = money(tax); |
|
totalEl.textContent = money(subtotal + tax); |
|
taxRow.style.display = tax > 0 ? "table-row" : "none"; |
|
} |
|
|
|
addBtn.addEventListener("click", function () { |
|
const first = body.querySelector(".invoice-line"); |
|
const clone = first.cloneNode(true); |
|
|
|
clone.querySelectorAll("textarea").forEach(el => el.value = ""); |
|
clone.querySelectorAll("input").forEach(function (el) { |
|
if (el.classList.contains("line-qty")) el.value = "1"; |
|
else if (el.classList.contains("line-unit")) el.value = ""; |
|
else if (el.classList.contains("line-taxable-hidden")) el.value = "0"; |
|
else if (el.classList.contains("line-taxable")) el.checked = false; |
|
}); |
|
|
|
clone.querySelector(".line-total").textContent = money(0); |
|
body.appendChild(clone); |
|
bindRow(clone); |
|
updateSummary(); |
|
}); |
|
|
|
body.querySelectorAll(".invoice-line").forEach(bindRow); |
|
currencySelect.addEventListener("change", updateSummary); |
|
updateSummary(); |
|
})(); |
|
</script> |
|
|
|
</body> |
|
</html> |
|
{% include "footer.html" %}
|
|
|