In case you want to do extensive Theme
work beyond what Theme Enhancer
can do for you, you need to be familiar with DOM and javascript.
That aside, you just need to create a New Theme and you will get this code:
Default theme code
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Styling for consistent look and feel -->
<style>
*, ::after, ::before, ::backdrop, ::file-selector-button {
margin: 0;
padding: 0;
}
*, ::after, ::before, ::backdrop, ::file-selector-button {
box-sizing: border-box;
border: 0 solid;
}
body {
margin: 0;
padding: 30px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #171717;
font-size: 12px;
line-height: 1.428571429;
min-width: 800px;
}
table {
font-size: 12px;
}
tr#table-headers th {
font-weight: bold;
padding: 5px 10px;
border: 1px solid #000;
text-align: start
}
tbody#table-rows td {
padding: 5px 10px;
border-left: 1px solid #000;
border-right: 1px solid #000;
text-align: start;
vertical-align: top
}
tbody#table-rows tr:last-child td {
padding-bottom: 30px;
border-bottom: 1px solid #000;
}
</style>
</head>
<body>
<!-- Top section: document title and optional business logo -->
<table style="margin-bottom: 20px; width: 100%">
<tbody>
<tr>
<td style="vertical-align: top">
<div style="font-size: 32px; line-height: 32px; font-weight: bold" id="title">Invoice</div>
</td>
<td style="text-align: end" id="business-logo">
</td>
</tr>
</tbody>
</table>
<!-- Second section: recipient info, document fields (like date, invoice #), and business info -->
<table style="margin-bottom: 20px; width: 100%">
<tbody>
<tr>
<td style="vertical-align: top" id="recipient-info"></td>
<td style="text-align: end; vertical-align: top" id="fields"></td>
<td style="width: 20px"></td>
<td style="width: 1px; border-left-width: 1px; border-left-color: #000; border-left-style: solid"></td>
<td style="width: 20px"></td>
<td style="width: 1px; white-space: nowrap; vertical-align: top" id="business-info"></td>
</tr>
</tbody>
</table>
<!-- Description block -->
<div style="font-weight: bold; font-size: 14px; margin-bottom: 20px" id="description"></div>
<!-- Main table containing column headers, line items, and totals -->
<table style="border-collapse: collapse; width: 100%">
<thead>
<tr id="table-headers"></tr>
</thead>
<tbody id="table-rows">
</tbody>
<tbody id="totals">
</tbody>
</table>
<script src="resources/qrcode/qrcode.js"></script>
<div id="qrcode" style="margin-bottom: 20px"></div>
<!-- Section for any additional custom fields -->
<div id="custom-fields"></div>
<!-- Section for footers -->
<table><tr><td><div id="footers"></div></td></tr></table>
<!-- Section for final status (e.g. PAID, VOID) with special styling -->
<div id="status" style="text-align: center"></div>
<script>
// Listen for messages sent from the parent frame (via postMessage API)
window.addEventListener("message", (event) => {
if (event.source !== window.parent) return; // Only accept messages from parent
if (event.data.type !== 'context-response') return; // Ignore irrelevant messages
// Extract the main data object sent from parent
const data = event.data.body;
// Set text direction (LTR or RTL) for the whole document
document.documentElement.dir = data.direction;
// Populate title and description if provided
document.getElementById("title").innerHTML = data.title || "No title";
document.getElementById("description").innerHTML = data.description || "";
// Inject business logo image if available
var businessLogoTd = document.getElementById("business-logo");
if (data.business.logo) {
const img = document.createElement("img");
img.src = data.business.logo;
img.style = "max-height: 150px; max-width: 300px; display: inline";
businessLogoTd.appendChild(img);
}
// Populate business info section with name and address (convert line breaks)
const business = data.business || {};
document.getElementById("business-info").innerHTML = `<strong>${business.name || ""}</strong><br>${business.address ? business.address.replace(/\n/g, "<br>") : ""}`;
// Populate recipient info section with name and address (convert line breaks)
const recipient = data.recipient || {};
document.getElementById("recipient-info").innerHTML = `<strong>${recipient.name || ""}</strong><br>${recipient.address ? recipient.address.replace(/\n/g, "<br>") : ""}`;
// Insert fields (e.g. issue date, due date) into right-hand side
const fieldsDiv = document.getElementById("fields");
fieldsDiv.innerHTML = "";
(data.fields || []).forEach(f => {
const div = document.createElement("div");
div.innerHTML = `<strong>${f.label}</strong><br />${f.text || ""}<br /><br />`;
fieldsDiv.appendChild(div);
});
// Build table headers dynamically based on `columns` definition
const headersRow = document.getElementById("table-headers");
headersRow.innerHTML = "";
(data.table.columns || []).forEach(col => {
const th = document.createElement("th");
th.innerHTML = col.label;
th.style.textAlign = col.align;
if (col.minWidth) {
th.style.whiteSpace = 'nowrap';
th.style.width = '1px';
}
else if (col.nowrap) {
th.style.whiteSpace = 'nowrap';
th.style.width = '80px';
}
headersRow.appendChild(th);
});
// Populate main table body with rows and alignments based on column definitions
const rowsBody = document.getElementById("table-rows");
rowsBody.innerHTML = "";
(data.table.rows || []).forEach(row => {
const tr = document.createElement("tr");
row.cells.forEach((cell, i) => {
var col = data.table.columns[i];
const td = document.createElement("td");
td.innerHTML = (cell.text || "").split("\n").join("<br />");
td.style.textAlign = col.align;
if (col.minWidth) {
td.style.whiteSpace = 'nowrap';
td.style.width = '1px';
}
else if (col.nowrap) {
td.style.whiteSpace = 'nowrap';
td.style.width = '80px';
}
tr.appendChild(td);
});
rowsBody.appendChild(tr);
});
// Populate totals section with subtotals, taxes, grand total, etc.
const totalsBody = document.getElementById("totals");
totalsBody.innerHTML = "";
(data.table.totals || []).forEach(total => {
const tr = document.createElement("tr");
const tdLabel = document.createElement("td");
const tdValue = document.createElement("td");
tdLabel.innerHTML = total.label;
tdLabel.colSpan = data.table.columns.length - 1;
tdLabel.style = 'padding: 5px 10px; text-align: end; vertical-align: top';
tdValue.innerHTML = total.text;
tdValue.id = total.key;
if (total.class) tdValue.classList.add(total.class);
tdValue.dataset.value = total.number;
tdValue.style = 'padding: 5px 10px; border: 1px solid #000; text-align: right; white-space: nowrap; vertical-align: top';
// Bold totals if marked as 'emphasis'
if (total.emphasis) {
tdLabel.style.fontWeight = 'bold';
tdValue.style.fontWeight = 'bold';
}
tr.appendChild(tdLabel);
tr.appendChild(tdValue);
totalsBody.appendChild(tr);
});
// Render custom fields section below the table (e.g. notes, terms)
const customFieldsDiv = document.getElementById("custom-fields");
customFieldsDiv.innerHTML = "";
(data.custom_fields || []).forEach(f => {
const div = document.createElement("div");
div.innerHTML = `<strong>${f.label || ""}</strong><br />${(f.text || "").split("\n").join("<br />")}<br /><br />`;
customFieldsDiv.appendChild(div);
});
// Render footers section
const footersDiv = document.getElementById("footers");
footersDiv.innerHTML = "";
(data.footers || []).forEach(f => {
const div = document.createElement("div");
div.style = 'margin-top: 20px';
div.innerHTML = f;
footersDiv.appendChild(div);
// Find and execute any script tags
const scripts = div.querySelectorAll("script");
scripts.forEach(script => {
const newScript = document.createElement("script");
// Copy script attributes if needed
for (const attr of script.attributes) {
newScript.setAttribute(attr.name, attr.value);
}
// Inline script handling
if (script.textContent) {
newScript.textContent = script.textContent;
}
// Replace the old <script> with the new one so it executes
script.parentNode.replaceChild(newScript, script);
});
});
// Display status label (e.g. PAID, CANCELLED) with colored border based on status type
const statusDiv = document.getElementById("status");
if (data.emphasis?.text != null) {
statusDiv.style.marginTop = '40px';
const span = document.createElement("span");
span.style = 'border-width: 5px; border-color: #FF0000; border-style: solid; padding: 10px; font-size: 20px; text-transform: uppercase';
if (data.emphasis.positive) {
span.style.color = 'green';
span.style.borderColor = 'green';
}
if (data.emphasis.negative) {
span.style.color = 'red';
span.style.borderColor = 'red';
}
span.innerHTML = data.emphasis.text;
statusDiv.appendChild(span);
}
// This handles QR code on invoices for Saudi Arabia - this is here just temporarily. Better way will be implemented.
if (data.legacyQrCodeForSaudiArabia) {
function appendTLV(tag, text, byteList) {
const encoded = new TextEncoder().encode(text);
byteList.push(tag);
byteList.push(encoded.length);
for (let b of encoded) byteList.push(b);
}
const byteList = [];
let businessName = 'No name';
if (data.business) businessName = data.business.name;
let vatNumber = '0000000000000';
let vatField = data.business.custom_fields.find(item => item.key === "d96d97e8-c857-42c6-8360-443c06a13de9");
if (vatField) vatNumber = vatField.text;
let timestamp = new Date((data.timestamp - 621355968000000000) / 10000).toISOString();
let total = 0;
let totalElement = document.getElementById('Total');
if (totalElement != null) total = parseFloat(totalElement.getAttribute('data-value'));
let vat = 0;
let taxAmounts = document.getElementsByClassName('taxAmount');
for (let i = 0; i < taxAmounts.length; i++) {
vat += parseFloat(taxAmounts[i].getAttribute('data-value'));
}
appendTLV(1, businessName, byteList);
appendTLV(2, vatNumber, byteList);
appendTLV(3, timestamp, byteList);
appendTLV(4, total.toFixed(2), byteList);
appendTLV(5, vat.toFixed(2), byteList);
// Convert to Uint8Array
const tlvBytes = Uint8Array.from(byteList);
// Convert to Base64
const qrData = btoa(String.fromCharCode(...tlvBytes));
new QRCode(document.getElementById("qrcode"), {
text: qrData,
width: 128,
height: 128,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.L
});
}
window.parent.postMessage({ type: "resize", width: document.documentElement.scrollWidth+1, height: document.documentElement.scrollHeight+1 }, "*");
}, false);
// Request context data from parent frame when page loads
window.addEventListener("load", () =>
window.parent.postMessage({ type: "context-request" }, "*")
);
</script>
</body>
</html>
How to change code
-
To change the styling, you can inject css
code inside the <style>
tag at the top. You can use the classes and ids available in the <body>
of the document.
-
To change the order of sections in the document, you can edit the contents of <body>
tag which holds the bare html
skeleton of your document. Make sure you don’t delete or change any of the id
or class
attributes as well as the <script>
since these are essential for the document to work.
-
Finally, you have the <script>
tag that is the last element inside your <body>
which injects actual content inside the html
skeleton. Normally, you don’t need to change anything here but in case you want to selectively treat individual fields, you can by reading the comments – which are done adequately, if I may say so – and this will give you an idea where to change the code.
As I said, direct editing of Themes
requires some IT skills. If you find this too difficult, I think Theme Enhancer
could do a very good job at this. You can tell the Theme Enhancer to save the theme for you and you can pick up from there.