From 93eeea15e8cba7f3569daf6758277baadb4a0bf0 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 3 Sep 2026 17:53:23 +0200 Subject: [PATCH] feat(pos): add ePOS-Print XML builder for sale receipts --- public/pos/epos-print.js | 54 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/public/pos/epos-print.js b/public/pos/epos-print.js index 93ffb2b..0f0e646 100644 --- a/public/pos/epos-print.js +++ b/public/pos/epos-print.js @@ -68,5 +68,57 @@ window.EposPrint = (function () { }); } - return { getSettings, setSettings, imageToRaster }; + function xmlEscape(s) { + return String(s == null ? '' : s).replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c])); + } + + // Mirrors buildEscPos() in receipt.html — same fields, same discount + // math — but emits ePOS-Print XML tags instead of raw ESC/POS bytes. + function buildEposXml(s, logo) { + const parts = []; + if (logo) parts.push(`${logo.base64}`); + parts.push(''); + parts.push('WFSALES POS '); + if (s.letterhead && s.letterhead.name) { + parts.push(`${xmlEscape(s.letterhead.name)} `); + } + const addrLines = []; + if (s.letterhead && s.letterhead.address) addrLines.push(xmlEscape(s.letterhead.address.split('\n')[0])); + if (s.letterhead && s.letterhead.phone) addrLines.push('Tel: ' + xmlEscape(s.letterhead.phone)); + if (s.letterhead && s.letterhead.vat_number) addrLines.push('VAT: ' + xmlEscape(s.letterhead.vat_number)); + if (addrLines.length) parts.push(`${addrLines.join(' ')} `); + parts.push('-------------------------------- '); + const d = new Date(s.createdAt); + parts.push(`Date: ${xmlEscape(d.toLocaleString('en-ZA'))} Slip: ${xmlEscape((s.localId || '').slice(-8).toUpperCase())} `); + if (s.customer) parts.push(`Customer: ${xmlEscape(s.customer.name)} `); + parts.push('-------------------------------- '); + const itemLines = s.items.map(it => { + const priceLine = ` ${it.qty} x ${(it.selling_price || 0).toFixed(2)} ${(it.selling_price * it.qty).toFixed(2)}`; + return `${xmlEscape(it.description || it.sku)} ${xmlEscape(priceLine)} `; + }).join(''); + parts.push(`${itemLines}`); + parts.push('-------------------------------- '); + const sub = s.sub != null ? s.sub : (s.total - (s.vat || 0)); + const vat = s.vat != null ? s.vat : (s.total * 15 / 115); + const discount = s.discount || { kind: 'none', input: 0, amount: 0 }; + const hasDiscount = discount.kind !== 'none' && Number(discount.amount) > 0; + if (hasDiscount) { + const gross = Number(s.gross) || s.items.reduce((sum, it) => sum + (Number(it.selling_price) || 0) * (Number(it.qty) || 0), 0); + const grossSub = gross / 1.15; + const grossVat = gross - grossSub; + parts.push(`Subtotal (gross)${' '.repeat(10)}R ${grossSub.toFixed(2)} VAT 15% (gross)${' '.repeat(10)}R ${grossVat.toFixed(2)} Total (gross)${' '.repeat(13)}R ${gross.toFixed(2)} Discount${discount.kind === 'percent' ? ` (${discount.input}%)` : ''}${' '.repeat(15)}- R ${discount.amount.toFixed(2)} `); + } + parts.push(`Subtotal${' '.repeat(22)}R ${sub.toFixed(2)} VAT 15%${' '.repeat(22)}R ${vat.toFixed(2)} `); + parts.push(`TOTAL${' '.repeat(28)}R ${s.total.toFixed(2)} `); + const paidLabel = s.payment === 'cash' ? `CASH R ${(s.cashGiven || 0).toFixed(2)}` : s.payment.toUpperCase(); + parts.push(`Paid: ${xmlEscape(paidLabel)} `); + if (s.payment === 'cash') parts.push(`Change${' '.repeat(24)}R ${(s.change || 0).toFixed(2)} `); + parts.push('-------------------------------- '); + if (s.letterhead && s.letterhead.footer_text) parts.push(`${xmlEscape(s.letterhead.footer_text)} `); + parts.push(''); + parts.push(''); + return `${parts.join('')}`; + } + + return { getSettings, setSettings, imageToRaster, buildEposXml }; })();