Cómo Crear un Generador de PDFs con Google Sheets, Docs y Apps Script
Esta aplicación interactiva te guía a través del tutorial completo para automatizar la creación de documentos. En lugar de un artículo estático, puedes navegar por cada paso, ver el código en pestañas y copiarlo fácilmente para construir tu propia herramienta.
¿Qué Vamos a Construir?
Crearemos una pequeña página web (privada para ti) que:
- ✔Lee los datos de tu Google Sheet.
- ✔Muestra cada fila con un botón de "Generar".
- ✔Toma tu plantilla de Google Docs y combina los datos.
- ✔Crea un Google Doc y un PDF en tu Drive.
- ✔Envía el PDF por correo electrónico.
- ✔Guarda el enlace del nuevo doc en tu Google Sheet.
Paso 1: Prepara tus Archivos en Google Drive
Antes de tocar una línea de código, necesitamos organizar nuestros tres archivos fuente en Google Drive. Esta sección detalla cómo configurar tu Hoja de Cálculo, tu Plantilla de Documento y tu Carpeta de destino.
Crea una nueva Hoja de Cálculo. En la primera fila, pon tus encabezados (ej. `Nombre`, `Email`, `Nombre del Curso`).
Importante: Añade una columna al final llamada `Link Documento`. Nuestro script la usará para pegar el enlace del archivo generado.
Crea un nuevo Documento de Google. Escribe tu texto y usa "placeholders" que coincidan **exactamente** con tus encabezados, pero entre llaves dobles.
Hola, {{Nombre}}. Te confirmamos tu inscripción al curso {{Nombre del Curso}}.
Crea una nueva carpeta en tu Google Drive (ej. "Documentos Generados"). Aquí es donde se guardarán todos los nuevos Docs y PDFs.
Necesitamos identificar estos 3 archivos. Ve a cada uno y copia su ID desde la barra de direcciones (URL). ¡Guárdalos!
- Google Sheet ID: `.../spreadsheets/d/``[ID_AQUÍ]``/edit...`
- Google Doc ID (Plantilla): `.../document/d/``[ID_AQUÍ]``/edit...`
- Google Drive ID (Carpeta): `.../folders/``[ID_AQUÍ]`
Paso 2: El Código Mágico (Apps Script)
Este es el corazón de la aplicación. Crearemos 3 archivos en tu proyecto de Apps Script. Usa las pestañas de abajo para ver el código de cada archivo y el botón Copiar para pegarlo fácilmente en tu editor.
// ---------------------------------------------
// CONFIGURACIÓN: ¡RELLENA ESTAS VARIABLES!
// ---------------------------------------------
const SHEET_ID = 'ID_DE_TU_GOOGLE_SHEET'; // <-- PEGA TU ID
const SHEET_NAME = 'Hoja 1'; // <-- Cambia esto si tu hoja tiene otro nombre
const TEMPLATE_ID = 'ID_DE_TU_PLANTILLA_DOC'; // <-- PEGA TU ID
const FOLDER_ID = 'ID_DE_TU_CARPETA_DRIVE'; // <-- PEGA TU ID
// --- Nombres de las columnas (Deben coincidir exacto) ---
const EMAIL_COLUMN_NAME = 'Email'; // Columna con el email del destinatario
const STATUS_COLUMN_NAME = 'Link Documento'; // Columna donde se guardará el link
// ---------------------------------------------
/**
* Sirve la aplicación web
*/
function doGet(e) {
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('Generador de Documentos PDF')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.DEFAULT);
}
/**
* Permite incluir otros archivos HTML (como el CSS)
*/
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
/**
* Obtiene todos los datos de la hoja de cálculo y los envía al frontend
*/
function getSheetData() {
try {
const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME);
const data = sheet.getDataRange().getValues();
return data;
} catch (e) {
Logger.log(e);
return []; // Devuelve array vacío en caso de error
}
}
/**
* La función principal que hace todo el trabajo.
* Se llama desde el frontend pasando el índice de la fila (base 0).
*/
function generateAndSendPdf(rowIndex) {
const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME);
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
const dataRow = sheet.getRange(rowIndex + 2, 1, 1, sheet.getLastColumn()).getValues()[0]; // +2 porque las filas son 1-index y la fila 1 es de headers
// Encontrar los índices de las columnas clave
const emailColumnIndex = headers.indexOf(EMAIL_COLUMN_NAME);
const statusColumnIndex = headers.indexOf(STATUS_COLUMN_NAME);
if (emailColumnIndex === -1 || statusColumnIndex === -1) {
return `Error: No se encontró la columna "${EMAIL_COLUMN_NAME}" o "${STATUS_COLUMN_NAME}". Revisa la CONFIGURACIÓN.`;
}
const email = dataRow[emailColumnIndex];
if (!email) {
return 'Error: No hay email en esta fila.';
}
try {
// 1. Copiar la plantilla
const templateFile = DriveApp.getFileById(TEMPLATE_ID);
const destinationFolder = DriveApp.getFolderById(FOLDER_ID);
const newFileName = `Documento - ${dataRow[0]} - ${new Date().toLocaleDateString()}`; // Asume que la col 0 es un nombre
const newFile = templateFile.makeCopy(newFileName, destinationFolder);
const doc = DocumentApp.openById(newFile.getId());
const body = doc.getBody();
// 2. Combinar datos (reemplazar placeholders)
headers.forEach((header, index) => {
body.replaceText(`{{${header}}}`, dataRow[index]);
});
// 3. Guardar y cerrar el Doc
doc.saveAndClose();
// 4. Crear el PDF
const pdfBlob = newFile.getAs('application/pdf').setName(doc.getName() + '.pdf');
// 5. Enviar el email
const subject = `Tu documento personalizado: ${doc.getName()}`;
const bodyEmail = 'Hola, \n\nAdjunto encontrarás tu documento personalizado.\n\nSaludos.';
MailApp.sendEmail(email, subject, bodyEmail, {
attachments: [pdfBlob]
});
// 6. Actualizar la hoja de cálculo con el link del DOC (no del PDF)
const docUrl = newFile.getUrl();
sheet.getRange(rowIndex + 2, statusColumnIndex + 1).setValue(docUrl); // +1 porque las columnas son 1-index
return `¡Enviado! Link guardado.`;
} catch (e) {
Logger.log(e);
return `Error: ${e.message}`;
}
}
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<?!= include('stylesheet'); ?>
</head>
<body>
<div class="container">
<h1><span role="img" aria-label="document">📄</span> Generador y Enviador de Documentos</h1>
<p>Estos son los datos de tu Google Sheet. Presiona "Generar" para procesar una fila.</p>
<button id="refreshButton" onclick="loadData()">Refrescar Datos</button>
<div id="status-global" class="status"></div>
<div class="table-container">
<table id="data-table">
<thead>
<!-- Las cabeceras se insertarán aquí -->
</thead>
<tbody>
<!-- Los datos se insertarán aquí -->
</tbody>
</table>
</div>
</div>
<script>
/**
* Se ejecuta cuando la página termina de cargar
*/
document.addEventListener('DOMContentLoaded', loadData);
/**
* Llama al backend para obtener los datos de la hoja
*/
function loadData() {
showGlobalStatus('Cargando datos...', 'loading');
google.script.run.withSuccessHandler(buildTable).withFailureHandler(showError).getSheetData();
}
/**
* Recibe los datos (array 2D) y construye la tabla HTML
*/
function buildTable(data) {
if (!data || data.length < 2) {
showGlobalStatus('No se encontraron datos o solo hay encabezados.', 'error');
return;
}
const table = document.getElementById('data-table');
const thead = table.querySelector('thead');
const tbody = table.querySelector('tbody');
// Limpiar tabla anterior
thead.innerHTML = '';
tbody.innerHTML = '';
// Crear encabezados
const headers = data.shift(); // Extrae la primera fila (headers)
let trHead = document.createElement('tr');
headers.forEach(header => {
trHead.innerHTML += `<th>${header}</th>`;
});
trHead.innerHTML += '<th>Acción</th>'; // Columna extra para el botón
thead.appendChild(trHead);
// Crear filas de datos
data.forEach((row, index) => {
let trBody = document.createElement('tr');
// Añadir celdas de datos
row.forEach(cell => {
trBody.innerHTML += `<td>${cell}</td>`;
});
// Añadir celda de botón y estado
trBody.innerHTML += `
<td class="action-cell">
<button id="btn-${index}" onclick="processRow(${index})">Generar y Enviar</button>
<div id="status-${index}" class="status"></div>
</td>`;
tbody.appendChild(trBody);
});
showGlobalStatus('Datos cargados.', 'success');
}
/**
* Llama al backend para procesar una fila específica
*/
function processRow(rowIndex) {
const button = document.getElementById(`btn-${rowIndex}`);
const statusEl = document.getElementById(`status-${rowIndex}`);
// Deshabilitar botón y mostrar carga
button.disabled = true;
statusEl.textContent = 'Procesando...';
statusEl.className = 'status loading';
// Llamar a la función del backend
google.script.run
.withSuccessHandler(function(response) {
// Esto se ejecuta cuando 'generateAndSendPdf' termina
statusEl.textContent = response;
if (response.startsWith('Error:')) {
statusEl.className = 'status error';
button.disabled = false; // Reactivar si hay error
} else {
statusEl.className = 'status success';
// Opcional: refrescar toda la tabla para ver el link
loadData();
}
})
.withFailureHandler(function(error) {
// Esto se ejecuta si la llamada falla
statusEl.textContent = `Error: ${error.message}`;
statusEl.className = 'status error';
button.disabled = false;
})
.generateAndSendPdf(rowIndex); // 'rowIndex' es el de base 0 del array de datos
}
function showGlobalStatus(message, type) {
const statusEl = document.getElementById('status-global');
statusEl.textContent = message;
statusEl.className = `status ${type}`;
}
function showError(error) {
showGlobalStatus(`Error al cargar: ${error.message}`, 'error');
}
</script>
</body>
</html>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f4f5f7;
margin: 0;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
h1 {
color: #172b4d;
border-bottom: 2px solid #0052cc;
padding-bottom: 10px;
}
p {
color: #42526e;
font-size: 1.1em;
}
#refreshButton {
background-color: #0052cc;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
margin-bottom: 20px;
}
#refreshButton:hover {
background-color: #0065ff;
}
.table-container {
width: 100%;
overflow-x: auto;
border: 1px solid #dfe1e6;
border-radius: 5px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #dfe1e6;
vertical-align: middle;
}
th {
background-color: #f4f5f7;
color: #5e6c84;
font-weight: 600;
}
tbody tr:last-child td {
border-bottom: none;
}
tbody tr:hover {
background-color: #f9f9fa;
}
button {
background-color: #0065ff;
color: white;
border: none;
padding: 8px 12px;
border-radius: 5px;
cursor: pointer;
font-weight: 500;
}
button:hover {
background-color: #0052cc;
}
button:disabled {
background-color: #a5adba;
cursor: not-allowed;
}
.action-cell {
min-width: 150px;
}
.status {
font-size: 0.9em;
margin-top: 5px;
}
.status.loading {
color: #5e6c84;
}
.status.success {
color: #006644;
font-weight: bold;
}
.status.error {
color: #bf2600;
font-weight: bold;
}
</style>
Paso 3: Autoriza e Implementa la App
Con el código en su sitio, solo quedan dos pasos: dar permiso al script para que acceda a tus servicios de Google y luego "publicarlo" como una aplicación web privada a la que puedas acceder desde una URL.
3A. Autorizar el Script
- En el editor de Apps Script, guarda todos los archivos (icono de disquete).
- En el menú "Seleccionar función", elige `doGet` y pulsa Ejecutar.
- La primera vez, Google te pedirá Revisar permisos.
- Elige tu cuenta, haz clic en Avanzado y luego en Ir a [Nombre de tu proyecto] (no seguro). (Es seguro, ¡es tu propio código!).
- Revisa los permisos (pedirá acceso a Docs, Sheets, Drive, Mail) y haz clic en Permitir.
3B. Implementar la Aplicación Web
- Haz clic en el botón azul Implementar (arriba a la derecha).
- Selecciona Nueva implementación.
- Junto a "Seleccionar tipo", elige Aplicación web.
- En la configuración:
- Ejecutar como: Elige Yo.
- Quién tiene acceso: Elige Solo yo.
- Haz clic en Implementar.
- ¡Listo! Apps Script te dará una URL de la aplicación web. ¡Cópiala!
Paso 4: ¡Usa tu Nueva Herramienta!
Este es el resultado. Abre la URL de la aplicación web que copiaste en el paso anterior. Esta es tu interfaz de control privada.
- Abre la URL de la aplicación web en tu navegador.
- Verás la tabla de tu Google Sheet cargada en la página.
- Haz clic en el botón "Generar y Enviar" de cualquier fila.
- Espera unos segundos... El estado cambiará a "¡Enviado! Link guardado."
- ¡Magia! Revisa tu Google Sheet (verás el nuevo link), tu carpeta de Drive (verás el Doc y el PDF) y el email del destinatario (tendrá el PDF adjunto).
Conclusión
¡Felicidades! Has construido una poderosa herramienta de automatización. Has transformado un proceso manual de copiar y pegar en una aplicación web funcional de un solo clic.
Puedes adaptar este script para cualquier cosa:
- Generar facturas mensuales.
- Enviar diplomas de cursos.
- Crear reportes personalizados.
- ...¡El límite es tu imaginación!