It’s much easier to troubleshoot once you realize the old “Theme Enhancer” approach isn’t the primary way to handle those customizations anymore. I appreciate you taking the time to explain the transition to the more integrated settings - it definitely clears up a lot of confusion!
I do it amazing but waiting to customise the sales invoices and receipts etc..![]()
If this is the case, then this needs to be announced officially and not leave users in the dark. It seems pointless to develop something, get us to use it and then abandon it leaving us in the lurch
We knew manager to embody legacy accounting functions. It is important to use known wheels rather than try to discover new ways of doing the same thing that people have done for 2000 years or more.
Which AI version were you using so that some of our clients can purchase?
You can use free AI chat like ChatGPT or DeepSeek, which supports long code
Do you have a sample? Or should we hire an AI expert?
It does require some persistence, but if you provide your old theme code along with the default custom theme code to the AI, and ask it to recreate the same appearance as your old theme using the new theme structure, you should be able to see the result.
Or perhaps you could also provide this code as a reference. This code produces a layout similar to the default view without custom themes, with the addition of the amount in words in two languages, Arabic and English and consistent layout across the on‑screen view, print preview, and PDF output.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice Document</title>
<style>
/* Print & reset styles */
@page {
size: A4;
margin: 0mm;
}
:root {
--bg: #f5f5f5;
--paper: #ffffff;
--border: #e5e5e5;
--black: #000000;
}
body {
background-color: var(--bg);
margin: 0;
padding: 0;
display: flex;
justify-content: center;
font-size: 0.875rem;
line-height: 1.4rem;
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
main {
background-color: var(--paper);
margin: 6mm;
border: 1px solid var(--border);
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
padding: 2rem;
max-width: 210mm;
width: 100%;
box-sizing: border-box;
}
main > *:first-child {
margin-top: 0;
padding-top: 0;
}
@media print {
body {
background: none;
margin: 0;
padding: 0;
display: block;
}
main {
margin: 0;
padding: 2rem;
border: none;
box-shadow: none;
border-radius: 0;
max-width: none;
width: auto;
}
}
address {
font-style: normal;
line-height: 1.4em;
}
/* Table styles */
table {
font-size: 14px;
width: 100%;
border-collapse: collapse;
}
#table-headers th {
font-weight: bold;
padding: 5px 10px;
border: 1px solid var(--black);
text-align: start;
}
#table-rows td {
padding: 5px 5px;
text-align: start;
vertical-align: top;
}
#table-rows tr.row td {
border-inline-start: 1px solid var(--black);
border-inline-end: 1px solid var(--black);
}
#table-rows tr.last-row td {
padding-bottom: 30px;
border-bottom: 1px solid var(--black);
}
#table-rows tr.column-total td {
font-weight: bold;
border: 1px solid var(--black);
white-space: nowrap;
text-align: right;
}
#table-rows tr.total td {
white-space: nowrap;
}
#table-rows tr.total td:first-child {
text-align: end;
}
#table-rows tr.total td:last-child {
border: 1px solid var(--black);
text-align: right;
}
.business-logo-img {
max-height: 75px;
max-width: 150px;
display: inline;
}
.separator-line {
width: 1px;
border-inline-start: 1px solid #000;
align-self: stretch;
}
.section-flex {
display: flex;
margin-bottom: 20px;
width: 100%;
align-items: flex-start;
gap: 20px;
}
.recipient-address {
flex: 1;
}
/* Perbaikan: fields-right agar sejajar */
.fields-right {
flex: 1;
text-align: end;
margin: 0;
padding: 0;
}
.fields-right dl {
margin: 0;
line-height: 1.4em; /* sama dengan address */
}
.fields-right dt {
margin: 0;
padding: 0;
font-weight: bold;
line-height: inherit;
}
.fields-right dd {
margin: 0 0 8px 0;
padding: 0;
line-height: inherit;
}
.fields-right dd:last-child {
margin-bottom: 0;
}
.business-address {
white-space: nowrap;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.footer-text {
margin-top: 20px;
}
.status-message {
margin-top: 40px;
text-align: center;
}
.status-span {
border-width: 5px;
border-style: solid;
padding: 10px;
font-size: 20px;
text-transform: uppercase;
}
.status-positive {
color: green;
border-color: green;
}
.status-negative {
color: red;
border-color: red;
}
.amount-words {
margin: 20px 0;
}
.amount-words div:first-child {
font-weight: bold;
}
</style>
</head>
<body>
<main>
<table style="width: 100%;">
<thead>
<tr>
<td>
<div class="flex-between">
<h1 id="title"></h1>
<div id="business-logo" style="text-align: end;"></div>
</div>
<div class="section-flex">
<address id="recipient-info" class="recipient-address"></address>
<div id="fields" class="fields-right"></div>
<div aria-hidden="true" class="separator-line"></div>
<address id="business-info" class="business-address"></address>
</div>
<p style="font-weight: bold; font-size: 14px; margin-bottom: 20px" id="description"></p>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<table style="border-collapse: collapse; width: 100%;">
<thead id="table-headers"></thead>
<tbody id="table-rows"></tbody>
</table>
<div id="amount-in-words" class="amount-words"></div>
<div id="qrcode" style="margin-bottom: 20px;"></div>
<div id="custom-fields"></div>
<div id="footers"></div>
<div id="status" class="status-message"></div>
</td>
</tr>
</tbody>
</table>
</main>
<script>
// --------------------------------------------------------------
// Helper: Convert number to words (English)
// --------------------------------------------------------------
function numberToWordsEn(num) {
if (num === null || isNaN(num)) return '';
const isNegative = num < 0;
let absolute = Math.abs(num);
const integerPart = Math.floor(absolute);
const decimalPart = Math.round((absolute - integerPart) * 100);
const ones = ['', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine',
'Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen',
'Seventeen', 'Eighteen', 'Nineteen'];
const tens = ['', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety'];
function convertHundreds(n) {
if (n === 0) return '';
let result = '';
if (n >= 100) {
result += ones[Math.floor(n / 100)] + ' Hundred ';
n %= 100;
}
if (n >= 20) {
result += tens[Math.floor(n / 10)] + ' ';
n %= 10;
}
if (n > 0 && n < 20) {
result += ones[n] + ' ';
}
return result.trim();
}
function convertNumber(n) {
if (n === 0) return 'Zero';
let words = '';
if (n >= 1000000) {
words += convertNumber(Math.floor(n / 1000000)) + ' Million ';
n %= 1000000;
}
if (n >= 1000) {
words += convertNumber(Math.floor(n / 1000)) + ' Thousand ';
n %= 1000;
}
if (n >= 100) {
words += convertHundreds(n);
n = 0;
} else if (n > 0) {
if (n < 20) words += ones[n] + ' ';
else {
words += tens[Math.floor(n / 10)] + ' ';
if (n % 10 > 0) words += ones[n % 10] + ' ';
}
}
return words.trim();
}
let result = isNegative ? 'Negative ' : '';
if (integerPart === 0 && decimalPart === 0) {
result += 'Zero';
} else {
if (integerPart > 0) result += convertNumber(integerPart);
if (decimalPart > 0) result += ' point ' + convertNumber(decimalPart);
}
return result.trim();
}
// --------------------------------------------------------------
// Helper: Convert number to words (Arabic)
// --------------------------------------------------------------
function numberToWordsAr(num) {
if (num === null || isNaN(num)) return '';
const isNegative = num < 0;
let absolute = Math.abs(num);
const integerPart = Math.floor(absolute);
const decimalPart = Math.round((absolute - integerPart) * 100);
const onesAr = ['', 'واحد', 'اثنان', 'ثلاثة', 'أربعة', 'خمسة', 'ستة', 'سبعة', 'ثمانية', 'تسعة',
'عشرة', 'أحد عشر', 'اثنا عشر', 'ثلاثة عشر', 'أربعة عشر', 'خمسة عشر',
'ستة عشر', 'سبعة عشر', 'ثمانية عشر', 'تسعة عشر'];
const tensAr = ['', '', 'عشرون', 'ثلاثون', 'أربعون', 'خمسون', 'ستون', 'سبعون', 'ثمانون', 'تسعون'];
const hundredsAr = ['', 'مائة', 'مائتان', 'ثلاثمائة', 'أربعمائة', 'خمسمائة', 'ستمائة', 'سبعمائة', 'ثمانمائة', 'تسعمائة'];
function convertHundredsAr(n) {
if (n === 0) return '';
let result = '';
if (n >= 100) {
result += hundredsAr[Math.floor(n / 100)] + ' ';
n %= 100;
}
if (n >= 20) {
result += tensAr[Math.floor(n / 10)] + ' ';
n %= 10;
}
if (n > 0 && n < 20) result += onesAr[n] + ' ';
return result.trim();
}
function convertNumberAr(n) {
if (n === 0) return 'صفر';
let words = '';
if (n >= 1000000) {
words += convertNumberAr(Math.floor(n / 1000000)) + ' مليون ';
n %= 1000000;
}
if (n >= 1000) {
words += convertNumberAr(Math.floor(n / 1000)) + ' ألف ';
n %= 1000;
}
if (n >= 100) {
words += convertHundredsAr(n);
n = 0;
} else if (n > 0) {
if (n < 20) words += onesAr[n] + ' ';
else {
words += tensAr[Math.floor(n / 10)] + ' ';
if (n % 10 > 0) words += onesAr[n % 10] + ' ';
}
}
return words.trim();
}
let result = isNegative ? 'سالب ' : '';
if (integerPart === 0 && decimalPart === 0) {
result += 'صفر';
} else {
if (integerPart > 0) result += convertNumberAr(integerPart);
if (decimalPart > 0) result += ' فاصلة ' + convertNumberAr(decimalPart);
}
result += ' ريال';
return result.trim();
}
// --------------------------------------------------------------
// Utility: Transform API data
// --------------------------------------------------------------
function transformApiData(originalData) {
const data = JSON.parse(JSON.stringify(originalData));
const standardKeys = ['InvoiceDate', 'DueDate', 'InvoiceNumber'];
const standardFields = [];
const customFields = [];
(data.fields || []).forEach(field => {
if (standardKeys.includes(field.key)) standardFields.push(field);
else customFields.push(field);
});
data.fields = standardFields;
data.custom_fields = customFields;
if (data.totals && !data.table.totals) data.table.totals = data.totals;
if (data.table.rows) data.table.rows = data.table.rows.filter(row => !row.isTotalRow);
if (!data.description && data.table.description) data.description = data.table.description;
if (data.status && !data.emphasis) {
data.emphasis = {
text: data.status.text,
positive: data.status.tone === 'positive',
negative: data.status.tone === 'negative'
};
}
if (!data.table.columns.some(col => col.sumText)) {
data.table.columns.forEach(col => { col.sumText = ''; });
}
return data;
}
// --------------------------------------------------------------
// Apply column style
// --------------------------------------------------------------
function applyColumnStyle(element, column, isLastColumn) {
element.style.textAlign = column.align || 'start';
if (column.shrinkToFit) {
element.style.whiteSpace = 'nowrap';
element.style.width = '1px';
element.style.minWidth = '';
return;
}
if (column.nowrap) {
element.style.whiteSpace = 'normal';
if (isLastColumn) {
element.style.minWidth = '60px';
element.style.width = 'auto';
} else {
element.style.width = 'auto';
element.style.minWidth = '';
}
} else {
element.style.whiteSpace = 'normal';
element.style.width = 'auto';
element.style.minWidth = '';
}
}
// --------------------------------------------------------------
// Main render function
// --------------------------------------------------------------
function renderDocument(data) {
document.documentElement.dir = data.direction || 'ltr';
const titleParts = [data?.business?.name, data?.title, data?.reference].filter(Boolean);
document.title = titleParts.join(' - ');
document.getElementById('title').innerHTML = data.title || 'No title';
document.getElementById('description').innerHTML = data.description || '';
// Business logo
const businessLogoDiv = document.getElementById('business-logo');
businessLogoDiv.innerHTML = '';
if (data.business.logo) {
const logoImg = document.createElement('img');
logoImg.src = data.business.logo;
logoImg.classList.add('business-logo-img');
businessLogoDiv.appendChild(logoImg);
}
// Business info
const business = data.business || {};
const businessInfoElem = document.getElementById('business-info');
businessInfoElem.innerHTML = `<strong>${business.name || ''}</strong><br>${business.address ? business.address.replace(/\n/g, '<br>') : ''}`;
// Recipient info
const recipient = data.recipient || {};
let recipientAddress = recipient.address || '';
recipientAddress = recipientAddress.replace(/\n/g, '<br>');
const recipientElem = document.getElementById('recipient-info');
recipientElem.innerHTML = `<strong>${recipient.name || ''}</strong><br>${recipientAddress}`;
// ----------------------------------------------------------
// Standard fields (gunakan dt/dd, tampilkan 2 baris per field)
// ----------------------------------------------------------
const fieldsContainer = document.getElementById('fields');
fieldsContainer.innerHTML = '';
const dl = document.createElement('dl');
(data.fields || []).forEach(field => {
const dt = document.createElement('dt');
dt.innerHTML = field.label;
const dd = document.createElement('dd');
dd.innerHTML = field.text;
dl.appendChild(dt);
dl.appendChild(dd);
});
fieldsContainer.appendChild(dl);
const columns = data.table.columns || [];
const lastColumnIndex = columns.length - 1;
// Table headers
const headersRow = document.getElementById('table-headers');
headersRow.innerHTML = '';
columns.forEach((column, idx) => {
const th = document.createElement('th');
th.innerHTML = column.label;
applyColumnStyle(th, column, idx === lastColumnIndex);
headersRow.appendChild(th);
});
// Table rows
const tableBody = document.getElementById('table-rows');
tableBody.innerHTML = '';
(data.table.rows || []).forEach(row => {
const tr = document.createElement('tr');
tr.className = 'row';
row.cells.forEach((cell, idx) => {
const column = columns[idx];
const td = document.createElement('td');
td.innerHTML = (cell.text || '').split('\n').join('<br/>');
applyColumnStyle(td, column, idx === lastColumnIndex);
tr.appendChild(td);
});
tableBody.appendChild(tr);
});
const allRows = tableBody.querySelectorAll('tr.row');
if (allRows.length > 0) allRows[allRows.length - 1].classList.add('last-row');
// Column totals row
const totalRow = document.createElement('tr');
totalRow.classList.add('column-total');
columns.forEach(column => {
const td = document.createElement('td');
td.style.textAlign = column.align;
if (column.label !== 'No.') td.innerHTML = column.sumText;
totalRow.appendChild(td);
});
if (totalRow.innerText.trim() !== '') tableBody.appendChild(totalRow);
// Additional totals
(data.table.totals || []).forEach(total => {
const tr = document.createElement('tr');
tr.className = 'total';
const labelCell = document.createElement('td');
labelCell.innerHTML = total.label;
labelCell.colSpan = columns.length - 1;
const valueCell = document.createElement('td');
valueCell.innerHTML = total.text;
valueCell.id = total.key;
if (total.class) valueCell.classList.add(total.class);
valueCell.dataset.value = total.number;
if (total.emphasis) {
labelCell.style.fontWeight = 'bold';
valueCell.style.fontWeight = 'bold';
}
tr.appendChild(labelCell);
tr.appendChild(valueCell);
tableBody.appendChild(tr);
});
// Amount in Words
const amountWordsDiv = document.getElementById('amount-in-words');
amountWordsDiv.innerHTML = '';
let totalAmount = null;
if (data.table.totals) {
const totalEntry = data.table.totals.find(t => t.key === 'Total' || t.label === 'الإجمالي' || t.label === 'Total');
if (totalEntry && typeof totalEntry.number === 'number') totalAmount = totalEntry.number;
}
if (totalAmount === null && data.table.totals) {
const lastEmphasized = data.table.totals.filter(t => t.emphasis).pop();
if (lastEmphasized && typeof lastEmphasized.number === 'number') totalAmount = lastEmphasized.number;
}
if (totalAmount !== null && !isNaN(totalAmount)) {
const isArabic = data.language === 'ar' || data.direction === 'rtl';
if (isArabic) {
amountWordsDiv.innerHTML = `<div><strong>المبلغ بالكلمات</strong></div><div>${numberToWordsAr(totalAmount)}</div>`;
} else {
amountWordsDiv.innerHTML = `<div><strong>Amount in Words</strong></div><div>${numberToWordsEn(totalAmount)}</div>`;
}
}
// Custom fields & QR code
const customFieldsContainer = document.getElementById('custom-fields');
const qrContainer = document.getElementById('qrcode');
customFieldsContainer.innerHTML = '';
qrContainer.innerHTML = '';
const qrKey = 'd2e9265a-460e-4a06-83f9-29a523a4d516';
(data.custom_fields || []).forEach(field => {
if (field.key === qrKey && field.image?.url) {
const div = document.createElement('div');
const label = field.label || 'QR Code';
div.innerHTML = `<strong>${label}</strong><br><img src="${field.image.url}" alt="QR Code" width="160" height="160">`;
customFieldsContainer.appendChild(div);
}
else if (field.displayAtTheTop && field.key !== qrKey) {
// Ini kemungkinan tidak terpakai karena sudah di standard fields
// Tapi kita tetap proses
const dt = document.createElement('dt');
dt.innerHTML = field.label;
const dd = document.createElement('dd');
dd.innerHTML = field.text;
fieldsContainer.appendChild(dt);
fieldsContainer.appendChild(dd);
}
else if (field.key !== qrKey) {
const div = document.createElement('div');
div.innerHTML = `<strong>${field.label || ''}</strong><br>${(field.text || '').split('\n').join('<br>')}<br><br>`;
customFieldsContainer.appendChild(div);
}
});
// Footers
const footersContainer = document.getElementById('footers');
footersContainer.innerHTML = '';
(data.footers || []).forEach(footerText => {
const div = document.createElement('div');
div.className = 'footer-text';
div.innerHTML = footerText;
footersContainer.appendChild(div);
div.querySelectorAll('script').forEach(oldScript => {
const newScript = document.createElement('script');
for (const attr of oldScript.attributes) newScript.setAttribute(attr.name, attr.value);
if (oldScript.textContent) newScript.textContent = oldScript.textContent;
oldScript.parentNode.replaceChild(newScript, oldScript);
});
});
// Status
const statusDiv = document.getElementById('status');
statusDiv.innerHTML = '';
if (data.emphasis?.text) {
const statusSpan = document.createElement('span');
statusSpan.classList.add('status-span');
if (data.emphasis.positive) statusSpan.classList.add('status-positive');
if (data.emphasis.negative) statusSpan.classList.add('status-negative');
statusSpan.innerHTML = data.emphasis.text;
statusDiv.appendChild(statusSpan);
}
}
// --------------------------------------------------------------
// Fetch data and render
// --------------------------------------------------------------
window.addEventListener('load', () => {
const apiUrl = '/api4/view-v1' + window.location.search;
fetch(apiUrl, { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' })
.then(response => response.json())
.then(data => renderDocument(transformApiData(data)))
.catch(error => {
console.error('Failed to load invoice data:', error);
document.body.innerHTML += '<div style="color:red; padding:20px;">Failed to load invoice. Please try again.</div>';
});
});
</script>
</body>
</html>
can anyone explain how this new theme enhancer works, in detail please
@LULUA_TYPING Please read this.
I tried customizing it, but I couldn’t find a way to do it successfully. I also followed the instructions provided in the tab, but they didn’t seem to work. It might be that my commands were not written correctly.
I even used ChatGPT to generate a custom theme code, but that didn’t work either. When I applied the theme, the invoice displayed the code itself instead of rendering the design properly.
Post the code you generated, use the </> button and post the code in there.
Sady this is probably beyond the ability of most users so are now left high and dry after using the original Theme Enhancer