From 5da79cc3ecb71f1dbba88d51fcd12c0ca6a4dc39 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Tue, 17 Feb 2026 06:30:48 +0000 Subject: [PATCH] Add ZPL label generator and print service with tests Implement ZPL generator for creating Zebra printer labels from article data (description, intercambio, barcode) with support for multiple copies and description truncation. Add TCP-based print service for sending ZPL data to network printers. Co-Authored-By: Claude Opus 4.6 --- src/services/print-service.js | 28 +++++++++++++++++++ src/services/zpl-generator.js | 19 +++++++++++++ tests/zpl-generator.test.js | 52 +++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 src/services/print-service.js create mode 100644 src/services/zpl-generator.js create mode 100644 tests/zpl-generator.test.js diff --git a/src/services/print-service.js b/src/services/print-service.js new file mode 100644 index 0000000..78ced30 --- /dev/null +++ b/src/services/print-service.js @@ -0,0 +1,28 @@ +const net = require('net'); + +function printZpl(zplData, printerIp, printerPort = 9100) { + return new Promise((resolve, reject) => { + const client = new net.Socket(); + + client.connect(printerPort, printerIp, () => { + client.write(zplData, () => { + client.end(); + }); + }); + + client.on('close', () => { + resolve({ success: true }); + }); + + client.on('error', (err) => { + reject(new Error(`Print error: ${err.message}`)); + }); + + client.setTimeout(10000, () => { + client.destroy(); + reject(new Error('Print timeout: could not connect to printer')); + }); + }); +} + +module.exports = { printZpl }; diff --git a/src/services/zpl-generator.js b/src/services/zpl-generator.js new file mode 100644 index 0000000..fa579b4 --- /dev/null +++ b/src/services/zpl-generator.js @@ -0,0 +1,19 @@ +function generateLabel(article, quantity = 1) { + const desc = (article.descripcion || '').substring(0, 40); + const intercambio = article.intercambio || 'S/N'; + const code = article.noIdentificacion || ''; + + const singleLabel = [ + '^XA', + `^FO30,20^A0N,28,28^FD${desc}^FS`, + `^FO30,60^A0N,22,22^FDInt: ${intercambio}^FS`, + `^FO30,95^BY2^BCN,50,Y,N,N^FD${code}^FS`, + '^XZ', + ].join('\n'); + + if (quantity <= 1) return singleLabel; + + return Array(quantity).fill(singleLabel).join('\n'); +} + +module.exports = { generateLabel }; diff --git a/tests/zpl-generator.test.js b/tests/zpl-generator.test.js new file mode 100644 index 0000000..63b4ed1 --- /dev/null +++ b/tests/zpl-generator.test.js @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { generateLabel } from '../src/services/zpl-generator.js'; + +describe('ZPL Generator', () => { + it('should generate valid ZPL with name, intercambio, and barcode', () => { + const zpl = generateLabel({ + descripcion: 'Balata ceramica delantera', + intercambio: 'INT-9999', + noIdentificacion: 'BK-001', + }); + + expect(zpl).toContain('^XA'); + expect(zpl).toContain('^XZ'); + expect(zpl).toContain('Balata ceramica delantera'); + expect(zpl).toContain('INT-9999'); + expect(zpl).toContain('BK-001'); + }); + + it('should handle missing intercambio', () => { + const zpl = generateLabel({ + descripcion: 'Filtro de aceite', + intercambio: null, + noIdentificacion: 'FT-200', + }); + + expect(zpl).toContain('Filtro de aceite'); + expect(zpl).toContain('S/N'); + expect(zpl).toContain('FT-200'); + }); + + it('should truncate long descriptions', () => { + const zpl = generateLabel({ + descripcion: 'Este es un nombre de articulo extremadamente largo que no cabe en la etiqueta', + intercambio: 'X-1', + noIdentificacion: 'LONG-001', + }); + + expect(zpl).toContain('^XA'); + expect(zpl).toContain('^XZ'); + }); + + it('should generate multiple labels for quantity > 1', () => { + const zpl = generateLabel({ + descripcion: 'Balata', + intercambio: 'INT-1', + noIdentificacion: 'BK-001', + }, 3); + + const labelCount = (zpl.match(/\^XA/g) || []).length; + expect(labelCount).toBe(3); + }); +});