feat(pos): 'Print slip' on account sales auto-routes to delivery note (network + BT)
This commit is contained in:
parent
79cc7d1f0a
commit
5c9f30dbd1
2 changed files with 76 additions and 39 deletions
|
|
@ -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;i<payload.length;i+=CHUNK){
|
||||
const slice = payload.slice(i, i+CHUNK);
|
||||
await btChar.writeValueWithoutResponse(slice);
|
||||
const enc = new TextEncoder();
|
||||
for(let i=0;i<text.length;i+=CHUNK){
|
||||
const slice = text.slice(i, i+CHUNK);
|
||||
await btChar.writeValueWithoutResponse(enc.encode(slice));
|
||||
await new Promise(r=>setTimeout(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<text.length;i++) bytes[i] = text.charCodeAt(i) & 0xff;
|
||||
try{
|
||||
|
|
@ -727,6 +721,49 @@ async function printDelivery(){
|
|||
}
|
||||
}
|
||||
|
||||
// Builds the ESC/POS byte stream for a delivery note. Used by
|
||||
// `printDelivery` (the explicit button) and by `printThermalBluetooth`
|
||||
// when the till prints an account sale via the "Print slip" button.
|
||||
// Mirrors the ePOS-Print XML in `EposPrint.buildEposDeliveryXml` —
|
||||
// when one changes, change the other so the on-paper output stays
|
||||
// in sync.
|
||||
function buildEscPosDelivery(s, showPrices){
|
||||
const E = (n) => 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';
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue