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>

