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.
309 lines
10 KiB
309 lines
10 KiB
<!doctype html> |
|
<html> |
|
<head> |
|
<title>Edit Invoice</title> |
|
<style> |
|
body { font-family: Arial, sans-serif; margin: 30px; } |
|
.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; } |
|
|
|
.info-box { |
|
border: 1px solid #2563eb; |
|
background: #eff6ff; |
|
color: #10203f; |
|
padding: 10px; |
|
margin-bottom: 15px; |
|
} |
|
.warn-box { |
|
border: 1px solid #aa6600; |
|
background: #fff4dd; |
|
color: #3b2400; |
|
padding: 10px; |
|
margin-bottom: 15px; |
|
} |
|
.error-box { |
|
border: 1px solid red; |
|
padding: 10px; |
|
margin-bottom: 15px; |
|
} |
|
.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, select { background: #ffffff; color: #111827; } |
|
.summary-box { border:1px solid #ccc; padding:10px; max-width:460px; margin:15px 0; } |
|
.summary-box table { width: 100%; } |
|
.summary-box th { text-align:left; padding-right:20px; } |
|
.remove-line { color: #991b1b; } |
|
</style> |
|
<link rel="icon" type="image/png" href="/static/favicon.png"> |
|
</head> |
|
|
|
<body> |
|
|
|
<h1>Edit Invoice</h1> |
|
|
|
<p><a href="/">Home</a></p> |
|
<p><a href="/invoices">Back to Invoices</a></p> |
|
<p><a href="/invoices/view/{{ invoice.id }}">View Invoice</a></p> |
|
|
|
{% if errors %} |
|
<div class="error-box"> |
|
<strong>Please fix the following:</strong> |
|
<ul> |
|
{% for error in errors %} |
|
<li>{{ error }}</li> |
|
{% endfor %} |
|
</ul> |
|
</div> |
|
{% endif %} |
|
|
|
{% if locked %} |
|
<div class="warn-box"> |
|
<strong>This invoice is locked for core edits because payments exist.</strong><br> |
|
Core accounting fields and line items cannot be changed after payment activity begins. Due date and notes can still be updated. |
|
</div> |
|
{% else %} |
|
<div class="info-box"> |
|
<strong>Manual status choices are limited to:</strong> draft, pending, or cancelled.<br> |
|
Partial, paid, and overdue are system-managed from payment activity and due dates. |
|
</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" id="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> |
|
|
|
<h2>Invoice Lines</h2> |
|
<table class="invoice-lines"> |
|
<thead> |
|
<tr> |
|
<th style="width:50%;">Description *</th> |
|
<th>Qty *</th> |
|
<th>Unit Cost *</th> |
|
<th>Line Total</th> |
|
<th></th> |
|
</tr> |
|
</thead> |
|
<tbody id="invoice-lines-body"> |
|
{% set line_items = invoice_items or [{'description':'', 'quantity':'1', 'unit_amount':'', 'line_total':'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" name="item_unit_amount[]" class="line-unit" value="{{ item.unit_amount or '' }}" required></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> |
|
<th>Tax / HST</th> |
|
<td> |
|
<input |
|
type="number" |
|
step="0.00000001" |
|
min="0" |
|
name="tax_amount" |
|
id="tax_amount" |
|
value="{{ invoice.tax_amount or '0' }}" |
|
required |
|
> |
|
<button type="button" id="calc-hst">Set 13% HST</button> |
|
</td> |
|
</tr> |
|
<tr><th>Total Amount</th><td><strong id="summary_total">0.00 CAD</strong></td></tr> |
|
</table> |
|
</div> |
|
{% else %} |
|
<p>Client ID<br><input value="{{ invoice.client_id }}" readonly></p> |
|
<p>Service ID<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> |
|
|
|
{% if locked %} |
|
<p> |
|
System Status<br> |
|
<span class="status-badge status-{{ invoice.status }}">{{ invoice.status }}</span> |
|
</p> |
|
{% else %} |
|
<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="cancelled" {% if invoice.status == 'cancelled' %}selected{% endif %}>cancelled</option> |
|
</select> |
|
</p> |
|
{% endif %} |
|
|
|
<p> |
|
Notes<br> |
|
<textarea name="notes" rows="7" cols="80">{{ invoice.notes or '' }}</textarea> |
|
</p> |
|
|
|
<p> |
|
<button type="submit" name="submit_action" value="save">Save Invoice</button> |
|
<button type="submit" name="submit_action" value="save_send">Save and Send</button> |
|
</p> |
|
|
|
</form> |
|
|
|
{% if not locked %} |
|
<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 taxInput = document.getElementById("tax_amount"); |
|
const totalEl = document.getElementById("summary_total"); |
|
const hstBtn = document.getElementById("calc-hst"); |
|
|
|
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 subtotalValue() { |
|
let subtotal = 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); |
|
subtotal += qty * unit; |
|
}); |
|
return subtotal; |
|
} |
|
|
|
function bindRow(row) { |
|
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; |
|
|
|
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; |
|
subtotal += lineTotal; |
|
row.querySelector(".line-total").textContent = money(lineTotal); |
|
}); |
|
|
|
const tax = Number(taxInput.value || 0); |
|
subtotalEl.textContent = money(subtotal); |
|
totalEl.textContent = money(subtotal + tax); |
|
} |
|
|
|
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 = ""; |
|
}); |
|
|
|
clone.querySelector(".line-total").textContent = money(0); |
|
body.appendChild(clone); |
|
bindRow(clone); |
|
updateSummary(); |
|
}); |
|
|
|
hstBtn.addEventListener("click", function () { |
|
const tax = subtotalValue() * 0.13; |
|
taxInput.value = tax.toFixed(8); |
|
updateSummary(); |
|
}); |
|
|
|
body.querySelectorAll(".invoice-line").forEach(bindRow); |
|
currencySelect.addEventListener("change", updateSummary); |
|
taxInput.addEventListener("input", updateSummary); |
|
taxInput.addEventListener("change", updateSummary); |
|
updateSummary(); |
|
})(); |
|
</script> |
|
{% endif %} |
|
|
|
{% include "footer.html" %} |
|
</body> |
|
</html>
|
|
|