feat(pos): add ePOS-Print XML builder for sale receipts

This commit is contained in:
Dave 2026-09-03 17:53:23 +02:00
parent 4f54051b66
commit 93eeea15e8

View file

@ -68,5 +68,57 @@ window.EposPrint = (function () {
});
}
return { getSettings, setSettings, imageToRaster };
function xmlEscape(s) {
return String(s == null ? '' : s).replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[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(`<image width="${logo.width}" height="${logo.height}" color="color_1" mode="mono" align="center">${logo.base64}</image>`);
parts.push('<feed line="1"/>');
parts.push('<text align="center" width="2" height="2">WFSALES POS&#10;</text>');
if (s.letterhead && s.letterhead.name) {
parts.push(`<text align="center">${xmlEscape(s.letterhead.name)}&#10;</text>`);
}
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(`<text align="center">${addrLines.join('&#10;')}&#10;</text>`);
parts.push('<text align="left">--------------------------------&#10;</text>');
const d = new Date(s.createdAt);
parts.push(`<text align="left">Date: ${xmlEscape(d.toLocaleString('en-ZA'))}&#10;Slip: ${xmlEscape((s.localId || '').slice(-8).toUpperCase())}&#10;</text>`);
if (s.customer) parts.push(`<text align="left">Customer: ${xmlEscape(s.customer.name)}&#10;</text>`);
parts.push('<text align="left">--------------------------------&#10;</text>');
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)}&#10;${xmlEscape(priceLine)}&#10;`;
}).join('');
parts.push(`<text align="left">${itemLines}</text>`);
parts.push('<text align="left">--------------------------------&#10;</text>');
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(`<text align="left">Subtotal (gross)${' '.repeat(10)}R ${grossSub.toFixed(2)}&#10;VAT 15% (gross)${' '.repeat(10)}R ${grossVat.toFixed(2)}&#10;Total (gross)${' '.repeat(13)}R ${gross.toFixed(2)}&#10;Discount${discount.kind === 'percent' ? ` (${discount.input}%)` : ''}${' '.repeat(15)}- R ${discount.amount.toFixed(2)}&#10;</text>`);
}
parts.push(`<text align="left">Subtotal${' '.repeat(22)}R ${sub.toFixed(2)}&#10;VAT 15%${' '.repeat(22)}R ${vat.toFixed(2)}&#10;</text>`);
parts.push(`<text align="left" em="true">TOTAL${' '.repeat(28)}R ${s.total.toFixed(2)}&#10;</text>`);
const paidLabel = s.payment === 'cash' ? `CASH R ${(s.cashGiven || 0).toFixed(2)}` : s.payment.toUpperCase();
parts.push(`<text align="left">Paid: ${xmlEscape(paidLabel)}&#10;</text>`);
if (s.payment === 'cash') parts.push(`<text align="left">Change${' '.repeat(24)}R ${(s.change || 0).toFixed(2)}&#10;</text>`);
parts.push('<text align="left">--------------------------------&#10;</text>');
if (s.letterhead && s.letterhead.footer_text) parts.push(`<text align="center">${xmlEscape(s.letterhead.footer_text)}&#10;</text>`);
parts.push('<feed line="3"/>');
parts.push('<cut type="feed"/>');
return `<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><epos-print xmlns="http://www.epson-pos.com/schemas/2011/03/epos-print">${parts.join('')}</epos-print></s:Body></s:Envelope>`;
}
return { getSettings, setSettings, imageToRaster, buildEposXml };
})();