billing frontend for mariadb. setup as otb_billing for outsidethebox.top accounting. also involved with outsidethedb
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

<!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>