From 5c9f30dbd1cbf35e5d6fdb9cddc684cc6bfc8289 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 4 Sep 2026 10:40:48 +0200 Subject: [PATCH] feat(pos): 'Print slip' on account sales auto-routes to delivery note (network + BT) --- public/pos/receipt.html | 113 ++++++++++++++++++++++++++-------------- public/pos/sw.js | 2 +- 2 files changed, 76 insertions(+), 39 deletions(-) diff --git a/public/pos/receipt.html b/public/pos/receipt.html index 08b29ce..849ba99 100644 --- a/public/pos/receipt.html +++ b/public/pos/receipt.html @@ -525,10 +525,26 @@ async function printThermalNetwork(settings){ } status.textContent = 'Sending to ' + settings.ip + '…'; try{ - const logo = await window.EposPrint.imageToRaster('/wfdash-logo.png', 384).catch(()=>null); - const xml = window.EposPrint.buildEposXml(sale, logo); + // Account sales print a delivery note (no payment line, has + // received-by / driver lines) — that's the document the driver + // hands to the customer at drop-off, not a "paid" receipt. Cash + // / card / EFT get the standard sale slip. + let xml, successLabel; + if (sale.payment === 'account') { + // Logos don't always render cleanly on the TM-T20II at small + // sizes; the delivery note skips the logo so the address + items + // get the most space. If you want the logo back, set `logo` + // here and pass it to buildEposDeliveryXml the same way + // buildEposXml takes it. + xml = window.EposPrint.buildEposDeliveryXml(sale); + successLabel = 'Delivery note printed · '; + } else { + const logo = await window.EposPrint.imageToRaster('/wfdash-logo.png', 384).catch(()=>null); + xml = window.EposPrint.buildEposXml(sale, logo); + successLabel = 'Printed · '; + } await window.EposPrint.printViaNetwork(settings.ip, xml); - status.textContent = 'Printed · ' + settings.ip; + status.textContent = successLabel + settings.ip; }catch(e){ status.textContent = 'Print failed: ' + e.message; errBox.classList.add('err'); @@ -568,14 +584,21 @@ async function printThermalBluetooth(){ } if(!btChar) throw new Error('No writable characteristic on printer.'); status.textContent = 'Printing…'; - const payload = buildEscPos(sale); + // Account sales print a delivery note (received-by / driver lines, + // no payment line). Everything else prints the standard sale slip. + const dnToggle = document.getElementById('dnShowPrices'); + const showPrices = dnToggle ? !!dnToggle.checked : true; + const text = sale.payment === 'account' + ? buildEscPosDelivery(sale, showPrices) + : buildEscPos(sale); const CHUNK = 80; - for(let i=0;isetTimeout(r,30)); } - status.textContent = 'Printed · ' + (btDevice.name||'printer'); + status.textContent = (sale.payment === 'account' ? 'Delivery note printed · ' : 'Printed · ') + (btDevice.name||'printer'); }catch(e){ if(e.name==='NotFoundError') status.textContent = 'Cancelled.'; else status.textContent = 'Print failed: ' + e.message; @@ -679,38 +702,9 @@ async function printDelivery(){ btn.disabled = false; return; } - const E = (n) => String.fromCharCode(n); - const ESC = E(0x1B); - // Mirror the on-screen "Include prices" toggle so the printed slip - // matches what the operator just checked. const dnToggle = document.getElementById('dnShowPrices'); const showPrices = dnToggle ? !!dnToggle.checked : true; - const lines = []; - lines.push(ESC+'@'); - lines.push(ESC+'a'+E(1)); - lines.push(ESC+'!'+E(0x10)); - lines.push('WFSALES POS\n'); - lines.push(ESC+'!'+E(0x00)); - lines.push('Delivery Note\n'); - lines.push(ESC+'a'+E(0)); - if(sale.customer?.company) lines.push('Company: ' + sale.customer.company + '\n'); - lines.push('Customer: ' + (sale.customer?.name||'—') + '\n'); - const d = new Date(sale.createdAt); - lines.push('Date: ' + d.toLocaleDateString('en-ZA')+'\n'); - lines.push('--------------------------------\n'); - for(const it of sale.items){ - lines.push((it.description||it.sku)+'\n'); - const qtyLine = ` ${it.qty} ${it.unit||'ea'}`; - lines.push(showPrices - ? `${qtyLine} ${fmt((it.selling_price||0)*it.qty)}\n` - : `${qtyLine}\n`); - } - lines.push('--------------------------------\n'); - if(showPrices) lines.push('TOTAL: ' + fmt(sale.total) + '\n'); - lines.push('Received by: __________ Driver: __________\n'); - lines.push('\n\n'); - lines.push(String.fromCharCode(0x1D)+'V'+String.fromCharCode(0x41)+String.fromCharCode(0x10)); - const text = lines.join(''); + const text = buildEscPosDelivery(sale, showPrices); const bytes = new Uint8Array(text.length); for(let i=0;i String.fromCharCode(n); + const ESC = E(0x1B); + const lines = []; + lines.push(ESC+'@'); + lines.push(ESC+'a'+E(1)); + lines.push(ESC+'!'+E(0x10)); + lines.push('WFSALES POS\n'); + lines.push(ESC+'!'+E(0x00)); + lines.push('Delivery Note\n'); + lines.push(ESC+'a'+E(0)); + if(s.letterhead && s.letterhead.name) lines.push(s.letterhead.name + '\n'); + if(s.letterhead){ + if(s.letterhead.address) lines.push(s.letterhead.address.split('\n')[0]+'\n'); + if(s.letterhead.phone) lines.push('Tel: '+s.letterhead.phone+'\n'); + if(s.letterhead.vat_number) lines.push('VAT: '+s.letterhead.vat_number+'\n'); + } + if(s.customer && s.customer.company) lines.push('Company: ' + s.customer.company + '\n'); + lines.push('Customer: ' + (s.customer && s.customer.name || '—') + '\n'); + const d = new Date(s.createdAt); + lines.push('Date: ' + d.toLocaleDateString('en-ZA')+'\n'); + lines.push('--------------------------------\n'); + for(const it of s.items){ + lines.push((it.description||it.sku)+'\n'); + const qtyLine = ` ${it.qty} ${it.unit||'ea'}`; + lines.push(showPrices + ? `${qtyLine} ${fmt((it.selling_price||0)*it.qty)}\n` + : `${qtyLine}\n`); + } + lines.push('--------------------------------\n'); + if(showPrices) lines.push('TOTAL: ' + fmt(s.total) + '\n'); + lines.push('Received by: __________ Driver: __________\n'); + lines.push('\n\n'); + lines.push(E(0x1D)+'V'+E(0x41)+E(0x10)); + return lines.join(''); +} + function togglePrinterSettings(){ const form = document.getElementById('printerSettingsForm'); form.style.display = form.style.display === 'none' ? 'flex' : 'none'; diff --git a/public/pos/sw.js b/public/pos/sw.js index 618009e..8249748 100644 --- a/public/pos/sw.js +++ b/public/pos/sw.js @@ -1,5 +1,5 @@ // Service worker — cache app shell, network-first for HTML + theme.js, cache-first for other static. -const CACHE = "pos-v19"; +const CACHE = "pos-v20"; const APP_SHELL = [ './', './index.html',