diff --git a/public/pos/receipt.html b/public/pos/receipt.html
index 0f9a38a..317b718 100644
--- a/public/pos/receipt.html
+++ b/public/pos/receipt.html
@@ -588,11 +588,12 @@ async function printThermalBluetooth(){
status.textContent = 'Printing…';
// Account sales print a delivery note (received-by / driver lines,
// no payment line). Everything else prints the standard sale slip.
+ const letterheadIdBt = sale.letterhead && sale.letterhead.id;
const dnToggle = document.getElementById('dnShowPrices');
- const showPrices = dnToggle ? !!dnToggle.checked : true;
+ const showPrices = dnToggle ? !!dnToggle.checked : window.PosSlipLayout.getShowPricingForPrint(letterheadIdBt);
const text = sale.payment === 'account'
- ? buildEscPosDelivery(sale, showPrices)
- : buildEscPos(sale);
+ ? buildEscPosDelivery(sale, showPrices, window.PosSlipLayout.getBlocksForPrint(letterheadIdBt, 'delivery'))
+ : buildEscPos(sale, window.PosSlipLayout.getBlocksForPrint(letterheadIdBt, 'sale'));
const CHUNK = 80;
const enc = new TextEncoder();
for(let i=0;i String.fromCharCode(n);
const ESC = E(0x1B), GS = E(0x1D);
- const cmds = [];
- cmds.push(ESC+'@');
- cmds.push(ESC+'a'+E(1));
- cmds.push(ESC+'!'+E(0x30));
- cmds.push('WFSALES POS\n');
- cmds.push(ESC+'!'+E(0x00));
- if(s.letterhead?.name) cmds.push(s.letterhead.name+'\n');
- cmds.push(ESC+'a'+E(0));
- if(s.letterhead){
- if(s.letterhead.address) cmds.push(s.letterhead.address.split('\n')[0]+'\n');
- if(s.letterhead.phone) cmds.push('Tel: '+s.letterhead.phone+'\n');
- if(s.letterhead.vat_number) cmds.push('VAT: '+s.letterhead.vat_number+'\n');
- }
- cmds.push('--------------------------------\n');
- const d = new Date(s.createdAt);
- cmds.push('Date: ' + d.toLocaleString('en-ZA')+'\n');
- cmds.push('Slip: ' + (s.localId||'').slice(-8).toUpperCase()+'\n');
- if(s.customer) cmds.push('Customer: ' + s.customer.name + '\n');
- cmds.push('--------------------------------\n');
- for(const it of s.items){
- cmds.push((it.description||it.sku) + '\n');
- const line = ` ${it.qty} x ${(it.selling_price||0).toFixed(2)} ${(it.selling_price*it.qty).toFixed(2)}`;
- cmds.push(line + '\n');
- }
- cmds.push('--------------------------------\n');
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;
- cmds.push(`Subtotal (gross)${' '.repeat(10)}R ${grossSub.toFixed(2)}\n`);
- cmds.push(`VAT 15% (gross)${' '.repeat(10)}R ${grossVat.toFixed(2)}\n`);
- cmds.push(`Total (gross)${' '.repeat(13)}R ${gross.toFixed(2)}\n`);
- cmds.push(`Discount${discount.kind==='percent' ? ` (${discount.input}%)` : ''}${' '.repeat(15)}- R ${discount.amount.toFixed(2)}\n`);
- }
- cmds.push(`Subtotal${' '.repeat(22)}R ${sub.toFixed(2)}\n`);
- cmds.push(`VAT 15%${' '.repeat(22)}R ${vat.toFixed(2)}\n`);
- cmds.push(ESC+'!'+E(0x08));
- cmds.push(`TOTAL${' '.repeat(28)}R ${s.total.toFixed(2)}\n`);
- cmds.push(ESC+'!'+E(0x00));
- cmds.push('Paid: ' + (s.payment==='cash' ? `CASH R ${(s.cashGiven||0).toFixed(2)}` : s.payment.toUpperCase()) + '\n');
- if(s.payment==='cash') cmds.push(`Change${' '.repeat(24)}R ${(s.change||0).toFixed(2)}\n`);
- cmds.push('--------------------------------\n');
- if(s.letterhead?.footer_text) cmds.push(s.letterhead.footer_text+'\n');
- cmds.push('\n\n');
- cmds.push(GS+'V'+E(0x41)+E(0x10));
- const text = cmds.join('');
- const bytes = new Uint8Array(text.length);
- for(let i=0;i ESC+'a'+E(1) + ESC+'!'+E(0x30) + (block.text||'WFSALES POS') + '\n' + ESC+'!'+E(0x00),
+ // Web Bluetooth GATT printing has no raster-image support in this
+ // app — this block intentionally prints nothing on this transport.
+ logo: () => '',
+ business_info: () => {
+ let out = ESC+'a'+E(0);
+ if(s.letterhead?.name) out += s.letterhead.name+'\n';
+ if(s.letterhead){
+ if(s.letterhead.address) out += s.letterhead.address.split('\n')[0]+'\n';
+ if(s.letterhead.phone) out += 'Tel: '+s.letterhead.phone+'\n';
+ if(s.letterhead.vat_number) out += 'VAT: '+s.letterhead.vat_number+'\n';
+ }
+ return out;
+ },
+ date_slip: () => {
+ const d = new Date(s.createdAt);
+ return '--------------------------------\n'
+ + 'Date: ' + d.toLocaleString('en-ZA')+'\n'
+ + 'Slip: ' + (s.localId||'').slice(-8).toUpperCase()+'\n';
+ },
+ customer: () => s.customer ? ('Customer: ' + s.customer.name + '\n') : '',
+ items: () => {
+ let out = '--------------------------------\n';
+ for(const it of s.items){
+ out += (it.description||it.sku) + '\n';
+ out += ` ${it.qty} x ${(it.selling_price||0).toFixed(2)} ${(it.selling_price*it.qty).toFixed(2)}\n`;
+ }
+ return out;
+ },
+ totals: () => {
+ let out = '--------------------------------\n';
+ 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;
+ out += `Subtotal (gross)${' '.repeat(10)}R ${grossSub.toFixed(2)}\n`;
+ out += `VAT 15% (gross)${' '.repeat(10)}R ${grossVat.toFixed(2)}\n`;
+ out += `Total (gross)${' '.repeat(13)}R ${gross.toFixed(2)}\n`;
+ out += `Discount${discount.kind==='percent' ? ` (${discount.input}%)` : ''}${' '.repeat(15)}- R ${discount.amount.toFixed(2)}\n`;
+ }
+ out += `Subtotal${' '.repeat(22)}R ${sub.toFixed(2)}\n`;
+ out += `VAT 15%${' '.repeat(22)}R ${vat.toFixed(2)}\n`;
+ out += ESC+'!'+E(0x08) + `TOTAL${' '.repeat(28)}R ${s.total.toFixed(2)}\n` + ESC+'!'+E(0x00);
+ return out;
+ },
+ payment: () => {
+ let out = 'Paid: ' + (s.payment==='cash' ? `CASH R ${(s.cashGiven||0).toFixed(2)}` : s.payment.toUpperCase()) + '\n';
+ if(s.payment==='cash') out += `Change${' '.repeat(24)}R ${(s.change||0).toFixed(2)}\n`;
+ return out;
+ },
+ footer: (block) => {
+ const text = block.text || s.letterhead?.footer_text;
+ let out = '--------------------------------\n';
+ if(text) out += text+'\n';
+ return out;
+ },
+ };
+}
+
+// `blocks` is the resolved block list for 'sale'. Returns a plain
+// string (matching buildEscPosDelivery) — the caller runs it through
+// TextEncoder before writing to the Bluetooth characteristic. (Note:
+// this used to manually pack a Uint8Array here, which the caller then
+// fed into TextEncoder.encode() — encoding a Uint8Array as if it were
+// a string, producing garbled output. Fixed as part of this refactor.)
+function buildEscPos(s, blocks){
+ const E = (n) => String.fromCharCode(n);
+ const ESC = E(0x1B), GS = E(0x1D);
+ const renderers = saleEscPosRenderers(s);
+ const body = blocks.filter(b=>b.enabled).map(b => renderers[b.id] ? renderers[b.id](b) : '').join('');
+ return ESC+'@' + body + '\n\n' + GS+'V'+E(0x41)+E(0x10);
}
async function printDelivery(){
@@ -706,9 +730,10 @@ async function printDelivery(){
btn.disabled = false;
return;
}
+ const letterheadIdBtDn = sale.letterhead && sale.letterhead.id;
const dnToggle = document.getElementById('dnShowPrices');
- const showPrices = dnToggle ? !!dnToggle.checked : true;
- const text = buildEscPosDelivery(sale, showPrices);
+ const showPrices = dnToggle ? !!dnToggle.checked : window.PosSlipLayout.getShowPricingForPrint(letterheadIdBtDn);
+ const text = buildEscPosDelivery(sale, showPrices, window.PosSlipLayout.getBlocksForPrint(letterheadIdBtDn, 'delivery'));
const bytes = new Uint8Array(text.length);
for(let i=0;i {
+ const E = (n) => String.fromCharCode(n);
+ const ESC = E(0x1B);
+ const title = block.text || 'WFSALES POS';
+ let out = ESC+'a'+E(1) + ESC+'!'+E(0x10) + title + '\n' + ESC+'!'+E(0x00) + 'Delivery Note\n' + ESC+'a'+E(0);
+ if(s.letterhead && s.letterhead.name) out += s.letterhead.name + '\n';
+ if(s.letterhead){
+ if(s.letterhead.address) out += s.letterhead.address.split('\n')[0]+'\n';
+ if(s.letterhead.phone) out += 'Tel: '+s.letterhead.phone+'\n';
+ if(s.letterhead.vat_number) out += 'VAT: '+s.letterhead.vat_number+'\n';
+ }
+ return out;
+ },
+ customer: () => {
+ let out = '';
+ if(s.customer && s.customer.company) out += 'Company: ' + s.customer.company + '\n';
+ out += 'Customer: ' + (s.customer && s.customer.name || '—') + '\n';
+ return out;
+ },
+ date: () => {
+ const d = new Date(s.createdAt);
+ return 'Date: ' + d.toLocaleDateString('en-ZA')+'\n';
+ },
+ items: () => {
+ let out = '--------------------------------\n';
+ for(const it of s.items){
+ out += (it.description||it.sku)+'\n';
+ const qtyLine = ` ${it.qty} ${it.unit||'ea'}`;
+ out += showPrices ? `${qtyLine} ${fmt((it.selling_price||0)*it.qty)}\n` : `${qtyLine}\n`;
+ }
+ return out;
+ },
+ totals: () => {
+ let out = '--------------------------------\n';
+ if(showPrices) out += 'TOTAL: ' + fmt(s.total) + '\n';
+ return out;
+ },
+ signature: () => 'Received by: __________ Driver: __________\n',
+ };
+}
+
+// `blocks` is the resolved block list for 'delivery'. Returns a
+// plain string (see buildEscPos's comment on why — TextEncoder needs
+// a string, not a Uint8Array).
+function buildEscPosDelivery(s, showPrices, blocks){
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('');
+ const renderers = deliveryEscPosRenderers(s, showPrices);
+ const body = blocks.filter(b=>b.enabled).map(b => renderers[b.id] ? renderers[b.id](b) : '').join('');
+ return ESC+'@' + body + '\n\n' + E(0x1D)+'V'+E(0x41)+E(0x10);
}
function togglePrinterSettings(){