From d47394723bc4df7fd7e0123a779837e9069dbad9 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 4 Sep 2026 13:31:13 +0200 Subject: [PATCH] refactor(pos): make buildEposCashupXml block-driven, fix broken logo image tag Also fixes a logo-ordering bug this surfaced: slip-layout.js's sale and cashup defaults put header_title before logo, which (once a real logo renders instead of the always-empty placeholder used in earlier regression tests) reversed the original image-then-title print order. Reordered both to logo-first and added the missing logo block to cashup's default catalog, since cashup's original code already attempted (brokenly) to print one. --- public/pos/cashup.html | 6 +- public/pos/epos-print.js | 137 +++++++++++++++++++++----------------- public/pos/slip-layout.js | 3 +- 3 files changed, 83 insertions(+), 63 deletions(-) diff --git a/public/pos/cashup.html b/public/pos/cashup.html index 8b0120b..3f60efe 100644 --- a/public/pos/cashup.html +++ b/public/pos/cashup.html @@ -444,7 +444,11 @@ async function printCashup(){ denoms, }; const settings = window.EposPrint.getSettings(); - const xml = window.EposPrint.buildEposCashupXml(letterhead, report); + const cashupBlocks = window.PosSlipLayout.getBlocksForPrint(letterhead.id, 'cashup'); + const cashupLogo = letterhead.logo_url + ? await window.EposPrint.imageToRaster(letterhead.logo_url, 384).catch(() => null) + : null; + const xml = window.EposPrint.buildEposCashupXml(letterhead, report, cashupBlocks, cashupLogo); if(settings.type === 'network'){ if(!settings.ip){ alert('Set the printer IP address in Printer settings first.'); diff --git a/public/pos/epos-print.js b/public/pos/epos-print.js index ef5daf0..cd686f5 100644 --- a/public/pos/epos-print.js +++ b/public/pos/epos-print.js @@ -236,69 +236,84 @@ window.EposPrint = (function () { // variance: number, // denoms: [{ label: "R200", value: 200, count: 2 }, ...], // } - // `letterhead` is the same shape as buildEposXml/Xml takes. - function buildEposCashupXml(letterhead, report) { + function cashupBlockRenderers(lh, r, logo) { + return { + header_title: function (block) { + const title = block.text || 'WFSALES POS'; + return '' + + `${xmlEscape(title)} ` + + 'CASH-UP REPORT '; + }, + logo: function () { + return logo ? `${logo.base64}` : ''; + }, + business_info: function () { + const parts = []; + if (lh.display_name) parts.push(`${xmlEscape(lh.display_name)} `); + if (lh.address) parts.push(`${xmlEscape(lh.address.split('\n')[0])} `); + if (lh.phone) parts.push(`Tel: ${xmlEscape(lh.phone)} `); + if (lh.vat_number) parts.push(`VAT: ${xmlEscape(lh.vat_number)} `); + return parts.join(''); + }, + shift_date: function () { + let out = '-------------------------------- '; + if (r.shiftLabel || r.dateLabel) out += `Shift: ${xmlEscape(r.shiftLabel || '')} Date: ${xmlEscape(r.dateLabel || '')} `; + if (r.counts) out += `Total sales: ${r.counts.cash + r.counts.card + r.counts.eft + r.counts.account} `; + return out; + }, + payment_breakdown: function () { + const lines = [ + `Cash ${pad((r.counts && r.counts.cash) || 0, 3)} sales R ${fmtMoney((r.totals && r.totals.cash) || 0)}`, + `Card ${pad((r.counts && r.counts.card) || 0, 3)} sales R ${fmtMoney((r.totals && r.totals.card) || 0)}`, + `EFT ${pad((r.counts && r.counts.eft) || 0, 3)} sales R ${fmtMoney((r.totals && r.totals.eft) || 0)}`, + `Acct ${pad((r.counts && r.counts.account) || 0, 3)} sales R ${fmtMoney((r.totals && r.totals.account) || 0)}`, + ]; + return '-------------------------------- ' + + 'PAYMENT METHOD BREAKDOWN ' + + `${lines.join(' ')} `; + }, + grand_total: function () { + return '-------------------------------- ' + + `GRAND TOTAL${' '.repeat(12)}R ${fmtMoney(r.grand || 0)} `; + }, + cash_reconciliation: function () { + return '-------------------------------- ' + + 'CASH RECONCILIATION ' + + `Cash sales: R ${fmtMoney((r.totals && r.totals.cash) || 0)} ` + + `Cash counted: R ${fmtMoney(r.cashCounted || 0)} ` + + `Variance: R ${fmtMoney(r.variance || 0)} `; + }, + denominations: function () { + if (!Array.isArray(r.denoms) || !r.denoms.length) return ''; + const dlines = r.denoms + .filter(function (d) { return (d.count || 0) > 0; }) + .map(function (d) { return `${d.label} x ${pad(d.count || 0, 3)} = R ${fmtMoney((d.value || 0) * (d.count || 0))}`; }); + if (!dlines.length) return ''; + return '-------------------------------- ' + + 'DENOMINATIONS COUNTED ' + + `${dlines.join(' ')} `; + }, + footer: function (block) { + const text = block.text || lh.footer_text; + if (!text) return ''; + return '-------------------------------- ' + + `${xmlEscape(text)} `; + }, + }; + } + + // `blocks` is the resolved block list for 'cashup'. `logo` is a + // pre-rasterized {width,height,base64} (see imageToRaster) or null — + // rasterizing is async, so the caller does it before calling this. + function buildEposCashupXml(letterhead, report, blocks, logo) { const lh = letterhead || {}; const r = report || {}; - const parts = []; - if (lh.logo_url) { - parts.push(`${(typeof window !== 'undefined' ? '' : '')}`); - } - parts.push(''); - parts.push('WFSALES POS '); - parts.push('CASH-UP REPORT '); - if (lh.display_name) { - parts.push(`${xmlEscape(lh.display_name)} `); - } - if (lh.address) { - parts.push(`${xmlEscape(lh.address.split('\n')[0])} `); - } - if (lh.phone) { - parts.push(`Tel: ${xmlEscape(lh.phone)} `); - } - if (lh.vat_number) { - parts.push(`VAT: ${xmlEscape(lh.vat_number)} `); - } - parts.push('-------------------------------- '); - if (r.shiftLabel || r.dateLabel) { - parts.push(`Shift: ${xmlEscape(r.shiftLabel || '')} Date: ${xmlEscape(r.dateLabel || '')} `); - } - if (r.counts) { - parts.push(`Total sales: ${r.counts.cash + r.counts.card + r.counts.eft + r.counts.account} `); - } - parts.push('-------------------------------- '); - parts.push('PAYMENT METHOD BREAKDOWN '); - const lines = [ - `Cash ${pad((r.counts && r.counts.cash) || 0, 3)} sales R ${fmtMoney((r.totals && r.totals.cash) || 0)}`, - `Card ${pad((r.counts && r.counts.card) || 0, 3)} sales R ${fmtMoney((r.totals && r.totals.card) || 0)}`, - `EFT ${pad((r.counts && r.counts.eft) || 0, 3)} sales R ${fmtMoney((r.totals && r.totals.eft) || 0)}`, - `Acct ${pad((r.counts && r.counts.account) || 0, 3)} sales R ${fmtMoney((r.totals && r.totals.account) || 0)}`, - ]; - parts.push(`${lines.join(' ')} `); - parts.push('-------------------------------- '); - parts.push(`GRAND TOTAL${' '.repeat(12)}R ${fmtMoney(r.grand || 0)} `); - parts.push('-------------------------------- '); - parts.push('CASH RECONCILIATION '); - parts.push(`Cash sales: R ${fmtMoney((r.totals && r.totals.cash) || 0)} `); - parts.push(`Cash counted: R ${fmtMoney(r.cashCounted || 0)} `); - parts.push(`Variance: R ${fmtMoney(r.variance || 0)} `); - if (Array.isArray(r.denoms) && r.denoms.length) { - parts.push('-------------------------------- '); - parts.push('DENOMINATIONS COUNTED '); - const dlines = r.denoms - .filter(d => (d.count || 0) > 0) - .map(d => `${d.label} x ${pad(d.count || 0, 3)} = R ${fmtMoney((d.value || 0) * (d.count || 0))}`); - if (dlines.length) { - parts.push(`${dlines.join(' ')} `); - } - } - if (lh.footer_text) { - parts.push('-------------------------------- '); - parts.push(`${xmlEscape(lh.footer_text)} `); - } - parts.push(''); - parts.push(''); - return `${parts.join('')}`; + const renderers = cashupBlockRenderers(lh, r, logo || null); + const body = blocks + .filter(function (b) { return b.enabled; }) + .map(function (b) { return renderers[b.id] ? renderers[b.id](b) : ''; }) + .join(''); + return `${body}`; } // Small helper for padded numbers in the cashup slip. diff --git a/public/pos/slip-layout.js b/public/pos/slip-layout.js index 89a0037..d362d52 100644 --- a/public/pos/slip-layout.js +++ b/public/pos/slip-layout.js @@ -44,8 +44,8 @@ window.PosSlipLayout = (function () { // consistency with the other two slip types. const DEFAULTS = { sale: [ - { id: 'header_title', enabled: true, text: null }, { id: 'logo', enabled: true }, + { id: 'header_title', enabled: true, text: null }, { id: 'business_info', enabled: true }, { id: 'date_slip', enabled: true }, { id: 'customer', enabled: true }, @@ -63,6 +63,7 @@ window.PosSlipLayout = (function () { { id: 'signature', enabled: true }, ], cashup: [ + { id: 'logo', enabled: true }, { id: 'header_title', enabled: true, text: null }, { id: 'business_info', enabled: true }, { id: 'shift_date', enabled: true },