Invoice and Packing Slip Themes

As we did in Quickbooks, we wanted to generate an Invoice and Packing Slip from the same Sales Invoice.

We created Invoice and Packing Slip Themes.

It Requires 3 Text Custom Fields:

Here’s the Invoice Theme:

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	
	<!-- CSS CUSTOMIZATION SECTION -->
	<style>
		/* PAGE SETUP - Controls print layout and margins */
		@page {
			/* A4 size is default but some web-browsers do not offer "Scale" option if A4 is set explicitly */
			/* Uncomment to force A4 paper size: */
			/*
            size: 8.5in 11in;
			*/
			
			/* Page margins - adjust these for more/less white space around content */
			margin: 5mm;
			
			/* Uncomment if you want page number in the footer */
			/*
			@bottom-center {
				content: counter(page);
				font-size: 0.8em;
			}
			*/
		}

		/* CSS RESET - Ensures consistent styling across browsers */
		*, ::after, ::before, ::backdrop, ::file-selector-button {
			margin: 0;
			padding: 0;
		}

		*, ::after, ::before, ::backdrop, ::file-selector-button {
			box-sizing: border-box;
			border: 0 solid;
		}

		/* BODY STYLES - Main document styling */
		body {
			margin: 0;
			padding: 30px; /* Space around document content */
			font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; /* Change font family here */
			color: #171717; /* Main text color */
			font-size: 12px; /* Base font size for document */
			line-height: 1.428571429;
			min-width: 800px; /* Minimum width to prevent layout breaking */
		}

		/* ADDRESS STYLING - For business and recipient addresses */
		address {
			font-style: normal; /* Remove italic styling */
			line-height: 1.5em;
		}

		/* DEFINITION LIST STYLING - Used for invoice fields (date, number, etc.) */
		dt {
			font-weight: bold;
			margin: 0 0 2px 0; /* Small gap below label */
		}

		dd {
			margin: 0 0 4px 0; /* Bigger gap below value */
		}

		dd:last-of-type {
			margin-bottom: 0; /* No gap after last pair */
		}

		/* PRINT STYLES - Applied when printing or generating PDF */
		@media print {
			body {
				padding: 0; /* Remove padding for print */
				min-width: auto; /* Allow natural width for print */
			}
		}

		/* TABLE STYLES - Main table containing line items */
		table {
			font-size: 12px;
			width: 100%;
		}

		/* TABLE HEADERS - Column headers for the line items table */
		tr#table-headers th {
			font-weight: bold;
			padding: 5px 10px; /* Cell padding */
			border: 1px solid #000; /* Header border color */
			text-align: start
		}

		/* TABLE CELLS - Basic cell styling */
		tbody#table-rows td {
			padding: 5px 10px; /* Cell padding */			
			text-align: start;
			vertical-align: top
		}

		/* LINE ITEM ROWS - Styling for each line item */
		tbody#table-rows tr.row td {
			border-left: 1px solid #000; /* Side borders for cells */
			border-right: 1px solid #000;
		}

		/* LAST ROW - Special styling for the last line item */
		tbody#table-rows tr.last-row td {
			padding-bottom: 30px; /* Extra space before totals */
			border-bottom: 1px solid #000; /* Bottom border */
		}

		/* COLUMN TOTALS - Sum row at bottom of columns (if enabled) */
		tbody#table-rows tr.column-total td {
			font-weight: bold;
			border: 1px solid #000;
			white-space: nowrap;
			text-align: right;
		}

		/* TOTALS SECTION - Subtotal, tax, total rows */
		tbody#table-rows tr.total td {
			white-space: nowrap; /* Prevent line breaks */
		}

		/* TOTAL LABELS - Right-aligned labels (e.g., "Subtotal:") */
		tbody#table-rows tr.total td:first-child {
			text-align: end;
		}

		/* TOTAL VALUES - Amount cells with border */
		tbody#table-rows tr.total td:last-child {
			border: 1px solid #000; /* Border around amounts */
			text-align: right;
		}
	</style>
</head>
<body>
<!--	<p id="debug" style="overflow-wrap: break-word"></p> -->

	<!-- MAIN LAYOUT TABLE - Used to ensure proper page breaks and header repetition -->
	<table>
		<thead>
			<!-- REPEATING HEADER SECTION -->
			<!-- The contents of the <thead> element will automatically repeat at the top of each printed page -->
			<!-- If you want anything here not to repeat on every page, move the relevant blocks into <tbody> -->
			<tr>
				<table>
					<tr>
						<td>
							<!-- INFO SECTION - Recipient, fields, and business details -->
							<section style="display: flex; margin-bottom: 20px; width: 100%; align-items: flex-start; gap: 20px">
								<!-- Business logo container - image will be inserted here -->
								<div id="business-logo" style="text-align: end"></div>

								<!-- Vertical separator line -->
