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.
 
 
 
 

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" %}