refactor(pos): make buildEposXml block-driven
This commit is contained in:
parent
c56211d35e
commit
81197ad641
2 changed files with 82 additions and 44 deletions
|
|
@ -72,52 +72,87 @@ window.EposPrint = (function () {
|
|||
return String(s == null ? '' : s).replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
||||
}
|
||||
|
||||
// Mirrors buildEscPos() in receipt.html — same fields, same discount
|
||||
// math — but emits ePOS-Print XML tags instead of raw ESC/POS bytes.
|
||||
function buildEposXml(s, logo) {
|
||||
const parts = [];
|
||||
if (logo) parts.push(`<image width="${logo.width}" height="${logo.height}" color="color_1" mode="mono" align="center">${logo.base64}</image>`);
|
||||
parts.push('<feed line="1"/>');
|
||||
parts.push('<text align="center" width="2" height="2">WFSALES POS </text>');
|
||||
if (s.letterhead && s.letterhead.name) {
|
||||
parts.push(`<text align="center">${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>`);
|
||||
parts.push('<text align="left">-------------------------------- </text>');
|
||||
const d = new Date(s.createdAt);
|
||||
parts.push(`<text align="left">Date: ${xmlEscape(d.toLocaleString('en-ZA'))} Slip: ${xmlEscape((s.localId || '').slice(-8).toUpperCase())} </text>`);
|
||||
if (s.customer) parts.push(`<text align="left">Customer: ${xmlEscape(s.customer.name)} </text>`);
|
||||
parts.push('<text align="left">-------------------------------- </text>');
|
||||
const itemLines = s.items.map(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('');
|
||||
parts.push(`<text align="left">${itemLines}</text>`);
|
||||
parts.push('<text align="left">-------------------------------- </text>');
|
||||
// Per-block XML fragment renderers for the sale slip. Each function
|
||||
// returns the exact XML this block contributes; `block` is the
|
||||
// resolved block ({id, enabled, text}) so header_title/footer can
|
||||
// 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.
|
||||
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);
|
||||
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;
|
||||
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">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>`);
|
||||
const paidLabel = s.payment === 'cash' ? `CASH R ${(s.cashGiven || 0).toFixed(2)}` : s.payment.toUpperCase();
|
||||
parts.push(`<text align="left">Paid: ${xmlEscape(paidLabel)} </text>`);
|
||||
if (s.payment === 'cash') parts.push(`<text align="left">Change${' '.repeat(24)}R ${(s.change || 0).toFixed(2)} </text>`);
|
||||
parts.push('<text align="left">-------------------------------- </text>');
|
||||
if (s.letterhead && s.letterhead.footer_text) parts.push(`<text align="center">${xmlEscape(s.letterhead.footer_text)} </text>`);
|
||||
parts.push('<feed line="3"/>');
|
||||
parts.push('<cut type="feed"/>');
|
||||
return `<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><epos-print xmlns="http://www.epson-pos.com/schemas/2011/03/epos-print">${parts.join('')}</epos-print></s:Body></s:Envelope>`;
|
||||
return {
|
||||
header_title: function (block) {
|
||||
const title = block.text || 'WFSALES POS';
|
||||
return '<feed line="1"/><text align="center" width="2" height="2">' + xmlEscape(title) + ' </text>';
|
||||
},
|
||||
logo: function () {
|
||||
return logo ? `<image width="${logo.width}" height="${logo.height}" color="color_1" mode="mono" align="center">${logo.base64}</image>` : '';
|
||||
},
|
||||
business_info: function () {
|
||||
const parts = [];
|
||||
if (s.letterhead && s.letterhead.name) parts.push(`<text align="center">${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>`);
|
||||
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>`;
|
||||
},
|
||||
customer: function () {
|
||||
return s.customer ? `<text align="left">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>`;
|
||||
},
|
||||
totals: function () {
|
||||
const parts = ['<text align="left">-------------------------------- </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">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>`);
|
||||
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>`;
|
||||
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>`;
|
||||
return out;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// `blocks` is the resolved block list from PosSlipLayout.resolveBlocks('sale', ...)
|
||||
// (or getBlocksForPrint). Renders each enabled block in order, then
|
||||
// the fixed closing feed+cut every sale slip ends with.
|
||||
function buildEposXml(s, logo, blocks) {
|
||||
const renderers = saleBlockRenderers(s, logo);
|
||||
const body = blocks
|
||||
.filter(function (b) { return b.enabled; })
|
||||
.map(function (b) { return renderers[b.id] ? renderers[b.id](b) : ''; })
|
||||
.join('');
|
||||
return `<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><epos-print xmlns="http://www.epson-pos.com/schemas/2011/03/epos-print">${body}<feed line="3"/><cut type="feed"/></epos-print></s:Body></s:Envelope>`;
|
||||
}
|
||||
|
||||
async function printViaNetwork(ip, xml) {
|
||||
|
|
@ -284,7 +319,8 @@ window.EposPrint = (function () {
|
|||
const logo = lh.logo_url
|
||||
? await imageToRaster(lh.logo_url, 384).catch(() => null)
|
||||
: null;
|
||||
const xml = buildEposXml(sale, logo);
|
||||
const blocks = window.PosSlipLayout.getBlocksForPrint(lh.id, 'sale');
|
||||
const xml = buildEposXml(sale, logo, blocks);
|
||||
if (settings.type === 'network') {
|
||||
if (!settings.ip) {
|
||||
throw new Error('Set the printer IP address in Printer settings first.');
|
||||
|
|
|
|||
|
|
@ -541,7 +541,9 @@ async function printThermalNetwork(settings){
|
|||
successLabel = 'Delivery note printed · ';
|
||||
} else {
|
||||
const logo = await window.EposPrint.imageToRaster('/wfdash-logo.png', 384).catch(()=>null);
|
||||
xml = window.EposPrint.buildEposXml(sale, logo);
|
||||
const letterheadId = sale.letterhead && sale.letterhead.id;
|
||||
const blocks = window.PosSlipLayout.getBlocksForPrint(letterheadId, 'sale');
|
||||
xml = window.EposPrint.buildEposXml(sale, logo, blocks);
|
||||
successLabel = 'Printed · ';
|
||||
}
|
||||
await window.EposPrint.printViaNetwork(settings.ip, xml);
|
||||
|
|
|
|||
Loading…
Reference in a new issue