Temporary Suspension of ZatcaEGS Service

For QR Phase I, there is no problem anymore, but for users who use QR Phase II, This can be a solution,

User checks Show custom field on printed documents on Base64 QRCode Custom fields.

Modify Default Themes to display QRCode Phase II if Base64 QRCode Custom fields have a value.

// Render custom fields section below the table (e.g. notes, terms)
			const customFieldsDiv = document.getElementById("custom-fields");
			customFieldsDiv.innerHTML = "";
			let qrcodetext = "";
			(data.custom_fields || []).forEach(f => {
                if (f.label === "Base64 QRCode") {
                    console.log("Found Base64 QRCode:", f.text);
                    qrcodetext = f.text;
                } else {
                    const div = document.createElement("div");
                    div.innerHTML = `<strong>${f.label}</strong><br />${(f.text || "").split("\n").join("<br />")}<br /><br />`;
                    customFieldsDiv.appendChild(div);
                }
            });
// Jika QR code have value, render to footers
			if (qrcodetext && qrcodetext.length > 10) {
				const existingQR = document.getElementById("signedQrCode");
				if (existingQR) existingQR.remove();

				const qrDiv = document.createElement("div");
				qrDiv.id = "signedQrCode";
				qrDiv.style.padding = "20px";
				footersDiv.appendChild(qrDiv);

				renderQRCode(qrcodetext, "signedQrCode");
			} else {
				if (data.type === "salesinvoice" && 
					(data.business.country === 'ar-SA' || data.business.country === 'en-SA')) {

					let qrBuffer = "";

					function appendTLV(tag, text) {
						qrBuffer += String.fromCharCode(tag);
						qrBuffer += String.fromCharCode(text.length);
						qrBuffer += text;
					}

					function utf8ToBase64(str) {
						const utf8Bytes = new TextEncoder().encode(str);
						const binaryStr = Array.from(utf8Bytes, byte => String.fromCharCode(byte)).join('');
						return btoa(binaryStr);
					}

					appendTLV('1', data.business.name || 'No name');
					appendTLV('2', '0000000000000');
					appendTLV('3', data.timestamp);
					appendTLV('4', '0.00');
					appendTLV('5', '0.00');

					new QRCode(document.getElementById("qrcode"), {
						text: utf8ToBase64(qrBuffer),
						width: 128,
						height: 128,
						colorDark: "#000000",
						colorLight: "#ffffff",
						correctLevel: QRCode.CorrectLevel.L
					});
				}
			}
        
            function renderQRCode(content, elementId) {
                new QRCode(document.getElementById(elementId), {
                    text: content,
                    width: 160,
                    height: 160,
                    colorDark: "#000000",
                    colorLight: "#fafafa",
                    correctLevel: QRCode.CorrectLevel.L
                });
        
                //parsing QR for tooltip
                let details = parseQRCodeContent(content);
                if (details.size > 0) {
                    let title = Array.from(details)
                        .map(([tag, value]) => `Tag ${tag} : ${value.join(', ')}`)
                        .join('\n');
                    const qrCodeDiv = document.getElementById(elementId);
                    if (qrCodeDiv) qrCodeDiv.title = title.trim();
                }
            }
        
            function parseQRCodeContent(qrCodeBase64) {
                let details = new Map();
                try {
                    let data = atob(qrCodeBase64.replace(/\+/g, '+'));
                    let index = 0;
                    while (index < data.length) {
                        let tag = data.charCodeAt(index++);
                        let length = data.charCodeAt(index++);
                        let value = data.substr(index, length);
                        index += length;
                        let decodedValue = (tag === 8 || tag === 9)
                            ? [...value].map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ').toUpperCase()
                            : new TextDecoder('utf-8').decode(new Uint8Array([...value].map(c => c.charCodeAt(0))));
                        details.set(tag, [decodedValue]);
                    }
                } catch (ex) {
                    console.error("Error decoding QR code: " + ex.message);
                }
                return details;
            }

Full modified Default Custom Theme, Support QRCode Phase 1 & Phase II

