UNPKG

nodefact

Version:

Biblioteca para facturación electrónica en Perú con Node.js. Compatible con UBL 2.1 y servicios web de SUNAT.

275 lines (274 loc) 11 kB
"use strict"; /** * Módulo PDF - Generación de representaciones imprimibles * * Este módulo proporciona funcionalidades para generar representaciones * imprimibles (PDF) de los documentos electrónicos. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getTemplateByDocumentType = exports.templates = exports.PDFGenerator = exports.PDFError = void 0; exports.generatePdf = generatePdf; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const pdfkit_1 = __importDefault(require("pdfkit")); const QRCode = __importStar(require("qrcode")); const Twig = require('twig'); const core_1 = require("../core"); const templates_1 = require("./templates"); Object.defineProperty(exports, "templates", { enumerable: true, get: function () { return templates_1.templates; } }); Object.defineProperty(exports, "getTemplateByDocumentType", { enumerable: true, get: function () { return templates_1.getTemplateByDocumentType; } }); // Clase para errores de generación de PDF class PDFError extends core_1.NodeFactError { constructor(message) { super(message); this.name = 'PDFError'; } } exports.PDFError = PDFError; /** * Clase para generar representaciones imprimibles (PDF) */ class PDFGenerator { constructor(options = {}) { this.options = { paper: options.paper || 'a4', orientation: options.orientation || 'portrait', fontSize: { title: options.fontSize?.title || 16, subtitle: options.fontSize?.subtitle || 14, normal: options.fontSize?.normal || 10, small: options.fontSize?.small || 8, }, color: { primary: options.color?.primary || '#007bff', secondary: options.color?.secondary || '#6c757d', text: options.color?.text || '#212529', }, margins: { top: options.margins?.top || 50, right: options.margins?.right || 40, bottom: options.margins?.bottom || 50, left: options.margins?.left || 40, }, qrCode: options.qrCode !== undefined ? options.qrCode : true, footer: { text: options.footer?.text || 'Representación impresa de comprobante electrónico', includePageNumber: options.footer?.includePageNumber !== undefined ? options.footer.includePageNumber : true, }, ...options, }; this.twig = Twig; } /** * Genera un PDF a partir de los datos de un documento electrónico * @param data Datos del documento electrónico * @param templateType Tipo de plantilla a utilizar * @returns Resultado de la generación del PDF */ async generate(data, templateType) { try { // Determinar la plantilla a utilizar let templateContent; if (templateType) { templateContent = (0, templates_1.getTemplateByDocumentType)(templateType); } else if (data.tipoDocumento) { templateContent = (0, templates_1.getTemplateByDocumentType)(data.tipoDocumento); } else { throw new PDFError('No se especificó el tipo de documento o plantilla'); } // Generar el código QR si está habilitado let qrCodeDataUrl; if (this.options.qrCode) { try { // Datos para el código QR según SUNAT const qrData = [ data.emisor.ruc, data.tipoDocumento, data.serie, data.correlativo, data.impuestos?.igv.toString() || '0.00', data.totalPrecioVenta.toString(), data.fechaEmision, data.receptor.tipoDocumento, data.receptor.numeroDocumento, ].join('|'); qrCodeDataUrl = await QRCode.toDataURL(qrData, { errorCorrectionLevel: 'M', margin: 1, width: 150, }); } catch (err) { console.error('Error al generar el código QR:', err); qrCodeDataUrl = undefined; } } // Preparar los datos para la plantilla const templateData = { ...data, qrCode: qrCodeDataUrl, footerText: this.options.footer?.text, includePageNumber: this.options.footer?.includePageNumber, pageNumber: 1, totalPages: 1, }; // Renderizar la plantilla HTML con Twig const htmlContent = await this.renderTemplate(templateContent, templateData); // Convertir HTML a PDF const pdfResult = await this.htmlToPdf(htmlContent, data); return pdfResult; } catch (err) { const error = err; return { success: false, error: `Error al generar el PDF: ${error.message}`, }; } } /** * Renderiza una plantilla Twig con los datos proporcionados * @param template Contenido de la plantilla * @param data Datos para la plantilla * @returns HTML renderizado */ renderTemplate(template, data) { return new Promise((resolve, reject) => { this.twig.renderString(template, data, (err, html) => { if (err) { reject(new PDFError(`Error al renderizar la plantilla: ${err.message}`)); } else { resolve(html); } }); }); } /** * Convierte HTML a PDF * @param html Contenido HTML * @param data Datos del documento * @returns Resultado de la generación del PDF */ async htmlToPdf(html, data) { // Nota: Esta es una implementación simplificada. // En un caso real, se utilizaría una biblioteca como puppeteer o wkhtmltopdf // para convertir HTML a PDF con mayor fidelidad. // Crear un nuevo documento PDF const doc = new pdfkit_1.default({ size: this.options.paper, layout: this.options.orientation, margin: this.options.margins?.top, info: { Title: `${data.tipoDocumentoDesc} ${data.serie}-${data.correlativo}`, Author: data.emisor.razonSocial, Subject: `${data.tipoDocumentoDesc} ${data.serie}-${data.correlativo}`, Keywords: 'factura, electrónica, sunat, perú', Creator: 'NodeFact', Producer: 'NodeFact', }, }); // Configurar el destino del PDF let pdfBuffer = null; const buffers = []; if (this.options.outputPath) { // Crear el directorio si no existe const dir = path.dirname(this.options.outputPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // Escribir el PDF a un archivo doc.pipe(fs.createWriteStream(this.options.outputPath)); } else { // Escribir el PDF a un buffer doc.on('data', buffers.push.bind(buffers)); doc.on('end', () => { pdfBuffer = Buffer.concat(buffers); }); } // Agregar contenido al PDF // Esto es una simplificación. En un caso real, se procesaría el HTML // y se convertiría a comandos PDFKit. doc.fontSize(this.options.fontSize?.title || 16) .text(`${data.tipoDocumentoDesc} ${data.serie}-${data.correlativo}`, { align: 'center', }) .moveDown(); doc.fontSize(this.options.fontSize?.normal || 10) .text(`Emisor: ${data.emisor.razonSocial}`) .text(`RUC: ${data.emisor.ruc}`) .text(`Dirección: ${data.emisor.direccion.direccion}`) .moveDown(); doc.text(`Receptor: ${data.receptor.razonSocial}`) .text(`${data.receptor.tipoDocumentoDesc}: ${data.receptor.numeroDocumento}`) .moveDown(); doc.text(`Fecha de emisión: ${data.fechaEmision}`) .moveDown(); // Finalizar el documento doc.end(); // Esperar a que se complete la generación del PDF if (!this.options.outputPath) { await new Promise((resolve) => { doc.on('end', () => { resolve(); }); }); } return { success: true, path: this.options.outputPath, buffer: pdfBuffer || undefined, }; } } exports.PDFGenerator = PDFGenerator; /** * Genera un PDF a partir de los datos de un documento electrónico * @param data Datos del documento electrónico * @param options Opciones de generación del PDF * @param templateType Tipo de plantilla a utilizar * @returns Resultado de la generación del PDF */ async function generatePdf(data, options = {}, templateType) { const generator = new PDFGenerator(options); return await generator.generate(data, templateType); }