Files

618 lines
22 KiB
JavaScript

// 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"; }
function textNormal() {
return "\x1D\x21\x00"; // GS ! n → normal
}
function textDoubleHeight() {
return "\x1D\x21\x10"; // GS ! 16 → doble alto
}
function textDoubleWidth() {
return "\x1D\x21\x01"; // GS ! 1 → doble ancho
}
function textDoubleSize() {
return "\x1D\x21\x11"; // GS ! 17 → doble ancho y alto
}
function textTripleSize() {
return "\x1D\x21\x22"; // GS ! 34 → triple (si la impresora lo soporta)
}
function textQuadSize() {
return "\x1D\x21\x33"; // GS ! 34 → triple (si la impresora lo soporta)
}
function textFifthSize() {
return "\x1D\x21\x44"; // GS ! 34 → triple (si la impresora lo soporta)
}
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, adelanto } = req.body;
ticketDPSTock(nombre, direccion, cp, localidad, provincia, pais, telefono, mensaje, comprobante, cliente, fecha, vendedor, productos, total, adelanto)
res.send(200)
})
app.post('/digitalpowerstock/ticket/gondola', (req, res) => {
const { nombre, top_price, off, bottom_price } = req.body;
ticketDPSTockGondola(nombre, top_price, off, bottom_price)
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 (Number(producto.descuento) > 0) {
ticket += left() + `${producto.descuento}% OFF` + 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, adelanto
) {
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);
if (adelanto) {
ticket += printBetween("Abonado: ", `$${adelanto}`) + 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 ticketDPSTockGondola(
nombre, top_price, off, bottom_price
) {
try {
//const products = productos;
let ticket = "";
// Encabezado
ticket += init();
ticket += left();
ticket += textDoubleSize() + nombre + textNormal() + line(1);
ticket += textQuadSize() + boldOn() + "$" + top_price + textNormal() + line();
ticket += textTripleSize() + "x unidad" + textNormal() + boldOff() + line();
if (off) {
ticket += textDoubleSize() + off + "% OFF" + line();
}
if (bottom_price && !off) {
ticket += textDoubleSize() + "$" + bottom_price + line();
}
ticket += line(2);
// 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();
})