Indonesia Tax System changes in 2025

Move footer script to themes, and call then function after calling sendResize();

			// Notify parent frame of content size after rendering
			sendResize();
			
			//Panggil Script DPP Nilai lain
			processSubtotal();
			
		}, false);

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


<script id="script-wrapper">
    
    //document.addEventListener("DOMContentLoaded", processSubtotal);

    function processSubtotal() {
        // Cari semua sel di kolom pertama yang mungkin mengandung "Subtotal" atau "Sub-total"
        const subtotalCell = [...document.querySelectorAll("tbody td:first-child")]
            .find(td => ["subtotal", "sub-total"].includes(td.textContent.trim().toLowerCase()));

        if (!subtotalCell) return;

        // Ambil baris tempat Subtotal ditemukan
        const firstSubtotalRow = subtotalCell.closest("tr");
        const subtotalValueCell = firstSubtotalRow.querySelector("td[data-value]") || firstSubtotalRow.querySelector("td:last-child");

        if (!subtotalValueCell) return;

        // Ambil nilai Subtotal
        const rawValue = subtotalValueCell.dataset.value || subtotalValueCell.textContent.trim();
        const isIdLocale = /,\d{2}$/.test(rawValue);
        const isEnLocale = /\.\d{2}$/.test(rawValue);

        let subtotalValue = isIdLocale 
            ? parseFloat(rawValue.replace(/\./g, "").replace(",", ".")) 
            : parseFloat(rawValue.replace(/,/g, ""));

        if (isNaN(subtotalValue) || subtotalValue === 0) return;

        // Hitung DPP Nilai Lain
        let dppNilaiLain = Math.round((11 / 12) * subtotalValue * 100) / 100;

        // Kloning baris Subtotal dan ubah menjadi DPP Nilai Lain
        const dppRow = firstSubtotalRow.cloneNode(true);
        const dppLabelCell = dppRow.querySelector("td[colspan]") || dppRow.querySelector("td:first-child");
        const dppValueCell = dppRow.querySelector("td[data-value]") || dppRow.querySelector("td:last-child");

        if (!dppLabelCell || !dppValueCell) return;

        dppLabelCell.textContent = "DPP Nilai Lain";
        const includeCurrencySymbol = subtotalValueCell.textContent.trim().includes("Rp");

        // Format angka sesuai locale
        const numberFormatter = new Intl.NumberFormat(isIdLocale ? "id-ID" : "en-US", {
            minimumFractionDigits: 2,
            maximumFractionDigits: 2
        });

        dppValueCell.textContent = includeCurrencySymbol
            ? `Rp ${numberFormatter.format(dppNilaiLain)}`
            : numberFormatter.format(dppNilaiLain);

        dppValueCell.setAttribute("data-value", dppNilaiLain.toFixed(2).replace(".", isIdLocale ? "," : "."));

        // Sisipkan baris setelah Subtotal pertama
        firstSubtotalRow.insertAdjacentElement("afterend", dppRow);

        // Hapus script setelah eksekusi
        document.getElementById("script-wrapper")?.remove();
    }
</script>

</body>
</html>

@yhart

Perhaps this code will be more useful for you. We modified the rendering of the Totals section directly, without waiting for the page to finish loading. The script is a bit long because the currency and number format settings are not exposed in the theme, so we need to detect them ourselves to ensure consistent number formatting.

Full theme code:

