pos_bluetooth_thermal_printer/static/src/js/escpos_generator.js

374 lines
12 KiB
JavaScript
Executable File

/** @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<Uint8Array>} 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;