/** @odoo-module **/ /** * ESC/POS Command Generator * * Generates ESC/POS command sequences for thermal printers. * Supports text formatting, alignment, sizing, and receipt generation. */ // ESC/POS Command Constants const ESC = 0x1B; const GS = 0x1D; const LF = 0x0A; const CR = 0x0D; // Initialization const INIT = [ESC, 0x40]; // Alignment commands const ALIGN_LEFT = [ESC, 0x61, 0x00]; const ALIGN_CENTER = [ESC, 0x61, 0x01]; const ALIGN_RIGHT = [ESC, 0x61, 0x02]; // Text emphasis const BOLD_ON = [ESC, 0x45, 0x01]; const BOLD_OFF = [ESC, 0x45, 0x00]; const UNDERLINE_ON = [ESC, 0x2D, 0x01]; const UNDERLINE_OFF = [ESC, 0x2D, 0x00]; // Paper control const FEED_LINE = [LF]; const CUT_PAPER = [GS, 0x56, 0x00]; // Character sets const CHARSET_USA = [ESC, 0x52, 0x00]; const CHARSET_FRANCE = [ESC, 0x52, 0x01]; const CHARSET_GERMANY = [ESC, 0x52, 0x02]; const CHARSET_UK = [ESC, 0x52, 0x03]; const CHARSET_DENMARK = [ESC, 0x52, 0x04]; const CHARSET_SWEDEN = [ESC, 0x52, 0x05]; const CHARSET_ITALY = [ESC, 0x52, 0x06]; const CHARSET_SPAIN = [ESC, 0x52, 0x07]; export class EscPosGenerator { constructor() { this.commands = []; this.characterSet = 'CP437'; // Default character set } /** * Initialize printer with default settings * @returns {Uint8Array} Initialization command sequence */ initialize() { return new Uint8Array(INIT); } /** * Set text alignment * @param {string} alignment - 'left', 'center', or 'right' * @returns {Uint8Array} Alignment command sequence */ setAlignment(alignment) { switch (alignment.toLowerCase()) { case 'left': return new Uint8Array(ALIGN_LEFT); case 'center': return new Uint8Array(ALIGN_CENTER); case 'right': return new Uint8Array(ALIGN_RIGHT); default: return new Uint8Array(ALIGN_LEFT); } } /** * Set text size * @param {number} width - Width multiplier (1-8) * @param {number} height - Height multiplier (1-8) * @returns {Uint8Array} Text size command sequence */ setTextSize(width = 1, height = 1) { // Validate input ranges width = Math.max(1, Math.min(8, width)); height = Math.max(1, Math.min(8, height)); // ESC/POS uses 0-7 for size (0 = normal, 7 = 8x) const widthValue = width - 1; const heightValue = height - 1; // Combine width and height into single byte // High nibble = width, low nibble = height const sizeValue = (widthValue << 4) | heightValue; return new Uint8Array([GS, 0x21, sizeValue]); } /** * Set text emphasis (bold and underline) * @param {boolean} bold - Enable bold * @param {boolean} underline - Enable underline * @returns {Uint8Array} Emphasis command sequence */ setEmphasis(bold = false, underline = false) { const commands = []; if (bold) { commands.push(...BOLD_ON); } else { commands.push(...BOLD_OFF); } if (underline) { commands.push(...UNDERLINE_ON); } else { commands.push(...UNDERLINE_OFF); } return new Uint8Array(commands); } /** * Feed paper and cut * @param {number} lines - Number of lines to feed before cutting (default: 3) * @returns {Uint8Array} Feed and cut command sequence */ feedAndCut(lines = 3) { const commands = []; // Feed lines for (let i = 0; i < lines; i++) { commands.push(...FEED_LINE); } // Cut paper commands.push(...CUT_PAPER); return new Uint8Array(commands); } /** * Encode text to bytes using the configured character set * @param {string} text - Text to encode * @returns {Uint8Array} Encoded text bytes */ encodeText(text) { if (!text) { return new Uint8Array(0); } // For CP437 and similar single-byte character sets, // we can use a simple encoding approach // For production, you might want to use a proper encoding library const encoder = new TextEncoder(); const encoded = encoder.encode(text); return encoded; } /** * Add a line of text with optional formatting * @param {string} text - Text to add * @param {Object} options - Formatting options * @returns {Uint8Array} Formatted text command sequence */ addLine(text, options = {}) { const commands = []; // Apply alignment if (options.align) { commands.push(...this.setAlignment(options.align)); } // Apply text size if (options.width || options.height) { commands.push(...this.setTextSize(options.width || 1, options.height || 1)); } // Apply emphasis if (options.bold !== undefined || options.underline !== undefined) { commands.push(...this.setEmphasis(options.bold || false, options.underline || false)); } // Add text commands.push(...this.encodeText(text)); // Add line feed commands.push(...FEED_LINE); // Reset formatting commands.push(...this.setTextSize(1, 1)); commands.push(...this.setEmphasis(false, false)); return new Uint8Array(commands); } /** * Generate complete receipt from Odoo receipt data * @param {Object} receiptData - Odoo receipt structure * @returns {Uint8Array} Complete ESC/POS command sequence */ generateReceipt(receiptData) { const commands = []; // Initialize printer commands.push(...this.initialize()); // Header section if (receiptData.headerData) { const header = receiptData.headerData; if (header.companyName) { commands.push(...this.addLine(header.companyName, { align: 'center', width: 2, height: 2, bold: true })); } if (header.address) { commands.push(...this.addLine(header.address, { align: 'center' })); } if (header.phone) { commands.push(...this.addLine(header.phone, { align: 'center' })); } if (header.taxId) { commands.push(...this.addLine(`Tax ID: ${header.taxId}`, { align: 'center' })); } // Separator line commands.push(...this.addLine(''.padEnd(48, '-'), { align: 'center' })); } // Order information if (receiptData.orderData) { const order = receiptData.orderData; if (order.orderName) { commands.push(...this.addLine(`Order: ${order.orderName}`, { bold: true })); } if (order.date) { commands.push(...this.addLine(`Date: ${order.date}`)); } if (order.cashier) { commands.push(...this.addLine(`Cashier: ${order.cashier}`)); } if (order.customer) { commands.push(...this.addLine(`Customer: ${order.customer}`)); } commands.push(...this.addLine(''.padEnd(48, '-'))); } // Line items if (receiptData.lines && receiptData.lines.length > 0) { // Header for items commands.push(...this.addLine('Item Qty Price Total', { bold: true })); receiptData.lines.forEach(line => { // Product name (truncate if too long) let productName = line.productName || ''; if (productName.length > 24) { productName = productName.substring(0, 21) + '...'; } // Format line with proper spacing const qty = (line.quantity || 0).toFixed(2).padStart(6); const price = (line.price || 0).toFixed(2).padStart(8); const total = (line.total || 0).toFixed(2).padStart(8); commands.push(...this.addLine(productName.padEnd(24))); commands.push(...this.addLine(`${' '.repeat(24)}${qty}${price}${total}`)); }); commands.push(...this.addLine(''.padEnd(48, '-'))); } // Totals section if (receiptData.totals) { const totals = receiptData.totals; if (totals.subtotal !== undefined) { const subtotalLine = `Subtotal:`.padEnd(40) + (totals.subtotal || 0).toFixed(2).padStart(8); commands.push(...this.addLine(subtotalLine)); } if (totals.discount !== undefined && totals.discount > 0) { const discountLine = `Discount:`.padEnd(40) + (totals.discount || 0).toFixed(2).padStart(8); commands.push(...this.addLine(discountLine)); } if (totals.tax !== undefined) { const taxLine = `Tax:`.padEnd(40) + (totals.tax || 0).toFixed(2).padStart(8); commands.push(...this.addLine(taxLine)); } if (totals.total !== undefined) { const totalLine = `TOTAL:`.padEnd(40) + (totals.total || 0).toFixed(2).padStart(8); commands.push(...this.addLine(totalLine, { bold: true, width: 2, height: 2 })); } commands.push(...this.addLine(''.padEnd(48, '-'))); } // Payment information if (receiptData.paymentData) { const payment = receiptData.paymentData; if (payment.method) { commands.push(...this.addLine(`Payment Method: ${payment.method}`)); } if (payment.amount !== undefined) { const amountLine = `Amount Paid:`.padEnd(40) + (payment.amount || 0).toFixed(2).padStart(8); commands.push(...this.addLine(amountLine)); } if (payment.change !== undefined && payment.change > 0) { const changeLine = `Change:`.padEnd(40) + (payment.change || 0).toFixed(2).padStart(8); commands.push(...this.addLine(changeLine, { bold: true })); } commands.push(...this.addLine(''.padEnd(48, '-'))); } // Footer section if (receiptData.footerData) { const footer = receiptData.footerData; if (footer.message) { commands.push(...this.addLine(footer.message, { align: 'center' })); } if (footer.barcode) { // Note: Barcode printing would require additional ESC/POS commands // For now, just print the barcode value as text commands.push(...this.addLine(footer.barcode, { align: 'center' })); } } // Feed and cut commands.push(...this.feedAndCut(4)); return new Uint8Array(commands); } /** * Helper method to combine multiple Uint8Arrays * @param {Array} arrays - Arrays to combine * @returns {Uint8Array} Combined array */ static combineArrays(arrays) { const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const arr of arrays) { result.set(arr, offset); offset += arr.length; } return result; } } export default EscPosGenerator;