// Node version: 16.20.2 // Package json dependencies /* "dependencies": { "@thiagoelg/node-printer": "^0.6.2", "cors": "^2.8.5", "express": "^5.1.0", "iconv": "^3.0.1", "nodemon": "^3.1.10" } */ // || nvm -> nvm install 16.20.2 import express from 'express' import cors from 'cors' import printer from '@thiagoelg/node-printer' import iconv from "iconv-lite"; import QRCode from 'qrcode' import { createCanvas, loadImage } from 'canvas' // Genera un QR y lo imprime en la impresora térmica const ESC = "\x1B" const GS = "\x1D" const defaultPrinter = printer.getDefaultPrinterName() function init() { return ESC + "@"; } function cut() { return GS + "V" + "\x41" + "\x00"; } function line(n = 1) { return "\n".repeat(n); } function boldOn() { return ESC + "E" + "\x01"; } function boldOff() { return ESC + "E" + "\x00"; } function center() { return ESC + "a" + "\x01"; } function centerOff() { return ESC + "a" + "\x00"; } function left() { return ESC + "a" + "\x00"; } function right() { return ESC + "a" + "\x02"; } const app = express() app.use(cors()) app.use(express.json()) const max = 30; app.get('/printers', (req, res) => { res.send(200) }) app.post('/digitalpowerstock/ticket/venta', (req, res) => { const { nombre, direccion, cp, localidad, provincia, pais, telefono, mensaje, comprobante, cliente, fecha, vendedor, productos, total } = req.body; ticketDPSTock(nombre, direccion, cp, localidad, provincia, pais, telefono, mensaje, comprobante, cliente, fecha, vendedor, productos, total) res.send(200) }) app.post('/facturador/ticket/a', (req, res) => { const { comercio, comercio_cuit, inicio_actividades, condicion_iva_comercio, punto_venta, cae, vencimiento_cae, iva_contenido, cliente_cuit, cliente_condicion, cliente_direccion, nombre, direccion, localidad, provincia, comprobante, cliente, fecha, qr_link, productos, total, subtotal } = req.body; ticketFacturadorA(comercio, comercio_cuit, inicio_actividades, condicion_iva_comercio, punto_venta, cae, vencimiento_cae, iva_contenido, cliente_cuit, cliente_condicion, cliente_direccion, nombre, direccion, localidad, provincia, comprobante, cliente, fecha, qr_link, productos, total, subtotal) res.send(200) }) app.post('/facturador/ticket/b', (req, res) => { const { comercio, comercio_cuit, inicio_actividades, condicion_iva_comercio, punto_venta, cae, vencimiento_cae, iva_contenido, cliente_cuit, cliente_condicion, cliente_direccion, nombre, direccion, cp, localidad, provincia, pais, telefono, mensaje, comprobante, cliente, fecha, qr_link, productos, total } = req.body; ticketFacturadorB(comercio, comercio_cuit, inicio_actividades, condicion_iva_comercio, punto_venta, cae, vencimiento_cae, iva_contenido, cliente_cuit, cliente_condicion, cliente_direccion, nombre, direccion, cp, localidad, provincia, pais, telefono, mensaje, comprobante, cliente, fecha, qr_link, productos, total ) res.send(200) }) app.post('/facturador/ticket/c', (req, res) => { const { comercio, comercio_cuit, inicio_actividades, condicion_iva_comercio, punto_venta, cae, vencimiento_cae, iva_contenido, cliente_cuit, cliente_condicion, cliente_direccion, nombre, direccion, cp, localidad, provincia, pais, telefono, mensaje, comprobante, cliente, fecha, qr_link, productos, total } = req.body; ticketFacturadorC(comercio, comercio_cuit, inicio_actividades, condicion_iva_comercio, punto_venta, cae, vencimiento_cae, iva_contenido, cliente_cuit, cliente_condicion, cliente_direccion, nombre, direccion, cp, localidad, provincia, pais, telefono, mensaje, comprobante, cliente, fecha, qr_link, productos, total ) res.send(200) }) async function printQRAsImage(link, moduleSize = 6) { try { const qrSize = moduleSize * 33; const qrBuffer = await QRCode.toBuffer(link, { width: qrSize, margin: 2, errorCorrectionLevel: 'M', type: 'png' }); const img = await loadImage(qrBuffer); const canvas = createCanvas(img.width, img.height); const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const width = canvas.width; const height = canvas.height; const widthBytes = Math.ceil(width / 8); const imageBytes = []; for (let y = 0; y < height; y++) { for (let x = 0; x < widthBytes; x++) { let byte = 0; for (let bit = 0; bit < 8; bit++) { const pixelX = x * 8 + bit; if (pixelX < width) { const idx = (y * width + pixelX) * 4; const brightness = imageData.data[idx]; if (brightness < 128) { byte |= (1 << (7 - bit)); } } } imageBytes.push(byte); } } const commands = []; // Centrar commands.push(Buffer.from([0x1B, 0x61, 0x01])); // GS v 0 const xL = widthBytes & 0xFF; const xH = (widthBytes >> 8) & 0xFF; const yL = height & 0xFF; const yH = (height >> 8) & 0xFF; commands.push(Buffer.from([0x1D, 0x76, 0x30, 0x00, xL, xH, yL, yH])); commands.push(Buffer.from(imageBytes)); commands.push(Buffer.from([0x0A, 0x0A])); commands.push(Buffer.from([0x1B, 0x61, 0x00])); return Buffer.concat(commands); } catch (error) { console.error('Error generando QR:', error); return Buffer.from(''); } } export function removeAccents(str) { return String(str) .normalize('NFD') // descompone letras + diacríticos .replace(/[\u0300-\u036f]/g, ''); // elimina marcas diacríticas } function printDoubleLine() { return left() + "============================================" + line(); } function printDottedLine() { return left() + "--------------------------------------------" + line(); } function printProducts(products) { let ticket = ""; const lineWidth = 46; const priceWidth = 10; for (const producto of products) { const nombre = removeAccents(producto?.nombre); const cantidad = producto?.cantidad; const precio = `$${producto?.precio}`; // Construir la parte izquierda: "2 x Producto" const prefix = `${cantidad} x `; const leftPart = `${prefix}${nombre}`; // Calcular espacio disponible para el texto completo (sin considerar el precio aún) const availableSpace = lineWidth; if (leftPart.length <= lineWidth - precio.length - 1) { // Si cabe todo en una línea (con el precio) const spaces = lineWidth - leftPart.length - precio.length; const spacePadding = " ".repeat(Math.max(0, spaces)); ticket += left() + leftPart + spacePadding + precio + line(); } else { // Dividir el texto en líneas const lines = []; let remainingText = leftPart; const indentation = " ".repeat(prefix.length); // Primera línea sin indentación lines.push(remainingText.substring(0, lineWidth)); remainingText = remainingText.substring(lineWidth); // Líneas intermedias con indentación while (remainingText.length > lineWidth - indentation.length - precio.length - 1) { const lineText = indentation + remainingText.substring(0, lineWidth - indentation.length); lines.push(lineText); remainingText = remainingText.substring(lineWidth - indentation.length); } // Última línea con el precio const lastLineText = indentation + remainingText; const spaces = lineWidth - lastLineText.length - precio.length; const spacePadding = " ".repeat(Math.max(0, spaces)); lines.push(lastLineText + spacePadding + precio); // Agregar todas las líneas al ticket for (const _line of lines) { ticket += left() + _line + line(); } } if (producto?.observaciones?.length > 0) { const obs = removeAccents(producto.observaciones); ticket += "Observaciones: " + obs + line(); } ticket += printDottedLine(); } return ticket; } function printBetween(leftText, rightText, lineWidth = 46) { // Asegurar que los valores sean strings const left = String(leftText); const right = String(rightText); // Calcular el espacio disponible para el texto izquierdo const availableSpace = lineWidth - right.length; // Si el texto izquierdo es muy largo, cortarlo let finalLeft = left; if (left.length > availableSpace) { finalLeft = left.substring(0, availableSpace - 3) + "..."; } // Calcular espacios de relleno const spaces = lineWidth - finalLeft.length - right.length; const spacePadding = " ".repeat(Math.max(0, spaces)); // Retornar la línea formateada return finalLeft + spacePadding + right; } async function ticketDPSTock( nombre, direccion, cp, localidad, provincia, pais, telefono, mensaje, comprobante, cliente, fecha, vendedor, productos, total ) { try { const products = JSON.parse(productos); //const products = productos; let ticket = ""; // Encabezado ticket += init(); ticket += center(); ticket += boldOn() + nombre + boldOff() + line(); ticket += direccion + line(); ticket += `(${cp}) ${localidad}` + line(); ticket += `${provincia} ${pais}` + line(); ticket += telefono + line(); ticket += mensaje + line(3); // Datos comprobante ticket += left(); ticket += `Nro comprobante: #${comprobante}` + line(); ticket += `Cliente: ${cliente}` + line(); ticket += `Fecha: ${fecha}` + line(); ticket += `Vendedor: ${vendedor}` + line(2); ticket += printProducts(JSON.parse(productos)) ticket += line(); // Total ticket += line(2); //ticket += left() + `Total: $${total}` + line(3); ticket += boldOn(); ticket += printBetween("Total: ", `$${total}`) + line(3); ticket += boldOff(); // Corte ticket += cut(); // Convertir a CP850 (acentos correctos) const data = iconv.encode(ticket, "CP850"); // Enviar a la impresora printer.printDirect({ data, printer: printer.getDefaultPrinterName(), type: "RAW", success: jobID => console.log("Trabajo enviado:", jobID), error: err => console.error("Error:", err) }); } catch (error) { console.error("Error al imprimir:", error); } } async function ticketFacturadorA( comercio, comercio_cuit, inicio_actividades, condicion_iva_comercio, punto_venta, cae, vencimiento_cae, iva_contenido, cliente_cuit, cliente_condicion, cliente_direccion, nombre, direccion, localidad, provincia, comprobante, cliente, fecha, qr_link, productos, total, subtotal ) { try { let ticket = ""; // Encabezado ticket += init(); ticket += center(); ticket += boldOn() + comercio + boldOff() + line(2); ticket += centerOff(); ticket += nombre + line(); ticket += "CUIT: Nro: " + comercio_cuit + line(); ticket += "Ing. Brutos: " + comercio_cuit + line(); ticket += "Direccion: " + direccion + line(); ticket += `${localidad} (${provincia})` + line(); ticket += "Inicio de Actividades: " + inicio_actividades + line(); ticket += condicion_iva_comercio + line(); ticket += "PUNTO DE VENTA: " + punto_venta + line(); ticket += printDoubleLine() ticket += "TICKET / FACTURA A" + line(); ticket += "Nro: " + comprobante + line(); ticket += "Fecha: " + fecha + line(); ticket += printDoubleLine() ticket += "TIPO CLIENTE: " + cliente_condicion + line(); ticket += "CLIENTE: " + cliente + line(); ticket += "CUIT: " + cliente_cuit + line(); ticket += "DIRECCION: " + cliente_direccion + line(); ticket += printDoubleLine() ticket += printProducts(JSON.parse(productos)) + line() // Total ticket += printBetween("SUBTOTAL. IMP. NETO GRAVADO: ", `$${subtotal}`) + line(); ticket += printBetween("IVA: ", `$${iva_contenido}`) + line(); ticket += boldOn(); ticket += printBetween("Total: ", `$${total}`) + line(2); ticket += boldOff(); ticket += boldOn() + "TRANSPARENCIA FISCAL" + boldOff() + line(); ticket += "Reg. Transp. Fiscal (Ley 27743)" + line() ticket += "CAE: " + cae + line() ticket += "Vencimiento CAE: " + vencimiento_cae + line() ticket += line(2) + center() // Convertir texto a CP850 ANTES del QR const textData = iconv.encode(ticket, "CP850"); // Generar QR como Buffer binario const qr = (await printQRAsImage(qr_link, 12)); // Agregar saltos y corte después del QR const footer = iconv.encode(line(1) + cut(), "CP850"); // Combinar todo const data = Buffer.concat([textData, qr, footer]); // Enviar a la impresora printer.printDirect({ data, printer: printer.getDefaultPrinterName(), type: "RAW", success: jobID => console.log("Trabajo enviado:", jobID), error: err => console.error("Error:", err) }); } catch (error) { console.error("Error al imprimir:", error); } } async function ticketFacturadorB( comercio, comercio_cuit, inicio_actividades, condicion_iva_comercio, punto_venta, cae, vencimiento_cae, iva_contenido, cliente_cuit, cliente_condicion, cliente_direccion, nombre, direccion, cp, localidad, provincia, pais, telefono, mensaje, comprobante, cliente, fecha, qr_link, productos, total ) { try { let ticket = ""; // Encabezado ticket += init(); ticket += center(); ticket += boldOn() + comercio + boldOff() + line(2); ticket += centerOff(); ticket += nombre + line(); ticket += "CUIT: Nro: " + comercio_cuit + line(); ticket += "Ing. Brutos: " + comercio_cuit + line(); ticket += "Direccion: " + direccion + line(); ticket += `${localidad} (${provincia})` + line(); ticket += "Inicio de Actividades: " + inicio_actividades + line(); ticket += condicion_iva_comercio + line(); ticket += "PUNTO DE VENTA: " + punto_venta + line(); ticket += printDoubleLine() ticket += "TICKET / FACTURA B" + line(); ticket += "Nro: " + comprobante + line(); ticket += "Fecha: " + fecha + line(); ticket += printDoubleLine() ticket += "TIPO CLIENTE: " + cliente_condicion + line(); ticket += "CLIENTE: " + cliente + line(); ticket += "CUIT: " + cliente_cuit + line(); ticket += "DIRECCION: " + cliente_direccion + line(); ticket += printDoubleLine() ticket += printProducts(JSON.parse(productos)) + line() // Total ticket += boldOn(); ticket += printBetween("Total: ", `$${total}`) + line(2); ticket += boldOff(); ticket += boldOn() + "TRANSPARENCIA FISCAL" + boldOff() + line(); ticket += "Reg. Transp. Fiscal (Ley 27743)" + line() ticket += "IVA CONTENIDO: $" + iva_contenido + line() ticket += "CAE: " + cae + line() ticket += "Vencimiento CAE: " + vencimiento_cae + line() ticket += line(2) + center() // Convertir texto a CP850 ANTES del QR const textData = iconv.encode(ticket, "CP850"); // Generar QR como Buffer binario const qr = (await printQRAsImage(qr_link, 12)); // Agregar saltos y corte después del QR const footer = iconv.encode(line(1) + cut(), "CP850"); // Combinar todo const data = Buffer.concat([textData, qr, footer]); // Enviar a la impresora printer.printDirect({ data, printer: printer.getDefaultPrinterName(), type: "RAW", success: jobID => console.log("Trabajo enviado:", jobID), error: err => console.error("Error:", err) }); } catch (error) { console.error("Error al imprimir:", error); } } async function ticketFacturadorC( comercio, comercio_cuit, inicio_actividades, condicion_iva_comercio, punto_venta, cae, vencimiento_cae, iva_contenido, cliente_cuit, cliente_condicion, cliente_direccion, nombre, direccion, cp, localidad, provincia, pais, telefono, mensaje, comprobante, cliente, fecha, qr_link, productos, total ) { try { let ticket = ""; // Encabezado ticket += init(); ticket += center(); ticket += boldOn() + comercio + boldOff() + line(2); ticket += centerOff(); ticket += nombre + line(); ticket += "CUIT: Nro: " + comercio_cuit + line(); ticket += "Ing. Brutos: " + comercio_cuit + line(); ticket += "Direccion: " + direccion + line(); ticket += `${localidad} (${provincia})` + line(); ticket += "Inicio de Actividades: " + inicio_actividades + line(); ticket += condicion_iva_comercio + line(); ticket += "PUNTO DE VENTA: " + punto_venta + line(); ticket += printDoubleLine() ticket += "TICKET / FACTURA C" + line(); ticket += "Nro: " + comprobante + line(); ticket += "Fecha: " + fecha + line(); ticket += printDoubleLine() ticket += "TIPO CLIENTE: " + cliente_condicion + line(); ticket += "CLIENTE: " + cliente + line(); ticket += "CUIT: " + cliente_cuit + line(); ticket += "DIRECCION: " + cliente_direccion + line(); ticket += printDoubleLine() ticket += printProducts(JSON.parse(productos)) + line() // Total ticket += boldOn(); ticket += printBetween("Total: ", `$${total}`); ticket += boldOff(); ticket += line(2); ticket += boldOn() + "TRANSPARENCIA FISCAL" + boldOff() + line(); ticket += "Reg. Transp. Fiscal (Ley 27743)" + line() ticket += "CAE: " + cae + line() ticket += "Vencimiento CAE: " + vencimiento_cae + line() ticket += line(2) + center() // Convertir texto a CP850 ANTES del QR const textData = iconv.encode(ticket, "CP850"); // Generar QR como Buffer binario const qr = (await printQRAsImage(qr_link, 12)); // Agregar saltos y corte después del QR const footer = iconv.encode(line(1) + cut(), "CP850"); // Combinar todo const data = Buffer.concat([textData, qr, footer]); // Enviar a la impresora printer.printDirect({ data, printer: printer.getDefaultPrinterName(), type: "RAW", success: jobID => console.log("Trabajo enviado:", jobID), error: err => console.error("Error:", err) }); } catch (error) { console.error("Error al imprimir:", error); } } app.listen(3030, () => { console.log("Servicio de impresion de tickets inicializado en el puerto 3030"); // testQROnly(); })