374 lines
12 KiB
JavaScript
Executable File
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;
|