<!--								<div aria-hidden="true" style="width: 1px; border-left: 1px solid #000; align-self: stretch "></div> -->

								<!-- Business address (right side) -->
								<address id="business-info" style="white-space: nowrap"></address>

								<!-- DOCUMENT HEADER - Title -->
								<header style="width: 2in; text-align: center; vertical-align: top; margin-bottom: 20px">
									<!-- Document title (e.g., "Tax Invoice") - populated by JavaScript -->
									<h1 id="title" style="font-size: 32px; line-height: 32px; font-weight: bold; text-align: center"></h1>
								</header>

								<!-- Document fields (invoice number, date, etc.) - middle -->
								<dl id="fields" style="flex: 1; text-align: end"></dl>
							</section>
						</td>
					</tr>
				</table>
			</tr>
			<tr>
				<td>
					<!-- INFO SECTION - Recipient, fields, and business details -->
					<section style="display: flex; margin-bottom: 20px; width: 100%; align-items: flex-start; gap: 20px">
						<b>Bill To:</b><br>
						<!-- Recipient/customer address (left side) -->
						<address id="recipient-billto" style="flex: 1"></address>

						<b>Ship To:</b><br>
						<!-- Recipient/customer address (left side) -->
						<address id="recipient-shipto" style="flex: 1"></address>
					</section>

					<!-- Optional description line (e.g., "Professional services") -->
					<p style="font-weight: bold; font-size: 14px; margin-bottom: 20px" id="description"></p>
				</td>
			</tr>
		</thead>
		<tbody>
			<!-- NON-REPEATING CONTENT SECTION -->
			<tr>
				<td>
					<!-- MAIN LINE ITEMS TABLE -->
					<!-- This table contains column headers, line items, and totals -->
					<table style="border-collapse: collapse; width: 100%">
						<thead>
							<!-- Table headers row - populated dynamically by JavaScript -->
							<tr id="table-headers"></tr>
						</thead>
						<tbody id="table-rows">
							<!-- Line items and totals will be inserted here by JavaScript -->
						</tbody>
					</table>

					<!-- QR CODE SECTION - For special features like Saudi Arabia e-invoicing -->
					<script src="resources/qrcode/qrcode.js"></script>
					<script src="resources/writtennumber/writtennumber.js"></script>
					<div id="qrcode" style="margin-bottom: 20px"></div>

					<!-- CUSTOM FIELDS SECTION - Notes, terms, and other custom content -->
					<div id="custom-fields"></div>

					<!-- FOOTERS SECTION - Can contain HTML or custom scripts -->
					<table><tr><td><div id="footers"></div></td></tr></table>
					<p style="position:absolute; bottom:0; color:red">
						<b>Thank you for your business! We appreciate your prompt attention to this Invoice.</b></p>
					<!-- STATUS SECTION - For stamps like PAID, VOID, CANCELLED -->
					<div id="status" style="text-align: center"></div>
				</td>
			</tr>
		</tbody>
	</table>

	<!-- JAVASCRIPT SECTION - Handles dynamic content population -->
	<script>
		/**
		 * Sends resize message to parent frame when content changes
		 * This ensures the iframe container adjusts to content height
		 */
		function sendResize() {
			window.parent.postMessage({
				type: "resize",
				width: document.documentElement.scrollWidth + 1,
				height: document.documentElement.scrollHeight + 1
			}, "*");
		}

		/**
		 * Main message listener - receives document data from parent frame
		 * The parent sends all invoice/document data via postMessage
		 */
		window.addEventListener("message", (event) => {

			// Security: Only accept messages from parent frame
			if (event.source !== window.parent) return;
			// Only process context-response messages
			if (event.data.type !== 'context-response') return;

			// Extract the main data object sent from parent
			// This contains all document information (business, recipient, items, etc.)
			const data = event.data.body;

			// Set text direction (LTR or RTL) based on language settings
			document.documentElement.dir = data.direction;

			/* OPTIONAL: Add document title to page headers for printing
			const style = document.createElement('style');
			style.textContent = `@page { @top-center { content: '${data.title}'; }}`;
			document.head.appendChild(style);
			*/

			// Set browser tab title - combines business name, document type, and reference
			document.title = [data?.business?.name, data?.title, data?.reference].filter(Boolean).join(' - ');

			// POPULATE DOCUMENT HEADER
			// Set document title (e.g., "Sales Invoice", "Purchase Order")
			document.getElementById("title").innerHTML = data.title || "No title";
			// Set description line (optional subtitle)
			document.getElementById("description").innerHTML = data.description || "";

			// BUSINESS LOGO
			// Insert business logo if available
			var businessLogoTd = document.getElementById("business-logo");
			if (data.business.logo) {
				const img = document.createElement("img");
				img.addEventListener("load", sendResize); // Resize iframe when logo loads
				img.src = data.business.logo;
				// Customize logo size constraints here
				img.style = "max-height: 80px; max-width: 300px; display: inline";
				businessLogoTd.appendChild(img);
			}

			// BUSINESS INFO SECTION (right side)
			// Display business name and address
			const business = data.business || {};
			document.getElementById("business-info").innerHTML = `<strong>${business.name || ""}</strong><br>${business.address ? business.address.replace(/\n/g, "<br>") : ""}`;

			// RECIPIENT INFO SECTION (left side)
			// Display customer/supplier name and address
			const recipient = data.recipient || {};
			document.getElementById("recipient-billto").innerHTML = `${recipient.address ? recipient.address.replace(/\n/g, "<br>") : ""}`;

			// DOCUMENT FIELDS (middle section)
			// These are key-value pairs like Invoice Number, Date, Due Date, etc.
			var dd__inv = document.createElement("dd");
			var dd___po = document.createElement("dd");
			var dd_date = document.createElement("dd");
			var dd__due = document.createElement("dd");
			
			(data.fields || []).forEach(f => 
			{
//				const dd = document.createElement("dd"); // Value
//				dd.innerHTML = '<b>' + f.label +':</b> ' + f.text;
				switch (f.label) 
				{
				case 'Invoice number':	dd__inv.innerHTML = '<b>' + f.label +': ' + f.text +'</b> '; ; 	break;
				case 'Invoice date': 	dd_date.innerHTML = '<b>' + f.label +':</b> ' + f.text; ;	break;
				case 'Due date':		dd__due.innerHTML = '<b>' + f.label +':</b> ' + f.text; ; 	break;
//				default: fieldsDiv.appendChild(dd);
				}

//				const dt = document.createElement("dt"); // Label
//				dt.innerHTML = f.label;
			});

// type,key,custom_theme,direction,title,description,reference,emphasis,fields,custom_fields,table,business,recipient,
// timestamp,footers,serviceLink,legacyQrCodeForSaudiArabia,amountInWords,EmailVariables
// custom_theme[0]: key,label,text,value,displayAtTheTop
//			var keys = Object.keys(data.custom_fields[0]);
//            var keys = data.custom_fields("ShipTo").text;
//			const DebugDiv = document.getElementById("debug");
//			DebugDiv.innerHTML  = keys + '<br>';
//            DebugDiv.innerHTML += data.custom_fields[0].label + ' ' + data.custom_fields[0].text;

			// TABLE HEADERS
			// Build column headers dynamically based on data.table.columns
			const headersRow = document.getElementById("table-headers");
			headersRow.innerHTML = "";
			(data.table.columns || []).forEach(col => {
				const th = document.createElement("th");
				th.innerHTML = col.label; // Column header text
				th.style.textAlign = col.align; // left, center, or right
				
				// Column width options:
				if (col.minWidth) {
					// Minimum width column (typically for numbers)
					th.style.whiteSpace = 'nowrap';
					th.style.width = '1px';
				}
				else if (col.nowrap) {
					// No-wrap column with fixed width
					th.style.whiteSpace = 'nowrap';
					th.style.width = '80px';
				}
				headersRow.appendChild(th);
			});

			// LINE ITEMS
			// Populate table with line items (products, services, etc.)
			const rowsBody = document.getElementById("table-rows");
			rowsBody.innerHTML = "";
			(data.table.rows || []).forEach(row => {
				const tr = document.createElement("tr");
				tr.className = 'row'; // Apply row styling
				
				// Create cells for each column
				row.cells.forEach((cell, i) => {
					var col = data.table.columns[i]; // Get column definition
					const td = document.createElement("td");
					// Convert newlines to HTML line breaks
					td.innerHTML = (cell.text || "").split("\n").join("<br />");
					// Apply column alignment
					td.style.textAlign = col.align;
					
					// Apply column width settings
					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);
			});

			// MARK LAST ROW
			// Add special styling to the last line item row
			const rows = rowsBody.querySelectorAll('tr.row');
			if (rows.length > 0) {
				const lastRow = rows[rows.length - 1];
				lastRow.classList.add('last-row'); // Adds bottom border and extra padding
			}

			// COLUMN TOTALS ROW (optional)
			// Shows sum of numeric columns if configured
			const tr = document.createElement("tr");
			tr.classList.add('column-total');
			(data.table.columns || []).forEach(col => {
				const td = document.createElement("td");
				td.style.textAlign = col.align;
				if (col.sumText) td.textContent = col.sumText; // Column sum if applicable
				tr.appendChild(td);
			});
			// Only add row if it has content
			if (tr.innerText) rowsBody.appendChild(tr);

			// TOTALS SECTION
			// Display subtotal, taxes, discounts, and grand total
			(data.table.totals || []).forEach(total => {
				const tr = document.createElement("tr");
				tr.className = 'total';
				
				// Label cell (e.g., "Subtotal:", "Tax:", "Total:")
				const tdLabel = document.createElement("td");
				tdLabel.innerHTML = total.label;
				tdLabel.colSpan = data.table.columns.length - 1; // Span all columns except last
				
				// Value cell (the amount)
				const tdValue = document.createElement("td");
				tdValue.innerHTML = total.text; // Formatted amount
				tdValue.id = total.key; // ID for targeting specific totals (e.g., 'Total')
				if (total.class) tdValue.classList.add(total.class); // CSS class (e.g., 'taxAmount')
				tdValue.dataset.value = total.number; // Raw numeric value for calculations

				// Bold formatting for important totals
				if (total.emphasis) {
					tdLabel.style.fontWeight = 'bold';
					tdValue.style.fontWeight = 'bold';
				}
				tr.appendChild(tdLabel);
				tr.appendChild(tdValue);
				rowsBody.appendChild(tr);
			});

			// CUSTOM FIELDS
			// Display custom fields like notes, terms, payment instructions, etc.
			const customFieldsDiv = document.getElementById("custom-fields");
			customFieldsDiv.innerHTML = "";

			(data.custom_fields || []).forEach(f => {
				if (f.label=='ShipTo') {
					document.getElementById("recipient-shipto").innerHTML = `${f.text ? f.text.replace(/\n/g, "<br>") : ""}`;
				}
				else
				{
					// Some custom fields can be displayed at the top with other document fields
					if (f.displayAtTheTop) 
					{
						switch (f.label) 
						{
						case 'Revision':	dd__inv.innerHTML += ' <b>Rev:</b>' + f.text;					break;
						case 'PO #':		dd___po.innerHTML = '<b>' + f.label + ': ' + f.text +'</b> ';	break;
						}
						// Add to the fields section at the top of document
	//					const dt = document.createElement("dt");
	//					dt.innerHTML = f.label;
//						const dd = document.createElement("dd");
//						dd.innerHTML = '<b>' + f.label + ':</b> ' + f.text;
	//					fieldsDiv.appendChild(dt);
//						fieldsDiv.appendChild(dd);
					}
					else {
						// Display as a labeled section below the table
						const div = document.createElement("div");
						div.innerHTML = `<strong>${f.label || ""}</strong><br />${(f.text || "").split("\n").join("<br />")}<br /><br />`;
						customFieldsDiv.appendChild(div);
					}
				}
			});

			const fieldsDiv = document.getElementById("fields"    ); 
			fieldsDiv.innerHTML = "";

			fieldsDiv.appendChild(dd__inv);
			fieldsDiv.appendChild(dd___po);
			fieldsDiv.appendChild(dd_date);
			fieldsDiv.appendChild(dd__due);

			// AMOUNT IN WORDS
			if (data.amountInWords) {
				var aiw = data.amountInWords;
				var aiwScript = document.createElement('script');
				aiwScript.src = 'resources/writtennumber/lang-' + aiw.language + '.js';
				aiwScript.onload = function () {
					var totalEntry = (data.table.totals || []).filter(function (t) { return t.key === "Total"; });
					if (totalEntry.length === 0) return;
					var totalNumber = totalEntry[totalEntry.length - 1].number;
					var text = "";
					if (aiw.currencyPrefix === "\u20B9" || aiw.currencyCode === "INR") {
						text = spellOutRupees(totalNumber) + " Rupees";
						var fraction = Math.round(totalNumber * 100) % 100;
						if (fraction === 0) text += " Only";
						else if (fraction === 1) text += " and One Paisa";
						else text += " and " + spellOutRupees(fraction) + " Paise";
					} else if (aiw.currencyPrefix === "\u09F3" || aiw.currencyCode === "BDT") {
						text = spellOutRupees(totalNumber) + " Taka";
						var fraction = Math.round(totalNumber * 100) % 100;
						if (fraction === 0) text += " Only";
						else if (fraction === 1) text += " and One Paisa";
						else text += " and " + spellOutRupees(fraction) + " Paise";
					} else {
						var decimalPlaces = aiw.decimalPlaces;
						var multiplier = 1;
						for (var dp = 0; dp < decimalPlaces; dp++) multiplier *= 10;
						var wn = writtenNumber(Math.floor(totalNumber), { language: aiw.language, currency: aiw.currencyCode });
						if (wn.length > 0) wn = wn.charAt(0).toUpperCase() + wn.substring(1);
						text = wn;
						var fraction = Math.round(totalNumber * multiplier) % multiplier;
						if (fraction > 0) {
							var denominator = "1";
							for (var dp = 0; dp < decimalPlaces; dp++) denominator += "0";
							text += " " + aiw.andText + " " + fraction + "/" + denominator;
						}
					}
					if (text) {
						var div = document.createElement("div");
						div.innerHTML = '<strong>' + aiw.label + '</strong><br />' + text + '<br /><br />';
						document.getElementById("custom-fields").appendChild(div);
					}
					sendResize();
				};
				document.head.appendChild(aiwScript);
			}

			// FOOTERS SECTION
			// Custom HTML footers - can contain signatures, scripts, or other content
			const footersDiv = document.getElementById("footers");
			footersDiv.innerHTML = "";
			(data.footers || []).forEach(f => {
				const div = document.createElement("div");
				div.style = 'margin-top: 20px';
				div.innerHTML = f; // Raw HTML content
				footersDiv.appendChild(div);

				// SCRIPT EXECUTION IN FOOTERS
				// Footers may contain <script> tags that need to be executed
				const scripts = div.querySelectorAll("script");
				scripts.forEach(script => {
					// Create new script element (required for execution)
					const newScript = document.createElement("script");

					// Copy all attributes from original script
					for (const attr of script.attributes) {
						newScript.setAttribute(attr.name, attr.value);
					}

					// Copy script content
					if (script.textContent) {
						newScript.textContent = script.textContent;
					}

					// Replace old script with new one to trigger execution
					script.parentNode.replaceChild(newScript, script);
				});
			});

			// STATUS STAMP
			// Display status label (e.g., PAID, CANCELLED, VOID) with colored border
			const statusDiv = document.getElementById("status");
			if (data.emphasis?.text != null) {
				statusDiv.style.marginTop = '40px';
				const span = document.createElement("span");
				// Default red border style
				span.style = 'border-width: 5px; border-color: #FF0000; border-style: solid; padding: 10px; font-size: 20px; text-transform: uppercase';
				
				// Green for positive status (e.g., PAID)
				if (data.emphasis.positive) {
					span.style.color = 'green';
					span.style.borderColor = 'green';
				}
				// Red for negative status (e.g., CANCELLED, VOID)
				if (data.emphasis.negative) {
					span.style.color = 'red';
					span.style.borderColor = 'red';
				}
				span.innerHTML = data.emphasis.text;
				statusDiv.appendChild(span);
			}

			// SAUDI ARABIA QR CODE (E-INVOICING)
			// Special QR code required for Saudi Arabian tax compliance
			// NOTE: This is a temporary implementation - will be improved in future versions
			if (data.legacyQrCodeForSaudiArabia) {
				/**
				 * Encodes data in TLV (Tag-Length-Value) format for QR code
				 */
				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 = [];

				// Extract business name
				let businessName = 'No name';
				if (data.business) businessName = data.business.name;

				// Extract VAT number from custom fields
				let vatNumber = '0000000000000';
				let vatField = data.business.custom_fields.find(item => item.key === "d96d97e8-c857-42c6-8360-443c06a13de9");
				if (vatField) vatNumber = vatField.text;

				// Convert .NET ticks to JavaScript date
				let timestamp = new Date((data.timestamp - 621355968000000000) / 10000).toISOString();

				// Get total amount from the Total element
				let total = 0;
				let totalElement = document.getElementById('Total');
				if (totalElement != null) total = parseFloat(totalElement.getAttribute('data-value'));

				// Calculate total VAT from all tax amount elements
				let vat = 0;
				let taxAmounts = document.getElementsByClassName('taxAmount');
				for (let i = 0; i < taxAmounts.length; i++) {
					vat += parseFloat(taxAmounts[i].getAttribute('data-value'));
				}

				// Build TLV data structure as per Saudi requirements
				appendTLV(1, businessName, byteList);      // Seller name
				appendTLV(2, vatNumber, byteList);         // VAT registration number
				appendTLV(3, timestamp, byteList);         // Invoice timestamp
				appendTLV(4, total.toFixed(2), byteList);  // Total amount with VAT
				appendTLV(5, vat.toFixed(2), byteList);    // Total VAT amount

				// Convert to Uint8Array
				const tlvBytes = Uint8Array.from(byteList);

				// Convert to Base64 for QR code
				const qrData = btoa(String.fromCharCode(...tlvBytes));

				// Generate QR code
				new QRCode(document.getElementById("qrcode"), {
					text: qrData,
					width: 128,
					height: 128,
					colorDark: "#000000",
					colorLight: "#ffffff",
					correctLevel: QRCode.CorrectLevel.L
				});
			}

			// Notify parent frame of content size after rendering
			sendResize();
		}, false);

		/**
		 * Initialize communication with parent frame
		 * Requests document data when page loads
		 */
		window.addEventListener("load", () =>
			window.parent.postMessage({ type: "context-request" }, "*")
		);
	</script>

</body>
</html>

2 Likes

Here’s the Packing Slip Theme:

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	
	<!-- CSS CUSTOMIZATION SECTION -->
	<style>
		/* PAGE SETUP - Controls print layout and margins */
		@page {
			/* A4 size is default but some web-browsers do not offer "Scale" option if A4 is set explicitly */
			/* Uncomment to force A4 paper size: */
			/*
            size: 8.5in 11in;
			*/
			
			/* Page margins - adjust these for more/less white space around content */
			margin: 5mm;
			
			/* Uncomment if you want page number in the footer */
			/*
			@bottom-center {
				content: counter(page);
				font-size: 0.8em;
			}
			*/
		}

		/* CSS RESET - Ensures consistent styling across browsers */
		*, ::after, ::before, ::backdrop, ::file-selector-button {
			margin: 0;
			padding: 0;
		}

		*, ::after, ::before, ::backdrop, ::file-selector-button {
			box-sizing: border-box;
			border: 0 solid;
		}

		/* BODY STYLES - Main document styling */
		body {
			margin: 0;
			padding: 30px; /* Space around document content */
			font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; /* Change font family here */
			color: #171717; /* Main text color */
			font-size: 12px; /* Base font size for document */
			line-height: 1.428571429;
			min-width: 800px; /* Minimum width to prevent layout breaking */
		}

		/* ADDRESS STYLING - For business and recipient addresses */
		address {
			font-style: normal; /* Remove italic styling */
			line-height: 1.5em;
		}

		/* DEFINITION LIST STYLING - Used for invoice fields (date, number, etc.) */
		dt {
			font-weight: bold;
			margin: 0 0 2px 0; /* Small gap below label */
		}

		dd {
			margin: 0 0 4px 0; /* Bigger gap below value */
		}

		dd:last-of-type {
			margin-bottom: 0; /* No gap after last pair */
		}

		/* PRINT STYLES - Applied when printing or generating PDF */
		@media print {
			body {
				padding: 0; /* Remove padding for print */
				min-width: auto; /* Allow natural width for print */
			}
		}

		/* TABLE STYLES - Main table containing line items */
		table {
			font-size: 12px;
			width: 100%;
		}

		/* TABLE HEADERS - Column headers for the line items table */
		tr#table-headers th {
			font-weight: bold;
			padding: 5px 10px; /* Cell padding */
			border: 1px solid #000; /* Header border color */
			text-align: start
		}

		/* TABLE CELLS - Basic cell styling */
		tbody#table-rows td {
			padding: 5px 10px; /* Cell padding */			
			text-align: start;
			vertical-align: top
		}

		/* LINE ITEM ROWS - Styling for each line item */
		tbody#table-rows tr.row td {
			border-left: 1px solid #000; /* Side borders for cells */
			border-right: 1px solid #000;
		}

		/* LAST ROW - Special styling for the last line item */
		tbody#table-rows tr.last-row td {
			padding-bottom: 30px; /* Extra space before totals */
			border-bottom: 1px solid #000; /* Bottom border */
		}

		/* COLUMN TOTALS - Sum row at bottom of columns (if enabled) */
		tbody#table-rows tr.column-total td {
			font-weight: bold;
			border: 1px solid #000;
			white-space: nowrap;
			text-align: right;
		}

		/* TOTALS SECTION - Subtotal, tax, total rows */
		tbody#table-rows tr.total td {
			white-space: nowrap; /* Prevent line breaks */
		}

		/* TOTAL LABELS - Right-aligned labels (e.g., "Subtotal:") */
		tbody#table-rows tr.total td:first-child {
			text-align: end;
		}

		/* TOTAL VALUES - Amount cells with border */
		tbody#table-rows tr.total td:last-child {
			border: 1px solid #000; /* Border around amounts */
			text-align: right;
		}
	</style>
