Thank you and appreciate @Mabaega for your immediate help, it works.
I understand what you mean.
Update to ZatcaEGS v25.06.27.0003.
Let’s give it a try…
@AYUB please check the latest version (25.6.28.2459) to see if your issue with scrollbars has been resolved.
how do I know if it is updated in manager or how can I update manager to get ZatcaEGS v25.06.27.0003
Create new business data, and integrate it to Non Production environment. I want to make sure this really works. I am preparing a way to auto update all customfields for business data that is already integrated with previous version of zatcaegs.
Why the issue with printing (in new update) is not being solved so far. It has been many days. Previously i used to select only the printer (80 mm or A5) while printing the receipt or invoice and it used to print perfectly. But now this is not working. Even the invoices are not being printed on A5 properly.
@lubos Thanks, its ok now no scroll bar is showing on view screen
As you can see, the footer is getting split into 2 pages. No matter what i do -scale down, reduce margins, etc. i end up with the same result. How can I get all this into a single page.
P.S.: Its a custom theme
There will be soon new version of theme enhancer which will be able to design templates with proper headers and footer.
Please keep in mind the Solution for Top Header (Logo, Business Details, All Field, Client Details, Description) Repeat on all pages.
If you want to use a custom theme, you might get some ideas from this example.
Not so good, but it seems to fit what you want
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
*, ::after, ::before, ::backdrop, ::file-selector-button {
margin: 0;
padding: 0;
box-sizing: border-box;
border: 0 solid;
}
body {
margin: 0;
padding: 0;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #171717;
font-size: 12px;
line-height: 1.428571429;
}
table {
font-size: 12px;
width: 100%;
border-collapse: collapse;
}
.table-default {
margin-bottom: 10px;
}
#main-items-table {
margin-top: 10px;
margin-bottom: 0px;
}
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;
}
tfoot#dummy-footer-row td {
padding-bottom: 0;
border-left: 1px solid #000;
border-right: 1px solid #000;
border-bottom: 1px solid #000;
height: 0.5em;
background: #fff;
}
.totals-table {
border-collapse: collapse;
width: 100%;
margin-top: -1px;
border: none;
}
.totals-table tr td,
.totals-table tr th {
border: none !important;
}
.totals-table td,
.totals-table th {
padding: 5px 10px;
font-size: 12px;
text-align: end;
}
.totals-table .label-cell {
text-align: end;
border: none !important;
}
.totals-table .col-total {
width: 100px;
white-space: nowrap;
border: 1px solid #000 !important;
background: #fff;
text-align: end;
}
.totals-table tr:first-child td,
.totals-table tr:first-child th {
border-top: 1px solid #000 !important;
}
#title {
font-size: 32px;
line-height: 32px;
font-weight: bold;
}
.text-end {
text-align: end;
}
.text-start {
text-align: start;
}
.text-center {
text-align: center;
}
.vertical-align-top {
vertical-align: top;
}
.spacer-20 {
width: 20px;
}
.divider {
width: 1px;
border-left: 1px solid #000;
}
@media screen {
.screen-padding {
padding: 30px;
}
}
@media print {
@page {
size: A4;
margin: 30px;
}
.screen-padding {
padding: 0px;
}
}
</style>
</head>
<body>
<div class="screen-padding">
<table id="main-items-table">
<thead>
<tr><td colspan="100%">
<table class="table-default">
<tbody>
<tr>
<td class="vertical-align-top">
<div id="title">Invoice</div>
</td>
<td class="text-end" id="business-logo"></td>
</tr>
</tbody>
</table>
</td></tr>
<tr><td colspan="100%">
<table class="table-default">
<tbody>
<tr>
<td class="vertical-align-top" id="recipient-info"></td>
<td class="text-end vertical-align-top" id="fields"></td>
<td class="spacer-20"></td>
<td class="divider"></td>
<td class="spacer-20"></td>
<td class="vertical-align-top" id="business-info"></td>
</tr>
</tbody>
</table>
</td></tr>
<tr><td colspan="100%">
<table class="table-default">
<tr>
<td id="description" style="font-weight: bold; font-size: 14px;"></td>
</tr>
</table>
</td></tr>
<tr id="table-headers"></tr>
</thead>
<tbody id="table-rows"></tbody>
<tfoot id="dummy-footer-row"></tfoot>
</table>
<table class="totals-table" id="totals-table"></table>
<table class="table-default">
<tr>
<td id="qrcode" class="text-start" style="width: 30%;"></td>
<td id="status" class="text-center" style="width: 40%;"></td>
<td id="tdnone" class="text-end" style="width: 30%;"></td>
</tr>
</table>
<table class="table-default"><tr><td><div id="custom-fields"></div></td></tr></table>
<table class="table-default"><tr><td><div id="footers"></div></td></tr></table>
<script src="resources/qrcode/qrcode.js"></script>
<script>
window.addEventListener("message", (event) => {
if (event.source !== window.parent) return;
if (event.data.type !== 'context-response') return;
const data = event.data.body;
// Log seluruh data JSON ke console
console.log("Received JSON data:", data);
document.documentElement.dir = data.direction;
document.getElementById("title").innerHTML = data.title || "No title";
document.getElementById("description").innerHTML = data.description || "";
// Business logo
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);
}
// Business info
const business = data.business || {};
document.getElementById("business-info").innerHTML = `<strong>${business.name || ""}</strong><br>${business.address ? business.address.replace(/\n/g, "<br>") : ""}`;
const recipient = data.recipient || {};
document.getElementById("recipient-info").innerHTML = `<strong>${recipient.name || ""}</strong><br>${recipient.address ? recipient.address.replace(/\n/g, "<br>") : ""}`;
const fieldsDiv = document.getElementById("fields");
fieldsDiv.innerHTML = "";
(data.fields || []).forEach(f => {
const div = document.createElement("div");
div.style.marginBottom = "0.3rem";
div.innerHTML = `
<div><strong>${f.label}</strong></div>
<div>${f.text || ""}</div>
`;
fieldsDiv.appendChild(div);
});
// Cari text terpanjang dari total.text
function getTextWidth(text, font = "12px Helvetica, Arial, sans-serif") {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
ctx.font = font;
return ctx.measureText(text).width;
}
const totalTexts = (data.table && data.table.totals || []).map(t => t.text || "");
const longestText = totalTexts.reduce((a, b) => (a.length > b.length ? a : b), "");
const estimatedWidth = getTextWidth(longestText) + 20 + 4;
//console.log(longestText);
const widthPx = estimatedWidth + "px";
//console.log(widthPx);
const headersRow = document.getElementById("table-headers");
headersRow.innerHTML = "";
const columns = data.table.columns || [];
columns.forEach((col, idx) => {
const th = document.createElement("th");
th.innerHTML = col.label;
th.style.textAlign = col.align;
const isLast = idx === columns.length - 1;
if (isLast) {
// Set lebar tetap untuk kolom terakhir
th.style.width = widthPx;
th.style.minWidth = widthPx;
th.style.maxWidth = widthPx;
th.style.whiteSpace = "nowrap";
} else if (col.minWidth) {
th.style.whiteSpace = 'nowrap';
th.style.width = '1px';
} else if (col.nowrap) {
th.style.whiteSpace = 'nowrap';
th.style.width = '60px';
}
headersRow.appendChild(th);
});
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 = '60px';
}
tr.appendChild(td);
});
rowsBody.appendChild(tr);
});
// --- DUMMY FOOTER ROW FOR REPEATED TABLE FOOTER ---
const tfoot = document.getElementById("dummy-footer-row");
tfoot.innerHTML = "";
const dummyTr = document.createElement("tr");
for (let i = 0; i < data.table.columns.length; i++) {
const td = document.createElement("td"); // dummy, blank
dummyTr.appendChild(td);
}
tfoot.appendChild(dummyTr);
// ---
const totalsTable = document.getElementById("totals-table");
totalsTable.innerHTML = '';
const mainCols = data.table.columns;
const colCount = mainCols.length;
const totalRows = data.table.totals || [];
// Cari custom field 'Total amount in words' (bahasa Inggris atau Arab)
const amountInWordsField = (data.custom_fields || []).find(f =>
f.label === "Total amount in words" || f.label === "إجمالي القيمة بالأحرف"
);
let tdblank;
totalRows.forEach((total, idx) => {
const tr = document.createElement("tr");
if (idx === 0) {
tdblank = document.createElement("td");
tdblank.className = 'label-cell';
tdblank.rowSpan = totalRows.length + 1;
tdblank.colSpan = colCount - 3;
tdblank.style.textAlign = 'start';
tdblank.style.verticalAlign = 'top';
tdblank.style.borderTop = '1px solid #000';
tdblank.style.padding = '10px';
tdblank.style.paddingInlineStart = '0px';
if (amountInWordsField) {
tdblank.innerHTML = `<strong>${amountInWordsField.label}</strong><br />${(amountInWordsField.text || "").split("\n").join("<br />")}`;
} else {
tdblank.innerHTML = ' ';
}
tr.appendChild(tdblank);
}
// Label cell
const tdLabel = document.createElement("td");
tdLabel.className = 'label-cell';
tdLabel.colSpan = 2;
tdLabel.style.textAlign = 'end';
tdLabel.innerHTML = total.label;
if (idx === 0) tdLabel.style.borderTop = '1px solid #000';
tr.appendChild(tdLabel);
// Amount cell
const totalTd = document.createElement("td");
totalTd.className = 'col-total';
totalTd.style.width = widthPx;
totalTd.style.minWidth = widthPx;
totalTd.style.maxWidth = widthPx;
totalTd.id = total.key;
totalTd.style.textAlign = 'right';
totalTd.innerHTML = total.text;
if (total.class) totalTd.classList.add(total.class);
totalTd.dataset.value = total.number;
if (total.emphasis) totalTd.style.fontWeight = 'bold';
totalTd.style.border = '1px solid #000';
if (idx === 0) totalTd.style.borderTop = '1px solid #000';
tr.appendChild(totalTd);
totalsTable.appendChild(tr);
});
// Tambahkan baris kosong tambahan (tanpa border) setelah total rows
const emptyRow = document.createElement("tr");
const emptyLabel = document.createElement("td");
emptyLabel.colSpan = 2;
emptyLabel.innerHTML = " ";
emptyLabel.style.border = "none";
emptyRow.appendChild(emptyLabel);
const emptyValue = document.createElement("td");
emptyValue.innerHTML = " ";
emptyValue.style.border = "none";
emptyRow.appendChild(emptyValue);
totalsTable.appendChild(emptyRow);
// --- Tampilkan custom field lain selain 'Total amount in words'
const customFieldsDiv = document.getElementById("custom-fields");
customFieldsDiv.innerHTML = "";
(data.custom_fields || []).forEach(f => {
//for zatca qrcode phase II
if (f.key === "a1b2c3d4-e5f6-4abc-8def-abcdef000018") return;
if (f.label === "Total amount in words" || f.label === "إجمالي القيمة بالأحرف") return;
const text = (f.text || "").trim();
if (text === "") return; // skip jika kosong atau hanya spasi
const div = document.createElement("div");
div.style.wordWrap = "break-word"; // Allow break inside long words
div.style.whiteSpace = "normal"; // Allow wrapping
div.style.overflowWrap = "break-word"; // Modern browsers
div.style.maxWidth = "100%"; // Optional: limit width to container
if (f.displayAtTheTop === true) {
div.style.marginBottom = "0.3rem";
div.innerHTML = `
<div><strong>${f.label}</strong></div>
<div>${(f.text || "").split("\n").join("<br />")}</div>
`;
fieldsDiv.appendChild(div);
} else {
div.innerHTML = `<strong>${f.label}</strong><br />${(f.text || "").split("\n").join("<br />")}<br /><br />`;
customFieldsDiv.appendChild(div);
}
});
//remove last div margin from fields cells
if (fieldsDiv.lastElementChild) {
fieldsDiv.lastElementChild.style.marginBottom = "0rem";
}
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);
const scripts = div.querySelectorAll("script");
scripts.forEach(script => {
const newScript = document.createElement("script");
for (const attr of script.attributes) {
newScript.setAttribute(attr.name, attr.value);
}
if (script.textContent) {
newScript.textContent = script.textContent;
}
script.parentNode.replaceChild(newScript, script);
});
});
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);
}
//for zatca qrcode phase II
const qrcodetext = (data.custom_fields || [])
.find(f => f.key === "a1b2c3d4-e5f6-4abc-8def-abcdef000018")?.text || "";
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);
window.addEventListener("load", () =>
window.parent.postMessage({ type: "context-request" }, "*")
);
</script>
</div>
</body>
</html>
Thank you so much, it will help a lot @Mabaega
All Good, a small issue in the theme, please, if you can help
- While clicking on Print Numbers are not coming into the box properly.
- While in Print option, A4 size top header is not repeating, If Legal, then Top header is repeating.
- When generating a direct PDF, there is no margin or space around the Table, Title, and Logo.
Yes, printing web pages can be quite tricky. I’ve made some adjustments to the code—hopefully it works better now. The output for both Print and PDF should now be more consistent.
Thank you @Mabaega
The top header is not repeating now.
The template code works well with my business data.
Try creating a new theme and replace all the code with the code above.
Link it to your invoice — it should work properly.
It’s highly recommended that you do not modify the theme above unless you fully understand what you’re doing.
Manager version 25.7.2.2469 has some issues with print settings. The previous version before this version used to print the invoices, etc. at the correct scale.
Now, version 25.7.2.2469 cuts off margins when printing invoices, etc.
Please check to verify.
Hello @frank2cook,
Welcome to the forum.
I have moved your post to this megathread which is meant to address all teething problems for the new and improved Themes
.
Please share some screenshots of your problem so we can see exactly what you see.
Also, please feel free to go through this topic to see if any previous post is helpful in your case, you can also use AI to summarize this long thread, the button should have this icon: