/** * ============================================================ * EXPEDIENTES DE FINIQUITOS - GUATEMALA * Apps Script completo para: * - Leer el Consolidado. * - Validar el estatus original. * - Generar PDFs individuales. * - Crear y mantener CONTROL_ENVIOS. * - Procesar todos, por lotes, por nombre o por fila. * - Regenerar el caso especial Lilian Jeannette / L43. * ============================================================ */ const CONFIG_PRESTACIONES = Object.freeze({ HOJA_CONSOLIDADO: 'Consolidado', HOJA_CONTROL: 'CONTROL_ENVIOS', PREFIJO_HOJAS_EMPLEADO: 'L', CARPETA_SALIDA: 'PDF Prestaciones P&G - Generados', TAMANO_LOTE: 10, PROPIEDAD_INDICE_LOTE: 'PRESTACIONES_INDICE_LOTE', ESTADO_LISTO: 'LISTO_PARA_ENVIAR', ESTADO_PENDIENTE: 'PENDIENTE_VALIDACION', ESTADO_ERROR: 'ERROR_GENERACION', ESTADO_ENVIADO: 'ENVIADO', ENCABEZADOS_CONTROL: [ 'NO.', 'Hoja', 'Nombre', 'Email personal', 'Estatus original', 'PDF File ID', 'PDF URL', 'Estado envío', 'Fecha generación', 'Observación', 'Fecha envío', 'Gmail ID', 'Última actualización' ] }); /** * Crea el menú del proyecto al abrir el Google Sheet. */ function onOpen() { SpreadsheetApp.getUi() .createMenu('Finiquitos Guatemala') .addItem( 'Generar todos los PDFs', 'generarPDFsPrestaciones' ) .addItem( 'Generar siguiente lote', 'generarPDFsPrestacionesPorLotes' ) .addItem( 'Reiniciar generación por lotes', 'reiniciarGeneracionPorLotes' ) .addSeparator() .addItem( 'Regenerar PDF por nombre', 'regenerarPDFPrestacionesPorNombre' ) .addItem( 'Regenerar PDF de la fila activa', 'regenerarPDFPrestacionesFilaActiva' ) .addSeparator() .addItem( 'Regenerar Lilian L43', 'regenerarPDFLilianL43' ) .addToUi(); } /** * Genera o regenera todos los registros válidos del Consolidado. * * Reglas: * - Aprobada: genera PDF. * - No aprobada: no genera PDF y queda PENDIENTE_VALIDACION. * - Sin email válido: genera PDF, pero queda PENDIENTE_VALIDACION. * - Irma / L29: genera PDF, pero nunca queda listo automáticamente. */ function generarPDFsPrestaciones() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const contexto = prepararContextoPrestaciones_(ss); const resultado = procesarFilasPrestaciones_( contexto, 0, contexto.filas.length ); SpreadsheetApp.getUi().alert( 'Generación terminada', construirResumen_(resultado), SpreadsheetApp.getUi().ButtonSet.OK ); } /** * Procesa los registros por lotes para evitar el límite * de tiempo máximo de ejecución de Apps Script. */ function generarPDFsPrestacionesPorLotes() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const contexto = prepararContextoPrestaciones_(ss); const propiedades = PropertiesService.getScriptProperties(); let indiceInicial = Number( propiedades.getProperty( CONFIG_PRESTACIONES.PROPIEDAD_INDICE_LOTE ) || 0 ); if ( !Number.isFinite(indiceInicial) || indiceInicial < 0 || indiceInicial >= contexto.filas.length ) { indiceInicial = 0; } const indiceFinal = Math.min( indiceInicial + CONFIG_PRESTACIONES.TAMANO_LOTE, contexto.filas.length ); const resultado = procesarFilasPrestaciones_( contexto, indiceInicial, indiceFinal ); if (indiceFinal >= contexto.filas.length) { propiedades.deleteProperty( CONFIG_PRESTACIONES.PROPIEDAD_INDICE_LOTE ); } else { propiedades.setProperty( CONFIG_PRESTACIONES.PROPIEDAD_INDICE_LOTE, String(indiceFinal) ); } const mensajeContinuacion = indiceFinal >= contexto.filas.length ? '\n\nSe completaron todas las filas.' : ( '\n\nPróximo lote: registros internos ' + `${indiceFinal + 1} a ` + `${Math.min( indiceFinal + CONFIG_PRESTACIONES.TAMANO_LOTE, contexto.filas.length )}.` ); SpreadsheetApp.getUi().alert( 'Lote procesado', construirResumen_(resultado) + mensajeContinuacion, SpreadsheetApp.getUi().ButtonSet.OK ); } /** * Reinicia el avance guardado de la generación por lotes. */ function reiniciarGeneracionPorLotes() { PropertiesService .getScriptProperties() .deleteProperty( CONFIG_PRESTACIONES.PROPIEDAD_INDICE_LOTE ); SpreadsheetApp.getUi().alert( 'El contador de generación por lotes fue reiniciado.' ); } /** * Solicita el nombre del colaborador y regenera solo su caso. */ function regenerarPDFPrestacionesPorNombre() { const ui = SpreadsheetApp.getUi(); const respuesta = ui.prompt( 'Regenerar PDF', 'Escribe el nombre del colaborador tal como aparece en Consolidado:', ui.ButtonSet.OK_CANCEL ); if (respuesta.getSelectedButton() !== ui.Button.OK) { return; } const nombreBuscado = normalizarTexto_( respuesta.getResponseText() ); if (!nombreBuscado) { ui.alert('Debes escribir un nombre.'); return; } const ss = SpreadsheetApp.getActiveSpreadsheet(); const contexto = prepararContextoPrestaciones_(ss); const indice = contexto.filas.findIndex( fila => normalizarTexto_(fila.nombre) === nombreBuscado ); if (indice === -1) { ui.alert( 'No encontré ese nombre en la hoja Consolidado.' ); return; } const resultado = procesarFilasPrestaciones_( contexto, indice, indice + 1 ); ui.alert( 'Regeneración terminada', construirResumen_(resultado), ui.ButtonSet.OK ); } /** * Regenera el caso asociado a la fila seleccionada * en la hoja Consolidado. */ function regenerarPDFPrestacionesFilaActiva() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const hojaActiva = ss.getActiveSheet(); if ( hojaActiva.getName() !== CONFIG_PRESTACIONES.HOJA_CONSOLIDADO ) { SpreadsheetApp.getUi().alert( `Selecciona primero una fila en la hoja ${CONFIG_PRESTACIONES.HOJA_CONSOLIDADO}.` ); return; } const rangoActivo = hojaActiva.getActiveRange(); if (!rangoActivo) { SpreadsheetApp.getUi().alert( 'Selecciona una fila de empleado.' ); return; } const filaActiva = rangoActivo.getRow(); if (filaActiva <= 1) { SpreadsheetApp.getUi().alert( 'Selecciona una fila de empleado, no el encabezado.' ); return; } const contexto = prepararContextoPrestaciones_(ss); const indice = contexto.filas.findIndex( fila => fila.numeroFila === filaActiva ); if (indice === -1) { SpreadsheetApp.getUi().alert( 'La fila seleccionada no contiene un empleado válido.' ); return; } const resultado = procesarFilasPrestaciones_( contexto, indice, indice + 1 ); SpreadsheetApp.getUi().alert( 'Regeneración terminada', construirResumen_(resultado), SpreadsheetApp.getUi().ButtonSet.OK ); } /** * Prepara el contexto completo del proyecto: * - Consolidado. * - Empleados. * - CONTROL_ENVIOS. * - Mapa de columnas. * - Mapa de filas existentes. * - Carpeta de salida. */ function prepararContextoPrestaciones_(ss) { const hojaConsolidado = ss.getSheetByName( CONFIG_PRESTACIONES.HOJA_CONSOLIDADO ); if (!hojaConsolidado) { throw new Error( `No existe la hoja ${CONFIG_PRESTACIONES.HOJA_CONSOLIDADO}.` ); } const valores = hojaConsolidado .getDataRange() .getDisplayValues(); if (valores.length < 2) { throw new Error( 'La hoja Consolidado no contiene empleados.' ); } const encabezados = valores[0]; const colNo = buscarColumna_(encabezados, [ 'NO.', 'NO', 'No.', 'No' ]); const colNombre = buscarColumna_(encabezados, [ 'Nombre', 'NOMBRE', 'Colaborador', 'Empleado' ]); const colEmail = buscarColumna_(encabezados, [ 'Email personal', 'Email Personal', 'Correo personal', 'Correo Personal', 'Email', 'Correo' ], false); const colEstatus = buscarColumna_(encabezados, [ 'Estatus original', 'Estatus Original', 'Estatus', 'Estado', 'Status' ]); if (colNo === -1) { throw new Error( 'No encontré la columna NO. en Consolidado.' ); } if (colNombre === -1) { throw new Error( 'No encontré la columna Nombre en Consolidado.' ); } if (colEstatus === -1) { throw new Error( 'No encontré la columna Estatus en Consolidado.' ); } const filas = []; for (let indice = 1; indice < valores.length; indice++) { const fila = valores[indice]; const no = normalizarNumeroCaso_(fila[colNo]); const nombre = String(fila[colNombre] || '') .replace(/\s+/g, ' ') .trim(); const email = colEmail >= 0 ? String(fila[colEmail] || '') .trim() .toLowerCase() : ''; const estatusOriginal = String( fila[colEstatus] || '' ) .replace(/\s+/g, ' ') .trim(); if (!no || !nombre) { continue; } if (normalizarTexto_(nombre) === 'total') { continue; } filas.push({ numeroFila: indice + 1, no, hoja: `${CONFIG_PRESTACIONES.PREFIJO_HOJAS_EMPLEADO}${no}`, nombre, email, estatusOriginal }); } if (!filas.length) { throw new Error( 'No encontré filas válidas para procesar.' ); } const hojaControl = asegurarHojaControl_(ss); const columnasControl = obtenerColumnasControl_(hojaControl); const mapaControl = construirMapaControl_( hojaControl, columnasControl ); const carpeta = obtenerOCrearCarpeta_( ss, CONFIG_PRESTACIONES.CARPETA_SALIDA ); return { ss, hojaConsolidado, hojaControl, columnasControl, mapaControl, carpeta, filas }; } /** * Procesa un tramo del arreglo de empleados. */ function procesarFilasPrestaciones_( contexto, indiceInicial, indiceFinal ) { const resultado = { procesados: 0, listos: 0, pendientes: 0, noAprobados: 0, errores: 0, detalleErrores: [] }; for ( let indice = indiceInicial; indice < indiceFinal; indice++ ) { const empleado = contexto.filas[indice]; try { if (!esEstatusAprobado_(empleado.estatusOriginal)) { actualizarControl_(contexto, { ...empleado, pdfFileId: '', pdfUrl: '', estado: CONFIG_PRESTACIONES.ESTADO_PENDIENTE, observacion: `No se generó PDF porque el Estatus original es "${empleado.estatusOriginal || 'vacío'}".`, fechaGeneracion: '' }); resultado.procesados++; resultado.pendientes++; resultado.noAprobados++; continue; } const hojaEmpleado = contexto.ss.getSheetByName( empleado.hoja ); if (!hojaEmpleado) { throw new Error( `No existe la hoja ${empleado.hoja}.` ); } SpreadsheetApp.flush(); Utilities.sleep(500); const nombreArchivo = limpiarNombreArchivo_( `Prestaciones Laborales - ${empleado.nombre}.pdf` ); const blob = exportarHojaComoPDF_( contexto.ss, hojaEmpleado, nombreArchivo ); const archivo = guardarOReemplazarArchivo_( contexto.carpeta, nombreArchivo, blob ); const esCasoEspecial = esCasoEspecial_(empleado); const tieneEmail = validarEmail_(empleado.email); let estado = CONFIG_PRESTACIONES.ESTADO_LISTO; let observacion = `PDF generado correctamente desde ${empleado.hoja}.`; if (esCasoEspecial) { estado = CONFIG_PRESTACIONES.ESTADO_PENDIENTE; observacion = `PDF generado desde ${empleado.hoja}. Caso excluido del envío automático; requiere validación manual.`; } else if (!tieneEmail) { estado = CONFIG_PRESTACIONES.ESTADO_PENDIENTE; observacion = `PDF generado desde ${empleado.hoja}, pero falta o es inválido el Email personal.`; } actualizarControl_(contexto, { ...empleado, pdfFileId: archivo.getId(), pdfUrl: archivo.getUrl(), estado, observacion, fechaGeneracion: new Date() }); resultado.procesados++; if ( estado === CONFIG_PRESTACIONES.ESTADO_LISTO ) { resultado.listos++; } else { resultado.pendientes++; } } catch (error) { resultado.errores++; resultado.detalleErrores.push( `${empleado.hoja} - ${empleado.nombre}: ${error.message}` ); actualizarControl_(contexto, { ...empleado, pdfFileId: '', pdfUrl: '', estado: CONFIG_PRESTACIONES.ESTADO_ERROR, observacion: `Error generando PDF: ${error.message}`, fechaGeneracion: new Date() }); } } return resultado; } /** * Crea CONTROL_ENVIOS si no existe. * * Si la hoja ya existe: * - No cambia el orden actual. * - No sobrescribe encabezados existentes. * - Solo agrega al final cualquier encabezado faltante. */ function asegurarHojaControl_(ss) { let hoja = ss.getSheetByName( CONFIG_PRESTACIONES.HOJA_CONTROL ); if (!hoja) { hoja = ss.insertSheet( CONFIG_PRESTACIONES.HOJA_CONTROL ); } const ultimaColumna = hoja.getLastColumn(); if (ultimaColumna === 0) { hoja .getRange( 1, 1, 1, CONFIG_PRESTACIONES .ENCABEZADOS_CONTROL .length ) .setValues([ CONFIG_PRESTACIONES .ENCABEZADOS_CONTROL ]); } else { const encabezadosActuales = hoja .getRange(1, 1, 1, ultimaColumna) .getDisplayValues()[0] .map(valor => String(valor || '').trim()); const normalizados = new Set( encabezadosActuales.map( encabezado => normalizarEncabezado_(encabezado) ) ); const faltantes = CONFIG_PRESTACIONES .ENCABEZADOS_CONTROL .filter( encabezado => !normalizados.has( normalizarEncabezado_(encabezado) ) ); if (faltantes.length) { hoja .getRange( 1, ultimaColumna + 1, 1, faltantes.length ) .setValues([faltantes]); } } const totalColumnas = hoja.getLastColumn(); hoja .getRange(1, 1, 1, totalColumnas) .setFontWeight('bold') .setBackground('#4F758B') .setFontColor('#FFFFFF') .setHorizontalAlignment('center'); hoja.setFrozenRows(1); hoja.autoResizeColumns(1, totalColumnas); return hoja; } /** * Obtiene los índices reales de las columnas de CONTROL_ENVIOS. * Los índices devueltos son 1-based. */ function obtenerColumnasControl_(hojaControl) { const ultimaColumna = hojaControl.getLastColumn(); if (ultimaColumna < 1) { throw new Error( 'CONTROL_ENVIOS no contiene encabezados.' ); } const encabezados = hojaControl .getRange(1, 1, 1, ultimaColumna) .getDisplayValues()[0] .map(valor => String(valor || '').trim()); const obtener = ( nombres, etiqueta, obligatoria = true ) => { const indice = buscarColumna_( encabezados, nombres, obligatoria ); if (indice === -1 && obligatoria) { throw new Error( `No encontré la columna ${etiqueta} en CONTROL_ENVIOS.` ); } return indice >= 0 ? indice + 1 : 0; }; return { no: obtener( ['NO.', 'NO', 'No'], 'NO.' ), hoja: obtener( ['Hoja'], 'Hoja' ), nombre: obtener( ['Nombre'], 'Nombre' ), email: obtener( [ 'Email personal', 'Email Personal', 'Correo personal' ], 'Email personal' ), estatusOriginal: obtener( [ 'Estatus original', 'Estatus Original' ], 'Estatus original' ), pdfFileId: obtener( ['PDF File ID'], 'PDF File ID' ), pdfUrl: obtener( ['PDF URL'], 'PDF URL' ), estadoEnvio: obtener( [ 'Estado envío', 'Estado Envio' ], 'Estado envío' ), fechaGeneracion: obtener( [ 'Fecha generación', 'Fecha generacion' ], 'Fecha generación' ), observacion: obtener( [ 'Observación', 'Observacion' ], 'Observación' ), fechaEnvio: obtener( [ 'Fecha envío', 'Fecha envio' ], 'Fecha envío', false ), gmailId: obtener( ['Gmail ID'], 'Gmail ID', false ), ultimaActualizacion: obtener( [ 'Última actualización', 'Ultima actualizacion' ], 'Última actualización', false ) }; } /** * Construye un mapa para encontrar rápidamente * una fila de CONTROL_ENVIOS por NO. o por Hoja. */ function construirMapaControl_( hojaControl, columnas ) { const ultimaFila = hojaControl.getLastRow(); const mapa = new Map(); if (ultimaFila < 2) { return mapa; } const ultimaColumna = hojaControl.getLastColumn(); const valores = hojaControl .getRange( 2, 1, ultimaFila - 1, ultimaColumna ) .getDisplayValues(); valores.forEach((fila, indice) => { const no = normalizarNumeroCaso_( fila[columnas.no - 1] ); const hoja = String( fila[columnas.hoja - 1] || '' ).trim(); if (no) { mapa.set( `NO:${no}`, indice + 2 ); } if (hoja) { mapa.set( `HOJA:${normalizarTexto_(hoja)}`, indice + 2 ); } }); return mapa; } /** * Inserta o actualiza la fila correspondiente * en CONTROL_ENVIOS sin alterar otras columnas. */ function actualizarControl_(contexto, datos) { const claveNo = `NO:${datos.no}`; const claveHoja = `HOJA:${normalizarTexto_(datos.hoja)}`; let filaControl = contexto.mapaControl.get(claveNo) || contexto.mapaControl.get(claveHoja) || 0; if (!filaControl) { filaControl = Math.max( contexto.hojaControl.getLastRow() + 1, 2 ); } const ultimaColumna = contexto.hojaControl.getLastColumn(); const rango = contexto.hojaControl.getRange( filaControl, 1, 1, ultimaColumna ); const existente = rango.getValues()[0]; const columnas = contexto.columnasControl; const valorExistente = columna => columna > 0 ? existente[columna - 1] : ''; const estadoExistente = String( valorExistente(columnas.estadoEnvio) || '' ) .trim() .toUpperCase(); let estadoFinal = datos.estado; let observacionFinal = datos.observacion; if ( estadoExistente === CONFIG_PRESTACIONES.ESTADO_ENVIADO && !datos.forzarEstado ) { estadoFinal = CONFIG_PRESTACIONES.ESTADO_ENVIADO; observacionFinal = `${datos.observacion} El registro ya estaba marcado como ENVIADO y se conservó ese estado.`; } const valores = existente.slice(); const asignar = (columna, valor) => { if (columna > 0) { valores[columna - 1] = valor; } }; asignar( columnas.no, datos.no ); asignar( columnas.hoja, datos.hoja ); asignar( columnas.nombre, datos.nombre ); asignar( columnas.email, datos.email || '' ); asignar( columnas.estatusOriginal, datos.estatusOriginal || '' ); asignar( columnas.pdfFileId, datos.pdfFileId || '' ); asignar( columnas.pdfUrl, datos.pdfUrl || '' ); asignar( columnas.estadoEnvio, estadoFinal ); asignar( columnas.fechaGeneracion, datos.fechaGeneracion || '' ); asignar( columnas.observacion, observacionFinal || '' ); if (columnas.ultimaActualizacion > 0) { asignar( columnas.ultimaActualizacion, new Date() ); } rango.setValues([valores]); aplicarFormatoFechasControl_( contexto.hojaControl, filaControl, columnas ); contexto.mapaControl.set( claveNo, filaControl ); contexto.mapaControl.set( claveHoja, filaControl ); } /** * Formatea las columnas de fecha disponibles * en CONTROL_ENVIOS. */ function aplicarFormatoFechasControl_( hojaControl, fila, columnas ) { const columnasFecha = [ columnas.fechaGeneracion, columnas.fechaEnvio, columnas.ultimaActualizacion ].filter(columna => columna > 0); columnasFecha.forEach(columna => { hojaControl .getRange(fila, columna) .setNumberFormat( 'yyyy-mm-dd hh:mm:ss' ); }); } /** * Exporta una sola hoja a PDF. */ function exportarHojaComoPDF_( spreadsheet, hoja, nombreArchivo ) { const parametros = [ 'format=pdf', 'size=letter', 'portrait=true', 'fitw=true', 'sheetnames=false', 'printtitle=false', 'pagenumbers=false', 'gridlines=false', 'fzr=false', 'attachment=true', 'top_margin=0.25', 'bottom_margin=0.25', 'left_margin=0.25', 'right_margin=0.25', `gid=${hoja.getSheetId()}` ].join('&'); const url = `https://docs.google.com/spreadsheets/d/${spreadsheet.getId()}/export?${parametros}`; const respuesta = UrlFetchApp.fetch( url, { method: 'get', headers: { Authorization: `Bearer ${ScriptApp.getOAuthToken()}` }, muteHttpExceptions: true } ); const codigo = respuesta.getResponseCode(); if (codigo !== 200) { throw new Error( `Google devolvió HTTP ${codigo} al exportar ${hoja.getName()} como PDF.` ); } return respuesta .getBlob() .setContentType('application/pdf') .setName(nombreArchivo); } /** * Busca o crea la carpeta de salida * dentro de la misma carpeta del Google Sheet. */ function obtenerOCrearCarpeta_( spreadsheet, nombreCarpeta ) { const archivoSpreadsheet = DriveApp.getFileById( spreadsheet.getId() ); const padres = archivoSpreadsheet.getParents(); const carpetaPadre = padres.hasNext() ? padres.next() : DriveApp.getRootFolder(); const carpetas = carpetaPadre.getFoldersByName( nombreCarpeta ); if (carpetas.hasNext()) { return carpetas.next(); } return carpetaPadre.createFolder( nombreCarpeta ); } /** * Elimina de Drive las versiones anteriores * con el mismo nombre y crea el archivo nuevo. */ function guardarOReemplazarArchivo_( carpeta, nombreArchivo, blob ) { const anteriores = carpeta.getFilesByName( nombreArchivo ); while (anteriores.hasNext()) { anteriores .next() .setTrashed(true); } return carpeta .createFile(blob) .setName(nombreArchivo); } /** * Busca una columna ignorando: * - Mayúsculas. * - Minúsculas. * - Tildes. * - Espacios. * - Signos. * * Devuelve un índice 0-based. */ function buscarColumna_( encabezados, nombresPosibles, obligatoria = true ) { const normalizados = encabezados.map( encabezado => normalizarEncabezado_(encabezado) ); for (const nombre of nombresPosibles) { const indice = normalizados.indexOf( normalizarEncabezado_(nombre) ); if (indice !== -1) { return indice; } } return obligatoria ? -1 : -1; } /** * Normaliza encabezados. */ function normalizarEncabezado_(valor) { return String(valor ?? '') .trim() .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9]/g, ''); } /** * Limpia un nombre para usarlo como archivo. */ function limpiarNombreArchivo_(nombre) { return String(nombre || '') .replace(/[\\/:*?"<>|]/g, '-') .replace(/[\u0000-\u001F\u007F]/g, '') .replace(/\s+/g, ' ') .trim() .slice(0, 180); } /** * Normaliza valores de NO. como: * - 1 * - 1.0 * - "1" */ function normalizarNumeroCaso_(valor) { const texto = String(valor ?? '').trim(); if (!texto) { return ''; } const numero = Number( texto.replace(',', '.') ); if (Number.isFinite(numero)) { return String( Math.trunc(numero) ); } return texto.replace(/\.0+$/, ''); } /** * Normalización general de texto. */ function normalizarTexto_(valor) { return String(valor ?? '') .trim() .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9]/g, ''); } /** * Indica si el estatus permite generar PDF. */ function esEstatusAprobado_(estatus) { const normalizado = normalizarTexto_(estatus); return ( normalizado === 'aprobada' || normalizado === 'aprobado' || normalizado === 'approved' ); } /** * Validación básica de correo. */ function validarEmail_(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/i.test( String(email || '').trim() ); } /** * Casos que nunca deben quedar listos * para envío automático. */ function esCasoEspecial_(empleado) { const nombre = normalizarTexto_(empleado.nombre); return ( nombre.includes( 'irmaalejandraventura' ) || String(empleado.hoja || '') .toUpperCase() === 'L29' ); } /** * Construye el resumen mostrado al finalizar. */ function construirResumen_(resultado) { let mensaje = `Procesados: ${resultado.procesados}\n` + `Listos para enviar: ${resultado.listos}\n` + `Pendientes de validación: ${resultado.pendientes}\n` + `No aprobados: ${resultado.noAprobados}\n` + `Errores: ${resultado.errores}`; if (resultado.detalleErrores.length) { mensaje += '\n\nDetalle de errores:\n' + resultado.detalleErrores .slice(0, 10) .join('\n'); if ( resultado.detalleErrores.length > 10 ) { mensaje += `\n... y ${resultado.detalleErrores.length - 10} error(es) adicional(es).`; } } return mensaje; } /** * ============================================================ * REGENERACIÓN ESPECIAL DE LILIAN JEANNETTE / L43 * ============================================================ * * Esta función: * - Exporta la pestaña L43 nueva/modificada. * - Reemplaza el PDF anterior con el mismo nombre. * - Marca Estatus original = Aprobada. * - Marca Estado envío = LISTO_PARA_ENVIAR. */ function regenerarPDFLilianL43() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const nombreHojaCaso = 'L43'; const nombreEmpleado = 'Lilian Jeannette Revolorio Perez'; const hojaCaso = ss.getSheetByName(nombreHojaCaso); if (!hojaCaso) { throw new Error( 'No existe la hoja L43. Revisa que la pestaña nueva se llame exactamente L43.' ); } const contexto = prepararContextoPrestaciones_(ss); const empleadoConsolidado = contexto.filas.find( empleado => normalizarNumeroCaso_( empleado.no ) === '43' || normalizarTexto_( empleado.hoja ) === 'l43' || normalizarTexto_( empleado.nombre ) === normalizarTexto_( nombreEmpleado ) ); const filaControlExistente = contexto.mapaControl.get('NO:43') || contexto.mapaControl.get('HOJA:l43') || 0; let email = empleadoConsolidado ? empleadoConsolidado.email : ''; if ( !email && filaControlExistente ) { email = String( contexto.hojaControl .getRange( filaControlExistente, contexto.columnasControl.email ) .getDisplayValue() || '' ) .trim() .toLowerCase(); } if (!email) { throw new Error( 'La fila de Lilian no tiene Email personal.' ); } SpreadsheetApp.flush(); Utilities.sleep(500); const nombrePDF = `${nombreHojaCaso} - ${limpiarNombreArchivo_(nombreEmpleado)}.pdf`; const pdfBlob = exportarHojaComoPDFL43_( ss.getId(), hojaCaso.getSheetId(), nombrePDF ); const archivoPDF = guardarOReemplazarArchivo_( contexto.carpeta, nombrePDF, pdfBlob ); actualizarControl_(contexto, { no: '43', hoja: nombreHojaCaso, nombre: nombreEmpleado, email, estatusOriginal: 'Aprobada', pdfFileId: archivoPDF.getId(), pdfUrl: archivoPDF.getUrl(), estado: CONFIG_PRESTACIONES.ESTADO_LISTO, observacion: 'PDF regenerado desde la pestaña L43 nueva/modificada. Listo para envío.', fechaGeneracion: new Date(), forzarEstado: true }); contexto.hojaControl.autoResizeColumns( 1, contexto.hojaControl.getLastColumn() ); SpreadsheetApp.getUi().alert( 'Listo. Se regeneró el PDF de Lilian L43 y se actualizó CONTROL_ENVIOS.' ); } /** * Exportación especial usada por L43. */ function exportarHojaComoPDFL43_( spreadsheetId, sheetId, nombrePDF ) { const urlBase = `https://docs.google.com/spreadsheets/d/${spreadsheetId}/export`; const parametros = { format: 'pdf', gid: sheetId, size: 'letter', portrait: 'true', scale: '4', sheetnames: 'false', printtitle: 'false', pagenumbers: 'false', gridlines: 'false', fzr: 'false', top_margin: '0.25', bottom_margin: '0.25', left_margin: '0.25', right_margin: '0.25', horizontal_alignment: 'CENTER', vertical_alignment: 'TOP' }; const queryString = Object.keys(parametros) .map( key => `${key}=${encodeURIComponent(parametros[key])}` ) .join('&'); const url = `${urlBase}?${queryString}`; const respuesta = UrlFetchApp.fetch( url, { headers: { Authorization: `Bearer ${ScriptApp.getOAuthToken()}` }, muteHttpExceptions: true } ); const codigo = respuesta.getResponseCode(); if (codigo !== 200) { throw new Error( `Error exportando PDF de L43. Código HTTP: ${codigo}` ); } return respuesta .getBlob() .setContentType('application/pdf') .setName(nombrePDF); }