</head>
<body>
<!--	<p id="debug" style="overflow-wrap: break-word"></p> -->

	<!-- MAIN LAYOUT TABLE - Used to ensure proper page breaks and header repetition -->
	<table>
		<thead>
			<!-- REPEATING HEADER SECTION -->
			<!-- The contents of the <thead> element will automatically repeat at the top of each printed page -->
			<!-- If you want anything here not to repeat on every page, move the relevant blocks into <tbody> -->
			<tr>
				<table>
					<tr>
						<td>
							<!-- INFO SECTION - Recipient, fields, and business details -->
							<section style="display: flex; margin-bottom: 20px; width: 100%; align-items: flex-start; gap: 20px">
								<!-- Business logo container - image will be inserted here -->
								<div id="business-logo" style="text-align: end"></div>

								<!-- Vertical separator line -->
<!--								<div aria-hidden="true" style="width: 1px; border-left: 1px solid #000; align-self: stretch "></div> -->

								<!-- Business address (right side) -->
								<address id="business-info" style="white-space: nowrap"></address>

								<!-- DOCUMENT HEADER - Title -->
								<header style="width: 2.0in; text-align: center; vertical-align: top; margin-bottom: 20px">
									<!-- Document title (e.g., "Tax Invoice") - populated by JavaScript -->
									<h1 id="title" style="font-size: 26px; line-height: 30px; font-weight: bold; text-align: center">Packing Slip</h1>
								</header>

								<!-- Document fields (invoice number, date, etc.) - middle -->
								<dl id="fields" style="flex: 1; text-align: end"></dl>
							</section>
						</td>
					</tr>
				</table>
			</tr>
			<tr>
				<td>
					<!-- INFO SECTION - Recipient, fields, and business details -->
					<section style="display: flex; margin-bottom: 20px; width: 100%; align-items: flex-start; gap: 20px">
						<b>Bill To:</b><br>
						<!-- Recipient/customer address (left side) -->
						<address id="recipient-billto" style="flex: 1"></address>

						<b>Ship To:</b><br>
						<!-- Recipient/customer address (left side) -->
						<address id="recipient-shipto" style="flex: 1"></address>
					</section>

					<!-- Optional description line (e.g., "Professional services") -->
					<p style="font-weight: bold; font-size: 14px; margin-bottom: 20px" id="description"></p>
				</td>
			</tr>
		</thead>
		<tbody>
			<!-- NON-REPEATING CONTENT SECTION -->
			<tr>
				<td>
					<!-- MAIN LINE ITEMS TABLE -->
					<!-- This table contains column headers, line items, and totals -->
					<table style="border-collapse: collapse; width: 100%">
						<thead>
							<!-- Table headers row - populated dynamically by JavaScript -->
							<tr id="table-headers"></tr>
						</thead>
						<tbody id="table-rows">
							<!-- Line items and totals will be inserted here by JavaScript -->
						</tbody>
					</table>

					<!-- QR CODE SECTION - For special features like Saudi Arabia e-invoicing -->
					<script src="resources/qrcode/qrcode.js"></script>
					<script src="resources/writtennumber/writtennumber.js"></script>
					<div id="qrcode" style="margin-bottom: 20px"></div>

					<!-- CUSTOM FIELDS SECTION - Notes, terms, and other custom content -->
					<div id="custom-fields"></div>

					<!-- FOOTERS SECTION - Can contain HTML or custom scripts -->
					<table><tr><td><div id="footers"></div></td></tr></table>
					<p style="position:absolute; bottom:0; color:red; text-align: center;">
						<b>Thank you for your business!</b>
					</p>
					<!-- STATUS SECTION - For stamps like PAID, VOID, CANCELLED -->
					<div id="status" style="text-align: center"></div>
				</td>
			</tr>
		</tbody>
	</table>

	<!-- JAVASCRIPT SECTION - Handles dynamic content population -->
	<script>
		/**
		 * Sends resize message to parent frame when content changes
		 * This ensures the iframe container adjusts to content height
		 */
		function sendResize() {
			window.parent.postMessage({
				type: "resize",
				width: document.documentElement.scrollWidth + 1,
				height: document.documentElement.scrollHeight + 1
			}, "*");
		}

		/**
		 * Main message listener - receives document data from parent frame
		 * The parent sends all invoice/document data via postMessage
		 */
		window.addEventListener("message", (event) => {

			// Security: Only accept messages from parent frame
			if (event.source !== window.parent) return;
			// Only process context-response messages
			if (event.data.type !== 'context-response') return;

			// Extract the main data object sent from parent
			// This contains all document information (business, recipient, items, etc.)
			const data = event.data.body;

			// Set text direction (LTR or RTL) based on language settings
			document.documentElement.dir = data.direction;

			/* OPTIONAL: Add document title to page headers for printing 
			const style = document.createElement('style');
			style.textContent = `@page { @top-center { content: '${data.title}'; }}`;
			document.head.appendChild(style);
			*/

			// Set browser tab title - combines business name, document type, and reference
			document.title = [data?.business?.name, data?.title, data?.reference].filter(Boolean).join(' - ');

			// POPULATE DOCUMENT HEADER
			// Set document title (e.g., "Sales Invoice", "Purchase Order")
// Skip for packing slip
//			document.getElementById("title").innerHTML = data.title || "No title";
			// Set description line (optional subtitle)
			document.getElementById("description").innerHTML = data.description || "";

			// BUSINESS LOGO
			// Insert business logo if available
			var businessLogoTd = document.getElementById("business-logo");
			if (data.business.logo) {
				const img = document.createElement("img");
				img.addEventListener("load", sendResize); // Resize iframe when logo loads
				img.src = data.business.logo;
				// Customize logo size constraints here
				img.style = "max-height: 80px; max-width: 300px; display: inline";
				businessLogoTd.appendChild(img);
			}

			// BUSINESS INFO SECTION (right side)
			// Display business name and address
			const business = data.business || {};
			document.getElementById("business-info").innerHTML = `<strong>${business.name || ""}</strong><br>${business.address ? business.address.replace(/\n/g, "<br>") : ""}`;

			// RECIPIENT INFO SECTION (left side)
			// Display customer/supplier name and address
			const recipient = data.recipient || {};
			document.getElementById("recipient-billto").innerHTML = `${recipient.address ? recipient.address.replace(/\n/g, "<br>") : ""}`;

			// DOCUMENT FIELDS (middle section)
			// These are key-value pairs like Invoice Number, Date, Due Date, etc.
			var dd__inv = document.createElement("dd");
			var dd___po = document.createElement("dd");
			var dd_date = document.createElement("dd");
			var dd__due = document.createElement("dd");
			
			(data.fields || []).forEach(f => 
			{
//				const dd = document.createElement("dd"); // Value
//				dd.innerHTML = '<b>' + f.label +':</b> ' + f.text;
				switch (f.label) 
				{
				case 'Invoice number':	dd__inv.innerHTML = '<b>' + f.label +': ' + f.text +'</b> '; ; 	break;
				case 'Invoice date': 	dd_date.innerHTML = '<b>Ship Date:</b> '  + f.text; ;	break;
				case 'Due date':		dd__due.innerHTML = '<b>' + f.label +':</b> ' + f.text; ; 	break;
//				default: fieldsDiv.appendChild(dd);
				}

//				const dt = document.createElement("dt"); // Label
//				dt.innerHTML = f.label;
			});

// type,key,custom_theme,direction,title,description,reference,emphasis,fields,custom_fields,table,business,recipient,
// timestamp,footers,serviceLink,legacyQrCodeForSaudiArabia,amountInWords,EmailVariables
// custom_theme[0]: key,label,text,value,displayAtTheTop
//			var keys = Object.keys(data.custom_fields[0]);
//            var keys = data.custom_fields("ShipTo").text;
//			const DebugDiv = document.getElementById("debug");
//			DebugDiv.innerHTML  = keys + '<br>';
//            DebugDiv.innerHTML += data.custom_fields[0].label + ' ' + data.custom_fields[0].text;

			// TABLE HEADERS
			// Build column headers dynamically based on data.table.columns
			const headersRow = document.getElementById("table-headers");
			headersRow.innerHTML = "";
			(data.table.columns || []).forEach((col, i) => {
				if (i <= 3)
				{
					const th = document.createElement("th");
					th.innerHTML = col.label; // Column header text
					th.style.textAlign = col.align; // left, center, or right
					
					// Column width options:
					if (col.minWidth) {
						// Minimum width column (typically for numbers)
						th.style.whiteSpace = 'nowrap';
						th.style.width = '1px';
					}
					else if (col.nowrap) {
						// No-wrap column with fixed width
						th.style.whiteSpace = 'nowrap';
						th.style.width = '80px';
					}
					headersRow.appendChild(th);
				}
			});

			// LINE ITEMS
			// Populate table with line items (products, services, etc.)
			const rowsBody = document.getElementById("table-rows");
			rowsBody.innerHTML = "";
			(data.table.rows || []).forEach(row => {
				const tr = document.createElement("tr");
				tr.className = 'row'; // Apply row styling
				
				// Create cells for each column
				row.cells.forEach((cell, i) => {
					if (i <= 3)
					{
						var col = data.table.columns[i]; // Get column definition
						const td = document.createElement("td");
						// Convert newlines to HTML line breaks
						td.innerHTML = (cell.text || "").split("\n").join("<br />");
						// Apply column alignment
						td.style.textAlign = col.align;
						
						// Apply column width settings
						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);
			});

			// MARK LAST ROW
			// Add special styling to the last line item row
			const rows = rowsBody.querySelectorAll('tr.row');
			if (rows.length > 0) {
				const lastRow = rows[rows.length - 1];
				lastRow.classList.add('last-row'); // Adds bottom border and extra padding
			}

			// COLUMN TOTALS ROW (optional)
			// Shows sum of numeric columns if configured
/*
			const tr = document.createElement("tr");
			tr.classList.add('column-total');
			(data.table.columns || []).forEach(col => {
				const td = document.createElement("td");
				td.style.textAlign = col.align;
				if (col.sumText) td.textContent = col.sumText; // Column sum if applicable
				tr.appendChild(td);
			});
			// Only add row if it has content
			if (tr.innerText) rowsBody.appendChild(tr);

			// TOTALS SECTION
			// Display subtotal, taxes, discounts, and grand total
			(data.table.totals || []).forEach(total => {
				const tr = document.createElement("tr");
				tr.className = 'total';
				
				// Label cell (e.g., "Subtotal:", "Tax:", "Total:")
				const tdLabel = document.createElement("td");
				tdLabel.innerHTML = total.label;
				tdLabel.colSpan = data.table.columns.length - 1; // Span all columns except last
				
				// Value cell (the amount)
				const tdValue = document.createElement("td");
				tdValue.innerHTML = total.text; // Formatted amount
				tdValue.id = total.key; // ID for targeting specific totals (e.g., 'Total')
				if (total.class) tdValue.classList.add(total.class); // CSS class (e.g., 'taxAmount')
				tdValue.dataset.value = total.number; // Raw numeric value for calculations

				// Bold formatting for important totals
				if (total.emphasis) {
					tdLabel.style.fontWeight = 'bold';
					tdValue.style.fontWeight = 'bold';
				}
				tr.appendChild(tdLabel);
				tr.appendChild(tdValue);
				rowsBody.appendChild(tr);
			});
*/
			// CUSTOM FIELDS
			// Display custom fields like notes, terms, payment instructions, etc.
			const customFieldsDiv = document.getElementById("custom-fields");
			customFieldsDiv.innerHTML = "";

			(data.custom_fields || []).forEach(f => {
				if (f.label=='ShipTo') {
					document.getElementById("recipient-shipto").innerHTML = `${f.text ? f.text.replace(/\n/g, "<br>") : ""}`;
				}
				else
				{
					// Some custom fields can be displayed at the top with other document fields
					if (f.displayAtTheTop) {
						switch (f.label) 
						{
						case 'Revision':	dd__inv.innerHTML += ' <b>Rev:</b>' + f.text;					break;
						case 'PO #':		dd___po.innerHTML = '<b>' + f.label + ': ' + f.text +'</b> ';	break;
						}
					}
					else {
						// Display as a labeled section below the table
						const div = document.createElement("div");
						div.innerHTML = `<strong>${f.label || ""}</strong><br />${(f.text || "").split("\n").join("<br />")}<br /><br />`;
						customFieldsDiv.appendChild(div);
					}
				}
			});

			const fieldsDiv = document.getElementById("fields"    ); 
			fieldsDiv.innerHTML = "";

			fieldsDiv.appendChild(dd__inv);
			fieldsDiv.appendChild(dd___po);
			fieldsDiv.appendChild(dd_date);
//			fieldsDiv.appendChild(dd__due);

			// AMOUNT IN WORDS
			if (data.amountInWords) {
				var aiw = data.amountInWords;
				var aiwScript = document.createElement('script');
				aiwScript.src = 'resources/writtennumber/lang-' + aiw.language + '.js';
				aiwScript.onload = function () {
					var totalEntry = (data.table.totals || []).filter(function (t) { return t.key === "Total"; });
					if (totalEntry.length === 0) return;
					var totalNumber = totalEntry[totalEntry.length - 1].number;
					var text = "";
					if (aiw.currencyPrefix === "\u20B9" || aiw.currencyCode === "INR") {
						text = spellOutRupees(totalNumber) + " Rupees";
						var fraction = Math.round(totalNumber * 100) % 100;
						if (fraction === 0) text += " Only";
						else if (fraction === 1) text += " and One Paisa";
						else text += " and " + spellOutRupees(fraction) + " Paise";
					} else if (aiw.currencyPrefix === "\u09F3" || aiw.currencyCode === "BDT") {
						text = spellOutRupees(totalNumber) + " Taka";
						var fraction = Math.round(totalNumber * 100) % 100;
						if (fraction === 0) text += " Only";
						else if (fraction === 1) text += " and One Paisa";
						else text += " and " + spellOutRupees(fraction) + " Paise";
					} else {
						var decimalPlaces = aiw.decimalPlaces;
						var multiplier = 1;
						for (var dp = 0; dp < decimalPlaces; dp++) multiplier *= 10;
						var wn = writtenNumber(Math.floor(totalNumber), { language: aiw.language, currency: aiw.currencyCode });
						if (wn.length > 0) wn = wn.charAt(0).toUpperCase() + wn.substring(1);
						text = wn;
						var fraction = Math.round(totalNumber * multiplier) % multiplier;
						if (fraction > 0) {
							var denominator = "1";
							for (var dp = 0; dp < decimalPlaces; dp++) denominator += "0";
							text += " " + aiw.andText + " " + fraction + "/" + denominator;
						}
					}
					if (text) {
						var div = document.createElement("div");
						div.innerHTML = '<strong>' + aiw.label + '</strong><br />' + text + '<br /><br />';
						document.getElementById("custom-fields").appendChild(div);
					}
					sendResize();
				};
				document.head.appendChild(aiwScript);
			}

			// FOOTERS SECTION
			// Custom HTML footers - can contain signatures, scripts, or other content
			const footersDiv = document.getElementById("footers");
			footersDiv.innerHTML = "";
			(data.footers || []).forEach(f => {
				const div = document.createElement("div");
				div.style = 'margin-top: 20px';
				div.innerHTML = f; // Raw HTML content
				footersDiv.appendChild(div);

				// SCRIPT EXECUTION IN FOOTERS
				// Footers may contain <script> tags that need to be executed
				const scripts = div.querySelectorAll("script");
				scripts.forEach(script => {
					// Create new script element (required for execution)
					const newScript = document.createElement("script");

					// Copy all attributes from original script
					for (const attr of script.attributes) {
						newScript.setAttribute(attr.name, attr.value);
					}

					// Copy script content
					if (script.textContent) {
						newScript.textContent = script.textContent;
					}

					// Replace old script with new one to trigger execution
					script.parentNode.replaceChild(newScript, script);
				});
			});

			// STATUS STAMP
			// Display status label (e.g., PAID, CANCELLED, VOID) with colored border
			const statusDiv = document.getElementById("status");
			if (data.emphasis?.text != null) {
				statusDiv.style.marginTop = '40px';
				const span = document.createElement("span");
				// Default red border style
				span.style = 'border-width: 5px; border-color: #FF0000; border-style: solid; padding: 10px; font-size: 20px; text-transform: uppercase';
				
				// Green for positive status (e.g., PAID)
				if (data.emphasis.positive) {
					span.style.color = 'green';
					span.style.borderColor = 'green';
				}
				// Red for negative status (e.g., CANCELLED, VOID)
				if (data.emphasis.negative) {
					span.style.color = 'red';
					span.style.borderColor = 'red';
				}
				span.innerHTML = data.emphasis.text;
				statusDiv.appendChild(span);
			}

			// SAUDI ARABIA QR CODE (E-INVOICING)
			// Special QR code required for Saudi Arabian tax compliance
			// NOTE: This is a temporary implementation - will be improved in future versions
			if (data.legacyQrCodeForSaudiArabia) {
				/**
				 * Encodes data in TLV (Tag-Length-Value) format for QR code
				 */
				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 = [];

				// Extract business name
				let businessName = 'No name';
				if (data.business) businessName = data.business.name;

				// Extract VAT number from custom fields
				let vatNumber = '0000000000000';
				let vatField = data.business.custom_fields.find(item => item.key === "d96d97e8-c857-42c6-8360-443c06a13de9");
				if (vatField) vatNumber = vatField.text;

				// Convert .NET ticks to JavaScript date
				let timestamp = new Date((data.timestamp - 621355968000000000) / 10000).toISOString();

				// Get total amount from the Total element
				let total = 0;
				let totalElement = document.getElementById('Total');
				if (totalElement != null) total = parseFloat(totalElement.getAttribute('data-value'));

				// Calculate total VAT from all tax amount elements
				let vat = 0;
				let taxAmounts = document.getElementsByClassName('taxAmount');
				for (let i = 0; i < taxAmounts.length; i++) {
					vat += parseFloat(taxAmounts[i].getAttribute('data-value'));
				}

				// Build TLV data structure as per Saudi requirements
				appendTLV(1, businessName, byteList);      // Seller name
				appendTLV(2, vatNumber, byteList);         // VAT registration number
				appendTLV(3, timestamp, byteList);         // Invoice timestamp
				appendTLV(4, total.toFixed(2), byteList);  // Total amount with VAT
				appendTLV(5, vat.toFixed(2), byteList);    // Total VAT amount

				// Convert to Uint8Array
				const tlvBytes = Uint8Array.from(byteList);

				// Convert to Base64 for QR code
				const qrData = btoa(String.fromCharCode(...tlvBytes));

				// Generate QR code
				new QRCode(document.getElementById("qrcode"), {
					text: qrData,
					width: 128,
					height: 128,
					colorDark: "#000000",
					colorLight: "#ffffff",
					correctLevel: QRCode.CorrectLevel.L
				});
			}

			// Notify parent frame of content size after rendering
			sendResize();
		}, false);

		/**
		 * Initialize communication with parent frame
		 * Requests document data when page loads
		 */
		window.addEventListener("load", () =>
			window.parent.postMessage({ type: "context-request" }, "*")
		);
	</script>

</body>
</html>

2 Likes

If you are sharing a theme it would be great to put the output here as screenshot as well.

2 Likes

Here you go:

1 Like