Files
finiquitos-guatemala/Generar PDFs.js
T
2026-07-27 16:48:02 +00:00

1535 lines
30 KiB
JavaScript

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