<!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: A4;
			*/
			
			/* Page margins - adjust these for more/less white space around content */
			margin: 20mm;
			
			/* 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 16px 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>
	<!-- 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>
				<td>
					<!-- DOCUMENT HEADER - Title and logo -->
					<header style="display: flex; justify-content: space-between; align-items: flex-start; 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"></h1>
						<!-- Business logo container - image will be inserted here -->
						<div id="business-logo" style="text-align: end"></div>
					</header>

					<!-- INFO SECTION - Recipient, fields, and business details -->
					<section style="display: flex; margin-bottom: 20px; width: 100%; align-items: flex-start; gap: 20px">
						<!-- Recipient/customer address (left side) -->
						<address id="recipient-info" style="flex: 1"></address>
						<!-- Document fields (invoice number, date, etc.) - middle -->
						<dl id="fields" style="flex: 1; text-align: end"></dl>
						<!-- 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>
					</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>
					<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>

					<!-- 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: 150px; 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-info").innerHTML = `<strong>${recipient.name || ""}</strong><br>${recipient.address ? recipient.address.replace(/\n/g, "<br>") : ""}`;

			// DOCUMENT FIELDS (middle section)
			// These are key-value pairs like Invoice Number, Date, Due Date, etc.
			const fieldsDiv = document.getElementById("fields");
			fieldsDiv.innerHTML = "";
			(data.fields || []).forEach(f => {
				const dt = document.createElement("dt"); // Label
				dt.innerHTML = f.label;
				const dd = document.createElement("dd"); // Value
				dd.innerHTML = f.text;
				fieldsDiv.appendChild(dt);
				fieldsDiv.appendChild(dd);
			});

			// 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;
				td.innerHTML = col.sumText; // Column sum if applicable
				tr.appendChild(td);
			});
			// Only add row if it has content
			if (tr.innerText) rowsBody.appendChild(tr);

			// TOTALS SECTION MODIFIED
			// Display subtotal, DPP Nilai Lain, taxes, discounts, and grand total
            (data.table.totals || []).forEach((total, index) => {
                const tr = document.createElement("tr");
                tr.className = 'total';
            
                // Label cell
                const tdLabel = document.createElement("td");
                tdLabel.innerHTML = total.label;
                tdLabel.colSpan = data.table.columns.length - 1;
            
                // Value cell
                const tdValue = document.createElement("td");
                tdValue.innerHTML = total.text;
                tdValue.id = total.key;
                if (total.class) tdValue.classList.add(total.class);
                tdValue.dataset.value = total.number;
            
                // Bold formatting
                if (total.emphasis) {
                    tdLabel.style.fontWeight = 'bold';
                    tdValue.style.fontWeight = 'bold';
                }
            
                tr.appendChild(tdLabel);
                tr.appendChild(tdValue);
                rowsBody.appendChild(tr);
            
                // ---- Tambahkan DPP Nilai Lain ----
                if (["subtotal", "sub-total"].includes(total.label.trim().toLowerCase()) && total.number) {
                    const dppNilaiLain = Math.round((11 / 12) * total.number * 100) / 100;
            
                    // Cari TaxAmount untuk format
                    const tax = (data.table.totals || []).find(t => t.class === "taxAmount");
            
                    const trDpp = document.createElement("tr");
                    trDpp.className = 'total';
            
                    const tdLabelDpp = document.createElement("td");
                    tdLabelDpp.innerHTML = "DPP Nilai Lain";
                    tdLabelDpp.colSpan = data.table.columns.length - 1;
            
                    const tdValueDpp = document.createElement("td");
            
                    if (tax) {
                        const taxText = tax.text;
            
                        // Ambil simbol mata uang dari awal text
                        const currencyMatch = taxText.match(/^(\D*)/);
                        const currencySymbol = currencyMatch ? currencyMatch[1] : '';
            
                        const match = taxText.match(/(\d{1,3}([.,]\d{3})*)([.,](\d+))?/);
            
                        if (match) {
                            const decimalDigits = match[4] ? match[4].length : 0;
                            const thousandSep = match[2] ? match[2][0] : '.';
                            const decimalSep = match[3] ? match[3][0] : ',';
            
                            let roundedValue = dppNilaiLain.toFixed(decimalDigits);
                            let [intPart, decPart] = roundedValue.split('.');
                            intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSep);
            
                            tdValueDpp.innerHTML = decPart ? `${currencySymbol}${intPart}${decimalSep}${decPart}` : `${currencySymbol}${intPart}`;
                        } else {
                            tdValueDpp.innerHTML = `${currencySymbol}${dppNilaiLain}`;
                        }
                    } else {
                        tdValueDpp.innerHTML = dppNilaiLain;
                    }
            
                    tdValueDpp.dataset.value = dppNilaiLain.toFixed(2).replace(".", ",");
            
                    trDpp.appendChild(tdLabelDpp);
                    trDpp.appendChild(tdValueDpp);
                    rowsBody.appendChild(trDpp);
                }
            });

			// CUSTOM FIELDS
			// Display custom fields like notes, terms, payment instructions, etc.
			const customFieldsDiv = document.getElementById("custom-fields");
			customFieldsDiv.innerHTML = "";
			(data.custom_fields || []).forEach(f => {
				// Some custom fields can be displayed at the top with other document fields
				if (f.displayAtTheTop) {
					// 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 = 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);
				}
			});

			
			// 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);
			}

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

Thanks for the reply. i managed to get the DPPNL working on my own. but my problem is the retention part. i used to have a retention field, where it takes the amount from a custom-field (not shown on printed doc) called Retention (%), and multiply by the Sub-Total. then Balance Due is just Total - Retention. I cant seem to get it working. both column to show just below Total.

If you are using Custom field, you can call it in custom themes and process it in your script.
Make sure to select Show custom field on printed documents for your custom field.

i tried it on Footers before and it worked but not anymore.

Do i really need to show on printed docs? used to work on Footers, not shown on printed docs

Hi, Iam newbee here.

It’s nice to have and try your custom theme here. But for my private purpose and my wild ignoring the standard process (I just want to get the Invoice style), when i use tax code in multiple rates, then I want to withholding tax become minus so the total net result is representing the real amount we will receive. Could you please handle the custom theme for me? Thank you

You do not need to create multiple rates in the tax code. The tax code is not intended for Withholding Tax Deductions. It is preferable to use the Withholding Tax Deduction feature and manually enter the calculated amount of Article 23 of the Withholding Tax (PPh Pasal 23), which is determined by multiplying the sub-total by 2%. If using a percentage, the withholding tax will be applied to the total after adding VAT. The label Withholding Tax or Potongan Pajak Penghasilan can be customized from the theme by changing the total.label value. Assign it with PPh Pasal 23 2% or something like that.

See this guide:

Enable Withholding Tax through Settings → Withholding Tax.


Enable/Add a Withholding Tax of type Amount (manually calculate the value) in the invoice.

Enable/Add a Custom Theme specifically to display DPP Nilai Lain and replace the label Withholding Tax / Potongan Pajak Penghasilan with PPh Pasal 23.

Custom Theme Script

<!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: A4;
			*/
			
			/* Page margins - adjust these for more/less white space around content */
			margin: 20mm;
			
			/* 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 16px 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>
	<!-- 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>
				<td>
					<!-- DOCUMENT HEADER - Title and logo -->
					<header style="display: flex; justify-content: space-between; align-items: flex-start; 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"></h1>
						<!-- Business logo container - image will be inserted here -->
						<div id="business-logo" style="text-align: end"></div>
					</header>

					<!-- INFO SECTION - Recipient, fields, and business details -->
					<section style="display: flex; margin-bottom: 20px; width: 100%; align-items: flex-start; gap: 20px">
						<!-- Recipient/customer address (left side) -->
						<address id="recipient-info" style="flex: 1"></address>
						<!-- Document fields (invoice number, date, etc.) - middle -->
						<dl id="fields" style="flex: 1; text-align: end"></dl>
						<!-- 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>
					</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>
					<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>

					<!-- 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: 150px; 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-info").innerHTML = `<strong>${recipient.name || ""}</strong><br>${recipient.address ? recipient.address.replace(/\n/g, "<br>") : ""}`;

			// DOCUMENT FIELDS (middle section)
			// These are key-value pairs like Invoice Number, Date, Due Date, etc.
			const fieldsDiv = document.getElementById("fields");
			fieldsDiv.innerHTML = "";
			(data.fields || []).forEach(f => {
				const dt = document.createElement("dt"); // Label
				dt.innerHTML = f.label;
				const dd = document.createElement("dd"); // Value
				dd.innerHTML = f.text;
				fieldsDiv.appendChild(dt);
				fieldsDiv.appendChild(dd);
			});

			// 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;
				td.innerHTML = col.sumText; // Column sum if applicable
				tr.appendChild(td);
			});
			// Only add row if it has content
			if (tr.innerText) rowsBody.appendChild(tr);

            // TOTALS SECTION
            let dppAdded = false; // <--- prevent duplicates
            
            (data.table.totals || []).forEach((total, index) => {
                const tr = document.createElement("tr");
                tr.className = 'total';
            
                const tdLabel = document.createElement("td");
				
				let labelText = total.label;
				// replace English or Indonesian WHT → PPh 23
				if (
					labelText.trim().toLowerCase() === "withholding tax" ||
					labelText.trim().toLowerCase() === "potongan pajak penghasilan"
				) {
					labelText = "PPh Pasal 23";
				}

				tdLabel.innerHTML = labelText;
                tdLabel.colSpan = data.table.columns.length - 1;
            
                const tdValue = document.createElement("td");
                tdValue.innerHTML = total.text;
                tdValue.id = total.key;
                if (total.class) tdValue.classList.add(total.class);
                tdValue.dataset.value = total.number;
            
                if (total.emphasis) {
                    tdLabel.style.fontWeight = 'bold';
                    tdValue.style.fontWeight = 'bold';
                }
            
                tr.appendChild(tdLabel);
                tr.appendChild(tdValue);
                rowsBody.appendChild(tr);
            
                // ---- Add DPP Nilai Lain only once ----
                if (!dppAdded && ["subtotal", "sub-total"].includes(total.label.trim().toLowerCase()) && total.number) {
                    dppAdded = true;
            
                    const dppNilaiLain = Math.round((11 / 12) * total.number * 100) / 100;
                    const tax = (data.table.totals || []).find(t => t.class === "taxAmount");
            
                    const trDpp = document.createElement("tr");
                    trDpp.className = 'total';
            
                    const tdLabelDpp = document.createElement("td");
                    tdLabelDpp.innerHTML = "DPP Nilai Lain";
                    tdLabelDpp.colSpan = data.table.columns.length - 1;
            
                    const tdValueDpp = document.createElement("td");
            
                    if (tax) {
                        const taxText = tax.text;
            
                        const currencyMatch = taxText.match(/^(\D*)/);
                        const currencySymbol = currencyMatch ? currencyMatch[1] : '';
            
                        const match = taxText.match(/(\d{1,3}([.,]\d{3})*)([.,](\d+))?/);
            
                        if (match) {
                            const decimalDigits = match[4] ? match[4].length : 0;
                            const thousandSep = match[2] ? match[2][0] : '.';
                            const decimalSep = match[3] ? match[3][0] : ',';
            
                            let roundedValue = dppNilaiLain.toFixed(decimalDigits);
                            let [intPart, decPart] = roundedValue.split('.');
                            intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSep);
            
                            tdValueDpp.innerHTML = decPart ? `${currencySymbol}${intPart}${decimalSep}${decPart}` : `${currencySymbol}${intPart}`;
                        } else {
                            tdValueDpp.innerHTML = `${currencySymbol}${dppNilaiLain}`;
                        }
                    } else {
                        tdValueDpp.innerHTML = dppNilaiLain;
                    }
            
                    tdValueDpp.dataset.value = dppNilaiLain.toFixed(2).replace(".", ",");
            
                    trDpp.appendChild(tdLabelDpp);
                    trDpp.appendChild(tdValueDpp);
                    rowsBody.appendChild(trDpp);
                }
            });

            

			// CUSTOM FIELDS
			// Display custom fields like notes, terms, payment instructions, etc.
			const customFieldsDiv = document.getElementById("custom-fields");
			customFieldsDiv.innerHTML = "";
			(data.custom_fields || []).forEach(f => {
				// Some custom fields can be displayed at the top with other document fields
				if (f.displayAtTheTop) {
					// 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 = 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);
				}
			});

			
			// 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);
			}

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