<!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>
		body {
			margin: 30px;
			font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
			color: #171717;
			font-size: 12px;
			line-height: 1.428571429;
		}

		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>
    
	<table  style='border-collapse: collapse; width: 100%'>
	    <tr><td><div id='qrcode' style='margin-bottom: 20px'></div></td></tr>

	    <!-- Section for any additional custom fields -->
	    <tr><td><div id='custom-fields'  style=' word-break: break-all; overflow-wrap: break-word; white-space: normal;'></div></td></tr>

	    <!-- Section for footers -->
	    <tr><td><div id='footers'  style='word-break: break-all; overflow-wrap: break-word; white-space: normal;'></div></td></tr>
    </table>
    
	<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 id="BusinessName">${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;
				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 = '';
			
			let qrcodetext = '';
			(data.custom_fields || []).forEach(f => {
                if (f.label === 'Base64 QRCode') {
                    console.log('Found Base64 QRCode:', f.text);
                    qrcodetext = f.text;
                } else {
                    console.log(f.label + ':', f.text);
                    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);
			}
            
            const qrcodeDiv = document.getElementById('qrcode');
            // This handles QR code Phase II on invoices for Saudi Arabia
            if (qrcodetext && qrcodetext.length > 10 && qrcodeDiv) {
            	qrcodeDiv.innerHTML = '';
            	renderQRCode(qrcodetext, 'qrcode'); 
			} else {
				// This handles QR code on invoices for Saudi Arabia - this is here just temporarily. Better way will be implemented.
    			if (data.type === 'salesinvoice') {
    				if (data.business.country === 'ar-SA' || data.business.country === 'en-SA') {
    
    					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));
    
    					console.log(qrData);
    
    					new QRCode(document.getElementById('qrcode'), {
    						text: qrData,
    						width: 128,
    						height: 128,
    						colorDark: '#000000',
    						colorLight: '#ffffff',
    						correctLevel: QRCode.CorrectLevel.L
    					});
    				}
    			}
			}
        
            function renderQRCode(content, elementId) {
                new QRCode(document.getElementById(elementId), {
                    text: content,
                    width: 160,
                    height: 160,
                    colorDark: '#000000',
                    colorLight: '#fafafa',
                    correctLevel: QRCode.CorrectLevel.L
                });
        
                // Tambahan opsional: parsing isi QR untuk tooltip
                let details = parseQRCodeContent(content);
                if (details.size > 0) {
                    let title = Array.from(details)
                        .map(([tag, value]) => `Tag ${tag} : ${value.join(', ')}`)
                        .join('\n');
                    const qrCodeDiv = document.getElementById(elementId);
                    if (qrCodeDiv) qrCodeDiv.title = title.trim();
                }
            }
        
            function parseQRCodeContent(qrCodeBase64) {
                let details = new Map();
                try {
                    let data = atob(qrCodeBase64.replace(/\+/g, '+'));
                    let index = 0;
                    while (index < data.length) {
                        let tag = data.charCodeAt(index++);
                        let length = data.charCodeAt(index++);
                        let value = data.substr(index, length);
                        index += length;
                        let decodedValue = (tag === 8 || tag === 9)
                            ? [...value].map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ').toUpperCase()
                            : new TextDecoder('utf-8').decode(new Uint8Array([...value].map(c => c.charCodeAt(0))));
                        details.set(tag, [decodedValue]);
                    }
                } catch (ex) {
                    console.error('Error decoding QR code: ' + ex.message);
                }
                return details;
            }

		}, false);

		// Request context data from parent frame when page loads
		window.addEventListener('load', () =>
			window.parent.postMessage({ type: 'context-request' }, '*')
		);
	</script>
	
</body>
</html>

@Mabaega I’m working on ability to create “Image Custom Fields”

This way your integration can simply upload image of QR code to invoice. No script necessary.

With scripts, it’s too fragile and for most users too technical.

@lubos,
Desktop 25.6.24.2435
Element ID missing in the last cell.

<tfoot id="totals">
	<tr>
		<td colspan="7" style="text-align: end;">Sub-total</td>
		<td data-value="127000" style="text-align: right;">ريال 127,000.00</td>
	</tr>
	<tr>
		<td colspan="7" style="text-align: end;">ضريبة القيمة المضافة 15%</td>
		<td data-value="18900" class="taxAmount" style="text-align: right;">ريال 18,900.00</td>
	</tr>
	<tr>
		<td colspan="7" style="text-align: end; font-weight: bold;">Total</td>
		<td data-value="145900" style="text-align: right; font-weight: bold;">ريال 145,900.00</td>
	</tr>
</tfoot>

ZatcaEGS needs this ID to obtain the Total Invoice value.

<td id="Total" data-value="145900" style="text-align: right; font-weight: bold;">ريال 145,900.00</td>

Is this custom theme by any chance? In my testing, I see id=Total attribute.

For the future, it’s better not to rely on HTML because it can really contain anything.

There is new variable I’m sending to relay endpoint called ViewData which contains total within its json object.

Also, the latest version has ability to create image custom fields. I’m working on leveraging it for Zatca Phase 1 implementation which will work without javascript.

Oh yes, this is my mistake. The element id for total value is still there.