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
JavaScript
;
/**
* 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);
}