fix(pos): reset stateful printer attributes per-block instead of relying on order
Root cause of the oversized/wrapping text on the printed slip: ePOS- Print XML's <text> attributes (width, height, em) are stateful on the printer — they persist across subsequent <text> tags until explicitly changed back, not per-tag like CSS. The sale slip's header_title block sets width="2" height="2" for the big title and nothing after it ever reset back to width="1" height="1", so the entire rest of the receipt (business info, items, totals, payment, footer) inherited double-size text — exactly the wrapping/misalignment seen in the photo. Same issue existed in the delivery note (header's height="2" leaking into every line after it) and cash-up (GRAND TOTAL's em="true" leaking into CASH RECONCILIATION's non-bold lines). Now every <text> tag in all three XML builders explicitly declares width/height/em rather than relying on inheritance from whatever tag ran before it. This isn't just a display fix — since blocks are now user-reorderable/toggleable, no block can safely assume what state a neighboring block left behind; every block must be correct in isolation regardless of order. Same class of bug existed on the Bluetooth ESC/POS side: ESC/POS's alignment command is equally persistent, and the sale slip's header_title centered the title but relied on the next block (business_info) to reset it back to left — broken the moment business_info is disabled or reordered. Fixed the same way: each block resets what it changed before returning.
This commit is contained in:
parent
ec711760a3
commit
fc361edfc8
2 changed files with 59 additions and 45 deletions
|
|
@ -78,6 +78,16 @@ window.EposPrint = (function () {
|
|||
// read their custom text. Dividers are owned by whichever block
|
||||
// came after them in the original fixed sequence — reordering a
|
||||
// block takes its leading divider with it.
|
||||
// ePOS-Print's <text> style attributes (width, height, em, ul,
|
||||
// reverse) are STATEFUL — the printer keeps whatever a previous
|
||||
// <text> tag set until a later tag explicitly changes it back; they
|
||||
// don't reset per-tag the way a browser resets inline styles. Since
|
||||
// blocks are user-reorderable/toggleable, no block can assume what
|
||||
// state a neighboring block left behind — every plain-style tag
|
||||
// here explicitly declares width="1" height="1" em="false" so it's
|
||||
// correct regardless of what printed immediately before it.
|
||||
const NORMAL = 'width="1" height="1" em="false"';
|
||||
|
||||
function saleBlockRenderers(s, logo) {
|
||||
const sub = s.sub != null ? s.sub : (s.total - (s.vat || 0));
|
||||
const vat = s.vat != null ? s.vat : (s.total * 15 / 115);
|
||||
|
|
@ -93,51 +103,51 @@ window.EposPrint = (function () {
|
|||
},
|
||||
business_info: function () {
|
||||
const parts = [];
|
||||
if (s.letterhead && s.letterhead.name) parts.push(`<text align="center">${xmlEscape(s.letterhead.name)} </text>`);
|
||||
if (s.letterhead && s.letterhead.name) parts.push(`<text align="center" ${NORMAL}>${xmlEscape(s.letterhead.name)} </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(' ')} </text>`);
|
||||
if (addrLines.length) parts.push(`<text align="center" ${NORMAL}>${addrLines.join(' ')} </text>`);
|
||||
return parts.join('');
|
||||
},
|
||||
date_slip: function () {
|
||||
const d = new Date(s.createdAt);
|
||||
return '<text align="left">-------------------------------- </text>'
|
||||
+ `<text align="left">Date: ${xmlEscape(d.toLocaleString('en-ZA'))} Slip: ${xmlEscape((s.localId || '').slice(-8).toUpperCase())} </text>`;
|
||||
return `<text align="left" ${NORMAL}>-------------------------------- </text>`
|
||||
+ `<text align="left" ${NORMAL}>Date: ${xmlEscape(d.toLocaleString('en-ZA'))} Slip: ${xmlEscape((s.localId || '').slice(-8).toUpperCase())} </text>`;
|
||||
},
|
||||
customer: function () {
|
||||
return s.customer ? `<text align="left">Customer: ${xmlEscape(s.customer.name)} </text>` : '';
|
||||
return s.customer ? `<text align="left" ${NORMAL}>Customer: ${xmlEscape(s.customer.name)} </text>` : '';
|
||||
},
|
||||
items: function () {
|
||||
const itemLines = s.items.map(function (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)} ${xmlEscape(priceLine)} `;
|
||||
}).join('');
|
||||
return `<text align="left">-------------------------------- ${itemLines}</text>`;
|
||||
return `<text align="left" ${NORMAL}>-------------------------------- ${itemLines}</text>`;
|
||||
},
|
||||
totals: function () {
|
||||
const parts = ['<text align="left">-------------------------------- </text>'];
|
||||
const parts = [`<text align="left" ${NORMAL}>-------------------------------- </text>`];
|
||||
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)} VAT 15% (gross)${' '.repeat(10)}R ${grossVat.toFixed(2)} Total (gross)${' '.repeat(13)}R ${gross.toFixed(2)} Discount${discount.kind === 'percent' ? ` (${discount.input}%)` : ''}${' '.repeat(15)}- R ${discount.amount.toFixed(2)} </text>`);
|
||||
parts.push(`<text align="left" ${NORMAL}>Subtotal (gross)${' '.repeat(10)}R ${grossSub.toFixed(2)} VAT 15% (gross)${' '.repeat(10)}R ${grossVat.toFixed(2)} Total (gross)${' '.repeat(13)}R ${gross.toFixed(2)} Discount${discount.kind === 'percent' ? ` (${discount.input}%)` : ''}${' '.repeat(15)}- R ${discount.amount.toFixed(2)} </text>`);
|
||||
}
|
||||
parts.push(`<text align="left">Subtotal${' '.repeat(22)}R ${sub.toFixed(2)} VAT 15%${' '.repeat(22)}R ${vat.toFixed(2)} </text>`);
|
||||
parts.push(`<text align="left" em="true">TOTAL${' '.repeat(28)}R ${s.total.toFixed(2)} </text>`);
|
||||
parts.push(`<text align="left" width="1" height="1">Subtotal${' '.repeat(22)}R ${sub.toFixed(2)} VAT 15%${' '.repeat(22)}R ${vat.toFixed(2)} </text>`);
|
||||
parts.push(`<text align="left" width="1" height="1" em="true">TOTAL${' '.repeat(28)}R ${s.total.toFixed(2)} </text>`);
|
||||
return parts.join('');
|
||||
},
|
||||
payment: function () {
|
||||
const paidLabel = s.payment === 'cash' ? `CASH R ${(s.cashGiven || 0).toFixed(2)}` : s.payment.toUpperCase();
|
||||
let out = `<text align="left">Paid: ${xmlEscape(paidLabel)} </text>`;
|
||||
if (s.payment === 'cash') out += `<text align="left">Change${' '.repeat(24)}R ${(s.change || 0).toFixed(2)} </text>`;
|
||||
let out = `<text align="left" ${NORMAL}>Paid: ${xmlEscape(paidLabel)} </text>`;
|
||||
if (s.payment === 'cash') out += `<text align="left" ${NORMAL}>Change${' '.repeat(24)}R ${(s.change || 0).toFixed(2)} </text>`;
|
||||
return out;
|
||||
},
|
||||
footer: function (block) {
|
||||
const text = block.text || (s.letterhead && s.letterhead.footer_text);
|
||||
let out = '<text align="left">-------------------------------- </text>';
|
||||
if (text) out += `<text align="center">${xmlEscape(text)} </text>`;
|
||||
let out = `<text align="left" ${NORMAL}>-------------------------------- </text>`;
|
||||
if (text) out += `<text align="center" ${NORMAL}>${xmlEscape(text)} </text>`;
|
||||
return out;
|
||||
},
|
||||
};
|
||||
|
|
@ -176,17 +186,17 @@ window.EposPrint = (function () {
|
|||
return {
|
||||
header_title: function (block) {
|
||||
const title = block.text || 'WFSALES POS';
|
||||
return `<text align="center" width="1" height="2">${xmlEscape(title)} </text><text align="center">Delivery Note </text>`;
|
||||
return `<text align="center" width="1" height="2">${xmlEscape(title)} </text><text align="center" ${NORMAL}>Delivery Note </text>`;
|
||||
},
|
||||
customer: function () {
|
||||
let out = '';
|
||||
if (s.customer && s.customer.company) out += `<text align="left">Company: ${xmlEscape(s.customer.company)} </text>`;
|
||||
out += `<text align="left">Customer: ${xmlEscape((s.customer && s.customer.name) || '—')} </text>`;
|
||||
if (s.customer && s.customer.company) out += `<text align="left" ${NORMAL}>Company: ${xmlEscape(s.customer.company)} </text>`;
|
||||
out += `<text align="left" ${NORMAL}>Customer: ${xmlEscape((s.customer && s.customer.name) || '—')} </text>`;
|
||||
return out;
|
||||
},
|
||||
date: function () {
|
||||
const d = new Date(s.createdAt);
|
||||
return `<text align="left">Date: ${xmlEscape(d.toLocaleDateString('en-ZA'))} </text>`;
|
||||
return `<text align="left" ${NORMAL}>Date: ${xmlEscape(d.toLocaleDateString('en-ZA'))} </text>`;
|
||||
},
|
||||
items: function () {
|
||||
const itemLines = s.items.map(function (it) {
|
||||
|
|
@ -194,15 +204,15 @@ window.EposPrint = (function () {
|
|||
const priceSuffix = showPricing ? ` R ${((it.selling_price || 0) * it.qty).toFixed(2)}` : '';
|
||||
return `${xmlEscape(it.description || it.sku)} ${xmlEscape(qtyLine + priceSuffix)} `;
|
||||
}).join('');
|
||||
return `<text align="left">-------------------------------- ${itemLines}</text>`;
|
||||
return `<text align="left" ${NORMAL}>-------------------------------- ${itemLines}</text>`;
|
||||
},
|
||||
totals: function () {
|
||||
let out = '<text align="left">-------------------------------- </text>';
|
||||
if (showPricing) out += `<text align="left">TOTAL: R ${s.total.toFixed(2)} </text>`;
|
||||
let out = `<text align="left" ${NORMAL}>-------------------------------- </text>`;
|
||||
if (showPricing) out += `<text align="left" ${NORMAL}>TOTAL: R ${s.total.toFixed(2)} </text>`;
|
||||
return out;
|
||||
},
|
||||
signature: function () {
|
||||
return '<text align="left">Received by: __________ Driver: __________ </text>';
|
||||
return `<text align="left" ${NORMAL}>Received by: __________ Driver: __________ </text>`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -249,16 +259,16 @@ window.EposPrint = (function () {
|
|||
},
|
||||
business_info: function () {
|
||||
const parts = [];
|
||||
if (lh.display_name) parts.push(`<text align="center">${xmlEscape(lh.display_name)} </text>`);
|
||||
if (lh.address) parts.push(`<text align="center">${xmlEscape(lh.address.split('\n')[0])} </text>`);
|
||||
if (lh.phone) parts.push(`<text align="center">Tel: ${xmlEscape(lh.phone)} </text>`);
|
||||
if (lh.vat_number) parts.push(`<text align="center">VAT: ${xmlEscape(lh.vat_number)} </text>`);
|
||||
if (lh.display_name) parts.push(`<text align="center" ${NORMAL}>${xmlEscape(lh.display_name)} </text>`);
|
||||
if (lh.address) parts.push(`<text align="center" ${NORMAL}>${xmlEscape(lh.address.split('\n')[0])} </text>`);
|
||||
if (lh.phone) parts.push(`<text align="center" ${NORMAL}>Tel: ${xmlEscape(lh.phone)} </text>`);
|
||||
if (lh.vat_number) parts.push(`<text align="center" ${NORMAL}>VAT: ${xmlEscape(lh.vat_number)} </text>`);
|
||||
return parts.join('');
|
||||
},
|
||||
shift_date: function () {
|
||||
let out = '<text align="left">-------------------------------- </text>';
|
||||
if (r.shiftLabel || r.dateLabel) out += `<text align="left">Shift: ${xmlEscape(r.shiftLabel || '')} Date: ${xmlEscape(r.dateLabel || '')} </text>`;
|
||||
if (r.counts) out += `<text align="left">Total sales: ${r.counts.cash + r.counts.card + r.counts.eft + r.counts.account} </text>`;
|
||||
let out = `<text align="left" ${NORMAL}>-------------------------------- </text>`;
|
||||
if (r.shiftLabel || r.dateLabel) out += `<text align="left" ${NORMAL}>Shift: ${xmlEscape(r.shiftLabel || '')} Date: ${xmlEscape(r.dateLabel || '')} </text>`;
|
||||
if (r.counts) out += `<text align="left" ${NORMAL}>Total sales: ${r.counts.cash + r.counts.card + r.counts.eft + r.counts.account} </text>`;
|
||||
return out;
|
||||
},
|
||||
payment_breakdown: function () {
|
||||
|
|
@ -268,20 +278,20 @@ window.EposPrint = (function () {
|
|||
`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 '<text align="left">-------------------------------- </text>'
|
||||
+ '<text align="left">PAYMENT METHOD BREAKDOWN </text>'
|
||||
+ `<text align="left">${lines.join(' ')} </text>`;
|
||||
return `<text align="left" ${NORMAL}>-------------------------------- </text>`
|
||||
+ `<text align="left" ${NORMAL}>PAYMENT METHOD BREAKDOWN </text>`
|
||||
+ `<text align="left" ${NORMAL}>${lines.join(' ')} </text>`;
|
||||
},
|
||||
grand_total: function () {
|
||||
return '<text align="left">-------------------------------- </text>'
|
||||
+ `<text align="left" em="true">GRAND TOTAL${' '.repeat(12)}R ${fmtMoney(r.grand || 0)} </text>`;
|
||||
return `<text align="left" ${NORMAL}>-------------------------------- </text>`
|
||||
+ `<text align="left" width="1" height="1" em="true">GRAND TOTAL${' '.repeat(12)}R ${fmtMoney(r.grand || 0)} </text>`;
|
||||
},
|
||||
cash_reconciliation: function () {
|
||||
return '<text align="left">-------------------------------- </text>'
|
||||
+ '<text align="left">CASH RECONCILIATION </text>'
|
||||
+ `<text align="left">Cash sales: R ${fmtMoney((r.totals && r.totals.cash) || 0)} </text>`
|
||||
+ `<text align="left">Cash counted: R ${fmtMoney(r.cashCounted || 0)} </text>`
|
||||
+ `<text align="left" em="${((r.variance || 0) === 0) ? 'true' : 'false'}">Variance: R ${fmtMoney(r.variance || 0)} </text>`;
|
||||
return `<text align="left" ${NORMAL}>-------------------------------- </text>`
|
||||
+ `<text align="left" ${NORMAL}>CASH RECONCILIATION </text>`
|
||||
+ `<text align="left" ${NORMAL}>Cash sales: R ${fmtMoney((r.totals && r.totals.cash) || 0)} </text>`
|
||||
+ `<text align="left" ${NORMAL}>Cash counted: R ${fmtMoney(r.cashCounted || 0)} </text>`
|
||||
+ `<text align="left" width="1" height="1" em="${((r.variance || 0) === 0) ? 'true' : 'false'}">Variance: R ${fmtMoney(r.variance || 0)} </text>`;
|
||||
},
|
||||
denominations: function () {
|
||||
if (!Array.isArray(r.denoms) || !r.denoms.length) return '';
|
||||
|
|
@ -289,15 +299,15 @@ window.EposPrint = (function () {
|
|||
.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 '<text align="left">-------------------------------- </text>'
|
||||
+ '<text align="left">DENOMINATIONS COUNTED </text>'
|
||||
+ `<text align="left">${dlines.join(' ')} </text>`;
|
||||
return `<text align="left" ${NORMAL}>-------------------------------- </text>`
|
||||
+ `<text align="left" ${NORMAL}>DENOMINATIONS COUNTED </text>`
|
||||
+ `<text align="left" ${NORMAL}>${dlines.join(' ')} </text>`;
|
||||
},
|
||||
footer: function (block) {
|
||||
const text = block.text || lh.footer_text;
|
||||
if (!text) return '';
|
||||
return '<text align="left">-------------------------------- </text>'
|
||||
+ `<text align="center">${xmlEscape(text)} </text>`;
|
||||
return `<text align="left" ${NORMAL}>-------------------------------- </text>`
|
||||
+ `<text align="center" ${NORMAL}>${xmlEscape(text)} </text>`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -621,7 +621,11 @@ function saleEscPosRenderers(s){
|
|||
const discount = s.discount || { kind: 'none', input: 0, amount: 0 };
|
||||
const hasDiscount = discount.kind !== 'none' && Number(discount.amount) > 0;
|
||||
return {
|
||||
header_title: (block) => ESC+'a'+E(1) + ESC+'!'+E(0x30) + (block.text||'WFSALES POS') + '\n' + ESC+'!'+E(0x00),
|
||||
// ESC/POS alignment (ESC a) and print-mode (ESC !) are persistent
|
||||
// printer state, not per-line attributes — since blocks are
|
||||
// reorderable, this must reset both back to normal itself rather
|
||||
// than relying on the next block (business_info) to do it.
|
||||
header_title: (block) => ESC+'a'+E(1) + ESC+'!'+E(0x30) + (block.text||'WFSALES POS') + '\n' + ESC+'!'+E(0x00) + ESC+'a'+E(0),
|
||||
// Web Bluetooth GATT printing has no raster-image support in this
|
||||
// app — this block intentionally prints nothing on this transport.
|
||||
logo: () => '',
|
||||
|
|
|
|||
Loading…
Reference in a new issue