Factura A, B, C, QR

This commit is contained in:
martin
2025-10-09 20:25:33 -03:00
parent 65b24fe27f
commit d143679251
6 changed files with 911 additions and 30 deletions

418
index.js
View File

@@ -10,11 +10,17 @@
"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"
@@ -26,19 +32,10 @@ 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 printTicketDataRaw(data) {
printer.printDirect({
data,
printer: defaultPrinter,
type: "RAW",
success: jobID => console.log("Trabajo enviado:", jobID),
error: err => console.error("Error:", err)
})
}
const app = express()
app.use(cors())
app.use(express.json())
@@ -56,6 +53,90 @@ app.post('/digitalpowerstock/ticket/venta', (req, res) => {
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)
@@ -63,6 +144,72 @@ export function removeAccents(str) {
.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; // Ajusta según tu impresora (32, 42, 48 caracteres típicos)
const priceWidth = 10; // Espacio reservado para el precio (ej: "$999.99")
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"
let leftPart = `${cantidad} x ${nombre}`;
// Calcular cuántos espacios necesitamos
const availableSpace = lineWidth - precio.length;
// Si el nombre es muy largo, cortarlo
if (leftPart.length > availableSpace) {
leftPart = leftPart.substring(0, availableSpace - 3) + "...";
}
// Rellenar con espacios hasta que el precio quede alineado a la derecha
const spaces = lineWidth - leftPart.length - precio.length;
const spacePadding = " ".repeat(Math.max(0, spaces));
ticket += left() + leftPart + spacePadding + precio + 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,
@@ -92,23 +239,8 @@ async function ticketDPSTock(
ticket += `Fecha: ${fecha}` + line();
ticket += `Vendedor: ${vendedor}` + line(2);
// Productos
const max = 20; // ancho máximo de nombre antes de cortar
for (const producto of products) {
const nombre = removeAccents(producto.nombre);
if (producto.nombre.length > max) {
ticket += left() + `${producto.cantidad} x ${nombre}` + line();
ticket += right() + `$${producto.precio}` + line();
} else {
ticket += left() + `${producto.cantidad} x ${nombre} - $${producto.precio}` + line();
}
if (producto.observacion) {
ticket += left() + `Observacion: ${producto.observacion}` + line();
}
ticket += line();
}
ticket += printProducts(JSON.parse(productos))
ticket += line();
// Total
ticket += line(2);
@@ -134,6 +266,236 @@ async function ticketDPSTock(
}
}
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 += printBetween("Total: ", `$${total}`) + 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);
}
}
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 += printBetween("Total: ", `$${total}`) + line(2);
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 += printBetween("Total: ", `$${total}`);
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();
})