/** * Cotizador Walmart Connect · Google Workspace Worker * GomezLee Marketing · 2026 * * Versión corregida para Google Apps Script V8. * - NO usa variables de entorno. * - NO usa Script Properties. * - Gemini NO se llama desde Apps Script. * - Las imágenes llegan desde los nodos nativos de Google Gemini en n8n. */ const WMC_CONFIG = { GOOGLE_AUTOMATION_SECRET: 'mwI_n-1r4_-QTLp1-zIt-zQKh_HqaoGoVhyapyLN2T2-r7xf8iP5AC3v9wJt5Av6', OUTPUT_FOLDER_ID: '19EmZsJ7PzWCaFzeij9AZ3-mkPctNyK0G', WALMART_TEMPLATE_ID: '17f1MB1uXQiQk4jn4_MPQixpBRwCpwDMZTsRmWA9UMOA', GLM_MONTHLY_TEMPLATE_ID: '1RL9BckErp1QCLdxlM6riBRh9iO4f14ezf6fUgizBZJw', // Confirmado en los ejemplos GLM enviados (Guatemala y Costa Rica). GLM_VENDOR_NUMBER: '1603060802', GLM_IVA_RATE: 0.12 }; const WMC = { NAVY: '#041E42', GREEN: '#6CC24A', BLUE: '#4F758B', TEXT: '#4A4A4A', LIGHT: '#F5F5F5' }; /** * Health check sencillo: al abrir la URL /exec en el navegador debe devolver JSON. */ function doGet() { return json_({ ok: true, service: 'Cotizador WMC · Google Workspace Worker', version: '2026-08-23.3' }); } function doPost(e) { try { const body = JSON.parse((e && e.postData && e.postData.contents) || '{}'); requireSecret_(body.secret); const action = String(body.action || ''); let result; if (action === 'generate_quote') { result = generateWalmartQuote_(body); } else if (action === 'generate_monthly_glm') { result = generateMonthlyGlm_(body); } else if (action === 'generate_proposal') { result = generateProposal_(body); } else if (action === 'generate_image') { result = generateImageArtifact_(body); } else { result = { ok: true, skipped: true, message: 'Acción sin artefacto.' }; } return json_(result); } catch (err) { console.error(err && err.stack ? err.stack : err); return json_({ ok: false, error: String(err && err.message ? err.message : err) }); } } function requireSecret_(value) { if (!WMC_CONFIG.GOOGLE_AUTOMATION_SECRET || String(value || '') !== WMC_CONFIG.GOOGLE_AUTOMATION_SECRET) { throw new Error('Solicitud no autorizada.'); } } function json_(obj) { return ContentService .createTextOutput(JSON.stringify(obj)) .setMimeType(ContentService.MimeType.JSON); } function safeName_(value, fallback) { const s = String(value || fallback || 'WMC') .replace(/[\\/:*?"<>|#%{}\[\]]+/g, ' ') .replace(/\s+/g, ' ') .trim(); return s.slice(0, 160) || String(fallback || 'WMC'); } function outputFolder_() { return DriveApp.getFolderById(WMC_CONFIG.OUTPUT_FOLDER_ID); } function copyTemplate_(templateId, name) { const file = DriveApp.getFileById(templateId); return file.makeCopy(safeName_(name, 'WMC'), outputFolder_()); } function norm_(v) { return String(v || '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase() .replace(/\s+/g, ' ') .trim(); } function findCell_(sheet, variants) { const values = sheet.getDataRange().getDisplayValues(); const wanted = variants.map(function(v) { return norm_(v); }); for (let r = 0; r < values.length; r++) { for (let c = 0; c < values[r].length; c++) { const n = norm_(values[r][c]); for (let i = 0; i < wanted.length; i++) { const w = wanted[i]; if (n === w || n.indexOf(w + ':') === 0 || n.indexOf(w) >= 0) { return { row: r + 1, col: c + 1, text: values[r][c] }; } } } } return null; } function findRow_(sheet, variants) { const c = findCell_(sheet, variants); return c ? c.row : null; } function setRightOfLabel_(sheet, variants, value) { if (value === undefined || value === null || value === '') return false; const cell = findCell_(sheet, variants); if (!cell) return false; sheet.getRange(cell.row, Math.min(cell.col + 1, sheet.getMaxColumns())).setValue(value); return true; } function visibleFirstSheet_(ss) { const sheets = ss.getSheets(); for (let i = 0; i < sheets.length; i++) { if (!sheets[i].isSheetHidden()) return sheets[i]; } return sheets[0]; } function findExactTextRowInColumnAfter_(sheet, col, text, afterRow, beforeRow) { const start = Math.max(1, Number(afterRow || 1)); const end = Math.min(sheet.getMaxRows(), Number(beforeRow || sheet.getMaxRows())); if (end < start) return null; const values = sheet.getRange(start, col, end - start + 1, 1).getDisplayValues(); const wanted = norm_(text); for (let i = 0; i < values.length; i++) { if (norm_(values[i][0]) === wanted) return start + i; } return null; } function firstNumber_() { for (let i = 0; i < arguments.length; i++) { const v = arguments[i]; if (v === '' || v === null || v === undefined) continue; const n = Number(v); if (Number.isFinite(n)) return n; } return null; } function numOrBlank_(v) { if (v === '' || v === null || v === undefined) return ''; const n = Number(v); return Number.isFinite(n) ? n : ''; } function countryCurrencyCode_(code) { const map = { GT: 'GTQ', CR: 'CRC', SV: 'USD', HN: 'HNL', NI: 'NIO' }; return map[String(code || '').toUpperCase()] || ''; } function countryCurrencySymbol_(code) { const map = { GT: 'Q', CR: '₡', SV: '$', HN: 'L', NI: 'C$' }; return map[String(code || '').toUpperCase()] || ''; } // ============================================================================ // NORMALIZACIÓN FINANCIERA DEFENSIVA // ============================================================================ function money_(value) { const n = Number(value); if (!Number.isFinite(n)) return 0; return Math.round((n + Number.EPSILON) * 100) / 100; } function isFeeSection_(section) { return norm_(section).indexOf('fee') >= 0; } function normalizeWalmartQuoteFinancials_(input) { const q = JSON.parse(JSON.stringify(input || {})); const rateRaw = Number(q.exchangeRate !== undefined ? q.exchangeRate : q.exchange_rate); const rate = Number.isFinite(rateRaw) && rateRaw > 0 ? rateRaw : 1; const items = Array.isArray(q.items) ? q.items : []; let subtotalUsd = 0; let explicitFeeUsd = 0; q.items = items.map(function(raw) { const it = raw && typeof raw === 'object' ? raw : {}; const units = firstNumber_(it.units, it.quantity, 0); const days = (it.days === '' || it.days === null || it.days === undefined) ? 1 : firstNumber_(it.days, 1); const unitUsd = firstNumber_(it.unitCostUsd, it.unit_cost_usd, 0); const suppliedTotal = firstNumber_(it.totalUsd, it.total_usd, 0); const computedTotal = money_(Number(units || 0) * Number(days || 0) * Number(unitUsd || 0)); const totalUsd = Number(unitUsd || 0) > 0 && Number(units || 0) > 0 ? computedTotal : money_(suppliedTotal || 0); const normalized = Object.assign({}, it, { units: Number(units || 0), days: Number(days || 0), unitCostUsd: money_(unitUsd || 0), totalUsd: totalUsd, unitCostLocal: money_(Number(unitUsd || 0) * rate), totalLocal: money_(totalUsd * rate) }); if (isFeeSection_(it.section)) explicitFeeUsd += totalUsd; else subtotalUsd += totalUsd; return normalized; }); subtotalUsd = money_(subtotalUsd); let feeRate = firstNumber_(q.agencyFeeRate, q.feeRate); if (feeRate === null && explicitFeeUsd > 0 && subtotalUsd > 0) feeRate = explicitFeeUsd / subtotalUsd; if (feeRate === null || !Number.isFinite(feeRate) || feeRate < 0) feeRate = 0.06; if (feeRate > 1) feeRate = feeRate / 100; const feeUsd = money_(subtotalUsd * feeRate); q.exchangeRate = rate; q.agencyFeeRate = feeRate; q.subtotalUsd = subtotalUsd; q.subtotalLocal = money_(subtotalUsd * rate); q.agencyFeeUsd = feeUsd; q.agencyFeeLocal = money_(feeUsd * rate); q.totalUsd = money_(subtotalUsd + feeUsd); q.totalLocal = money_(q.totalUsd * rate); return q; } function readWalmartGrandTotals_(sheet, q) { const totalCell = findCell_(sheet, ['Total con todos los rubros', 'Total general']); if (!totalCell) { return { totalUsd: money_(q.totalUsd || 0), totalLocal: money_(q.totalLocal || 0) }; } const row = totalCell.row; const usd = Number(sheet.getRange(row, 5).getValue()); const local = Number(sheet.getRange(row, 7).getValue()); return { totalUsd: Number.isFinite(usd) ? money_(usd) : money_(q.totalUsd || 0), totalLocal: Number.isFinite(local) ? money_(local) : money_(q.totalLocal || 0) }; } // ============================================================================ // 1) COTIZACIÓN OFICIAL WALMART CONNECT // ============================================================================ function generateWalmartQuote_(body) { const q = normalizeWalmartQuoteFinancials_(body.quote || {}); if (!q.countryCode || !Array.isArray(q.items) || !q.items.length) { throw new Error('La cotización no contiene país/items suficientes.'); } const stamp = Utilities.formatDate( new Date(), Session.getScriptTimeZone() || 'America/Guatemala', 'yyyyMMdd-HHmm' ); const name = safeName_( 'WMC_' + (q.project || q.advertiser || 'Cotizacion') + '_' + q.countryCode + '_' + stamp, 'WMC_Cotizacion' ); const file = copyTemplate_(WMC_CONFIG.WALMART_TEMPLATE_ID, name); const ss = SpreadsheetApp.openById(file.getId()); const sheet = ss.getSheetByName('Solicitud Cotizaciones') || visibleFirstSheet_(ss); setRightOfLabel_(sheet, ['Brand Solution'], q.brandSolution || 'WMC'); setRightOfLabel_(sheet, ['País', 'Pais'], q.country || q.countryCode); setRightOfLabel_(sheet, ['Agencia'], 'GomezLee Marketing S.A.'); setRightOfLabel_(sheet, ['Proyecto'], q.project || ''); setRightOfLabel_(sheet, ['Anunciante'], q.advertiser || ''); setRightOfLabel_(sheet, ['Formato'], q.format || ''); setRightOfLabel_(sheet, ['Tipo de cambio'], Number(q.exchangeRate || 1)); const sectionDefs = [ { key: 'Personal', aliases: ['Personal'] }, { key: 'Mobiliario - Equipo', aliases: ['Mobiliario - Equipo', 'Mobiliario y Equipo', 'Mobiliario'] }, { key: 'Insumos de degustación y/o preparación', aliases: ['Insumos de degustación y/o preparación', 'Insumos de degustación', 'Insumos'] }, { key: 'Logística e Instalación', aliases: ['Logística e Instalación', 'Logistica e Instalación', 'Logistica e Instalacion', 'Logística', 'Logistica'] } ]; const grouped = {}; for (let i = 0; i < q.items.length; i++) { const it = q.items[i] || {}; const section = String(it.section || 'Otros'); if (!grouped[section]) grouped[section] = []; grouped[section].push(it); } // De abajo hacia arriba para que las inserciones no desplacen secciones todavía no procesadas. const results = []; for (let i = sectionDefs.length - 1; i >= 0; i--) { const def = sectionDefs[i]; const items = getItemsForSection_(q.items, def.aliases); results.push(fillWalmartSection_(sheet, def.aliases, items)); } // Reordenamos para cálculos posteriores. const sectionTotals = []; for (let i = 0; i < sectionDefs.length; i++) { const headerRow = findRow_(sheet, sectionDefs[i].aliases); if (!headerRow) continue; const totalRow = findExactTextRowInColumnAfter_(sheet, 1, 'Total', headerRow + 1); if (totalRow) sectionTotals.push(totalRow); } configureWalmartFee_(sheet, q, sectionTotals); configureWalmartGrandTotal_(sheet, sectionTotals); setRightOfLabel_(sheet, ['Vigencia de la cotización', 'Vigencia'], q.validity || '30 días'); setRightOfLabel_( sheet, ['Observaciones o exclusiones', 'Observaciones'], Array.isArray(q.observations) ? q.observations.join('\n') : (q.observations || '') ); insertVisualReferences_(sheet, q.visualReferences || []); // Forzamos el recálculo del machote y guardamos en la trazabilidad los mismos // totales finales que ve el usuario en la hoja (incluyendo Fee de agencia). SpreadsheetApp.flush(); const finalTotals = readWalmartGrandTotals_(sheet, q); q.totalUsd = finalTotals.totalUsd; q.totalLocal = finalTotals.totalLocal; let data = ss.getSheetByName('_WMC_DATA'); if (!data) data = ss.insertSheet('_WMC_DATA'); data.clear(); data.getRange('A1:B8').setValues([ ['generatedAt', new Date()], ['countryCode', q.countryCode], ['currencyCode', q.currencyCode || countryCurrencyCode_(q.countryCode)], ['exchangeRate', Number(q.exchangeRate || 1)], ['totalUsd', Number(q.totalUsd || 0)], ['totalLocal', Number(q.totalLocal || 0)], ['conversationId', body.conversationId || ''], ['quoteJson', JSON.stringify(q)] ]); data.hideSheet(); try { sheet.setName(safeName_('Cotización ' + (q.format || '') + ' ' + q.countryCode, 'Cotización').slice(0, 99)); } catch (ignore) {} SpreadsheetApp.flush(); return { ok: true, artifactType: 'quote', artifactId: file.getId(), artifactUrl: ss.getUrl(), title: name, provider: 'Google Sheets' }; } function getItemsForSection_(items, aliases) { const wanted = aliases.map(function(v) { return norm_(v); }); const out = []; for (let i = 0; i < items.length; i++) { const it = items[i] || {}; const section = norm_(it.section || ''); let match = false; for (let j = 0; j < wanted.length; j++) { if (section === wanted[j] || section.indexOf(wanted[j]) >= 0 || wanted[j].indexOf(section) >= 0) { match = true; break; } } if (match) out.push(it); } return out; } function fillWalmartSection_(sheet, aliases, items) { const headerRow = findRow_(sheet, aliases); if (!headerRow) return { headerRow: null, totalRow: null }; let totalRow = findExactTextRowInColumnAfter_(sheet, 1, 'Total', headerRow + 1); if (!totalRow) throw new Error('No se encontró la fila Total para la sección ' + aliases[0] + '.'); const start = headerRow + 1; let slots = totalRow - start; const needed = Math.max(1, items.length); if (needed > slots) { const extra = needed - slots; sheet.insertRowsBefore(totalRow, extra); if (slots > 0) { sheet.getRange(start, 1, 1, 7).copyTo( sheet.getRange(totalRow, 1, extra, 7), SpreadsheetApp.CopyPasteType.PASTE_FORMAT, false ); } totalRow += extra; slots += extra; } // Limpia únicamente las filas de detalle; nunca toca la fila Total. if (slots > 0) sheet.getRange(start, 1, slots, 7).clearContent(); for (let i = 0; i < items.length; i++) { const row = start + i; const it = items[i] || {}; sheet.getRange(row, 1, 1, 4).setValues([[ it.description || 'Item', numOrBlank_(it.units), numOrBlank_(it.days), numOrBlank_(it.unitCostUsd) ]]); sheet.getRange(row, 5).setFormula('=B' + row + '*C' + row + '*D' + row); sheet.getRange(row, 6).setFormula('=D' + row + '*$B$11'); sheet.getRange(row, 7).setFormula('=E' + row + '*$B$11'); } // Mantiene fórmulas neutras en espacios libres para que el subtotal siga siendo confiable. for (let r = start + items.length; r < totalRow; r++) { sheet.getRange(r, 5).setFormula('=B' + r + '*C' + r + '*D' + r); sheet.getRange(r, 6).setFormula('=D' + r + '*$B$11'); sheet.getRange(r, 7).setFormula('=E' + r + '*$B$11'); } const end = totalRow - 1; sheet.getRange(totalRow, 4).setFormula('=SUM(D' + start + ':D' + end + ')'); sheet.getRange(totalRow, 5).setFormula('=SUM(E' + start + ':E' + end + ')'); sheet.getRange(totalRow, 6).setFormula('=SUM(F' + start + ':F' + end + ')'); sheet.getRange(totalRow, 7).setFormula('=SUM(G' + start + ':G' + end + ')'); sheet.getRange(start, 4, Math.max(1, slots), 2).setNumberFormat('$#,##0.00;[Red]-$#,##0.00'); sheet.getRange(start, 6, Math.max(1, slots), 2).setNumberFormat('#,##0.00;[Red]-#,##0.00'); return { headerRow: headerRow, totalRow: totalRow }; } function configureWalmartFee_(sheet, q, sectionTotalRows) { const feeHeader = findRow_(sheet, ['Fee de agencia', 'Fee Agencia', 'Fee']); if (!feeHeader) return; const feeCalcRow = feeHeader + 1; let feeRate = firstNumber_(q.agencyFeeRate, q.feeRate); const feeItems = getItemsForSection_(q.items || [], ['Fee de agencia', 'Fee Agencia', 'Fee']); if (feeRate === null && feeItems.length) { const baseUsd = sumItemTotals_(q.items || [], false); const feeUsd = sumItemTotals_(feeItems, true); if (baseUsd > 0 && feeUsd >= 0) feeRate = feeUsd / baseUsd; } // Si Gemini no suministra un fee explícito, conserva el 6% del machote oficial. if (feeRate === null || !Number.isFinite(feeRate) || feeRate < 0) { const current = Number(sheet.getRange(feeHeader, 4).getValue()); feeRate = Number.isFinite(current) && current >= 0 ? current : 0.06; } if (feeRate > 1) feeRate = feeRate / 100; sheet.getRange(feeHeader, 4).setValue(feeRate).setNumberFormat('0.00%'); sheet.getRange(feeHeader, 6).setValue(feeRate).setNumberFormat('0.00%'); const dRefs = sectionTotalRows.map(function(r) { return 'D' + r; }); const eRefs = sectionTotalRows.map(function(r) { return 'E' + r; }); const fRefs = sectionTotalRows.map(function(r) { return 'F' + r; }); const gRefs = sectionTotalRows.map(function(r) { return 'G' + r; }); sheet.getRange(feeCalcRow, 4).setFormula('=(' + dRefs.join('+') + ')*D' + feeHeader); sheet.getRange(feeCalcRow, 5).setFormula('=(' + eRefs.join('+') + ')*D' + feeHeader); sheet.getRange(feeCalcRow, 6).setFormula('=(' + fRefs.join('+') + ')*F' + feeHeader); sheet.getRange(feeCalcRow, 7).setFormula('=(' + gRefs.join('+') + ')*F' + feeHeader); } function configureWalmartGrandTotal_(sheet, sectionTotalRows) { const totalCell = findCell_(sheet, ['Total con todos los rubros', 'Total general']); if (!totalCell) return; const grandRow = totalCell.row; const feeHeader = findRow_(sheet, ['Fee de agencia', 'Fee Agencia', 'Fee']); const feeCalcRow = feeHeader ? feeHeader + 1 : null; const cols = [4, 5, 6, 7]; for (let i = 0; i < cols.length; i++) { const col = cols[i]; const letter = columnLetter_(col); const refs = sectionTotalRows.map(function(r) { return letter + r; }); if (feeCalcRow) refs.push(letter + feeCalcRow); sheet.getRange(grandRow, col).setFormula('=' + refs.join('+')); } } function sumItemTotals_(items, includeAll) { let total = 0; for (let i = 0; i < items.length; i++) { const it = items[i] || {}; if (!includeAll && norm_(it.section || '').indexOf('fee') >= 0) continue; const n = firstNumber_(it.totalUsd, it.total_usd); if (n !== null) total += n; } return total; } function columnLetter_(col) { let n = Number(col); let s = ''; while (n > 0) { const rem = (n - 1) % 26; s = String.fromCharCode(65 + rem) + s; n = Math.floor((n - 1) / 26); } return s; } // ============================================================================ // 2) REFERENCIAS VISUALES // ============================================================================ function insertVisualReferences_(sheet, refs) { if (!Array.isArray(refs) || !refs.length) return; const row = findRow_(sheet, ['Referencias visuales', 'Referencia visual']); if (!row) return; let col = 1; refs.slice(0, 6).forEach(function(ref) { try { const blob = visualBlob_(ref); if (!blob) return; const img = sheet.insertImage(blob, col, row + 1); const maxW = 360; const maxH = 260; const scale = Math.min(1, maxW / img.getWidth(), maxH / img.getHeight()); img.setWidth(Math.round(img.getWidth() * scale)); img.setHeight(Math.round(img.getHeight() * scale)); col = Math.min(col + 4, Math.max(1, sheet.getMaxColumns() - 1)); } catch (err) { console.warn('No se pudo insertar referencia visual: ' + err); } }); } function driveIdFromUrl_(url) { const text = String(url || ''); let m = text.match(/\/d\/([a-zA-Z0-9_-]{15,})/); if (m) return m[1]; m = text.match(/[?&]id=([a-zA-Z0-9_-]{15,})/); return m ? m[1] : ''; } function visualBlob_(ref) { const obj = ref && typeof ref === 'object' ? ref : {}; const directId = String(obj.imageDriveId || obj.driveId || obj.fileId || '').trim(); if (directId) return DriveApp.getFileById(directId).getBlob(); const url = typeof ref === 'string' ? ref : String(obj.url || obj.imageUrl || '').trim(); if (!url) return null; const driveId = driveIdFromUrl_(url); if (driveId) return DriveApp.getFileById(driveId).getBlob(); const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true, followRedirects: true }); if (response.getResponseCode() < 200 || response.getResponseCode() >= 300) { throw new Error('No se pudo descargar la referencia visual. HTTP ' + response.getResponseCode()); } return response.getBlob(); } // ============================================================================ // 3) MENSUALIZACIÓN INTERNA GLM // ============================================================================ function generateMonthlyGlm_(body) { const q = body.quote || {}; const a = body.artifact || {}; const metadata = a.metadata && typeof a.metadata === 'object' ? a.metadata : {}; const stamp = Utilities.formatDate( new Date(), Session.getScriptTimeZone() || 'America/Guatemala', 'yyyyMMdd-HHmm' ); const name = safeName_( 'GLM-' + (q.project || a.title || 'WMC') + '_' + (q.countryCode || '') + '_' + stamp, 'GLM-WMC' ); const file = copyTemplate_(WMC_CONFIG.GLM_MONTHLY_TEMPLATE_ID, name); const ss = SpreadsheetApp.openById(file.getId()); const sheet = ss.getSheetByName('Cotización') || visibleFirstSheet_(ss); const country = q.country || q.countryName || q.countryCode || ''; const countryCode = String(q.countryCode || '').toUpperCase(); const advertiser = q.advertiser || a.client || ''; const project = q.project || a.project || q.format || 'Cotización Walmart Connect'; const contact = a.attention || metadata.attention || q.contactName || q.contact_name || ''; const email = a.email || metadata.email || q.contactEmail || q.contact_email || (body.user ? body.user.email : '') || ''; const currencySymbol = a.currencySymbol || metadata.currencySymbol || q.currencySymbol || countryCurrencySymbol_(countryCode); const vendor = a.vendorNumber || metadata.vendorNumber || WMC_CONFIG.GLM_VENDOR_NUMBER; const quoteCode = a.quoteCode || metadata.quoteCode || q.quoteCode || q.quote_code || ''; // El formato GLM real guarda estos valores dentro de la misma celda A6:A12. sheet.getRange('A6').setValue('País: ' + country); sheet.getRange('E6').setValue(quoteCode); sheet.getRange('A7').setValue('Empresa: Walmart Connect'); sheet.getRange('A8').setValue('Asunto: ' + project); sheet.getRange('A9').setValue('Atención:' + contact); sheet.getRange('A10').setValue('Correo:' + email); sheet.getRange('A11').setValue('Moneda: ' + currencySymbol); sheet.getRange('A12').setValue('Vendor #: ' + vendor); const summaryTitle = a.summaryTitle || metadata.summaryTitle || ('Walmart Connect-' + (advertiser || project)); sheet.getRange('A14').setValue(summaryTitle); const monthlyDate = parseDateOrNow_(a.monthlyDate || metadata.monthlyDate || a.date || metadata.date); sheet.getRange('A15').setValue(monthlyDate).setNumberFormat('mmmm yyyy'); sheet.getRange('B17').setValue(country); const lines = Array.isArray(a.monthlyLines) && a.monthlyLines.length ? a.monthlyLines : (Array.isArray(q.items) ? q.items : []); const activity = fillGlmActivityLines_(sheet, lines, q, a, metadata); refreshGlmBudgetSummary_(sheet, activity); let data = ss.getSheetByName('_WMC_MONTHLY_DATA'); if (!data) data = ss.insertSheet('_WMC_MONTHLY_DATA'); data.clear(); data.getRange('A1:B7').setValues([ ['generatedAt', new Date()], ['countryCode', countryCode], ['currency', currencySymbol], ['sourceQuoteId', a.sourceQuoteId || ''], ['conversationId', body.conversationId || ''], ['quoteJson', JSON.stringify(q)], ['artifactJson', JSON.stringify(a)] ]); data.hideSheet(); SpreadsheetApp.flush(); return { ok: true, artifactType: 'monthly_glm', artifactId: file.getId(), artifactUrl: ss.getUrl(), title: name, provider: 'Google Sheets' }; } function fillGlmActivityLines_(sheet, lines, quote, artifact, metadata) { const header = findCell_(sheet, ['DESCRIPCIÓN', 'Descripcion', 'Descripción']); if (!header) throw new Error('No se encontró el encabezado DESCRIPCIÓN en la plantilla GLM.'); const start = header.row + 1; let totalRow = findExactTextRowInColumnAfter_(sheet, 4, 'Total', start); if (!totalRow) throw new Error('No se encontró la primera fila Total del bloque de actividades GLM.'); let slots = totalRow - start; const needed = Math.max(1, lines.length); if (needed > slots) { const extra = needed - slots; sheet.insertRowsBefore(totalRow, extra); if (slots > 0) { sheet.getRange(start, 1, 1, 5).copyTo( sheet.getRange(totalRow, 1, extra, 5), SpreadsheetApp.CopyPasteType.PASTE_FORMAT, false ); } totalRow += extra; slots += extra; } if (slots > 0) sheet.getRange(start, 1, slots, 5).clearContent(); for (let i = 0; i < lines.length; i++) { const row = start + i; const it = lines[i] || {}; const qty = firstNumber_(it.units, it.quantity, 1); const days = firstNumber_(it.days, 1); const localUnit = firstNumber_( it.unitCostLocal, it.unit_cost_local, firstNumber_(it.unitCostUsd, it.unit_cost_usd, 0) * firstNumber_(quote.exchangeRate, quote.exchange_rate, 1) ); sheet.getRange(row, 1, 1, 4).setValues([[ it.description || it.concept || it.item_name || 'Item', qty, days, localUnit ]]); sheet.getRange(row, 5).setFormula('=D' + row + '*B' + row + '*C' + row); } const end = totalRow - 1; sheet.getRange(totalRow, 4).setValue('Total '); sheet.getRange(totalRow, 5).setFormula('=SUM(E' + start + ':E' + end + ')'); const taxRow = totalRow + 1; const grandRow = totalRow + 2; let taxRate = firstNumber_(artifact.taxRate, metadata.taxRate, WMC_CONFIG.GLM_IVA_RATE); if (taxRate > 1) taxRate = taxRate / 100; sheet.getRange(taxRow, 4).setValue('IVA '); sheet.getRange(taxRow, 5).setFormula('=E' + totalRow + '*' + taxRate); sheet.getRange(grandRow, 4).setValue('Total '); sheet.getRange(grandRow, 5).setFormula('=E' + totalRow + '+E' + taxRow); sheet.getRange(start, 4, Math.max(1, slots), 2).setNumberFormat('#,##0.00;[Red]-#,##0.00'); sheet.getRange(totalRow, 5, 3, 1).setNumberFormat('#,##0.00;[Red]-#,##0.00'); return { startRow: start, endRow: end, totalRow: totalRow, taxRow: taxRow, grandRow: grandRow }; } function refreshGlmBudgetSummary_(sheet, activity) { const pmCell = findCell_(sheet, ['Project Management']); let pmTotalRow = null; if (pmCell) { // En el machote GLM real, el total de Project Management es el último Total antes de Presupuesto. const presupuestoCell = findCell_(sheet, ['Presupuesto']); const before = presupuestoCell ? presupuestoCell.row - 1 : sheet.getMaxRows(); for (let r = before; r > pmCell.row; r--) { const d = norm_(sheet.getRange(r, 4).getDisplayValue()); if (d === 'total') { pmTotalRow = r; break; } } } const sinFee = findCell_(sheet, ['Sub-Total-1 Presupuesto actividades SIN FEE']); const conFee = findCell_(sheet, ['Sub-Total-1 Presupuesto actividades CON FEE']); const totalActividad = findCell_(sheet, ['Total De Actividad', 'Total de Actividad']); if (sinFee) { if (pmTotalRow) sheet.getRange(sinFee.row, 2).setFormula('=E' + pmTotalRow); else sheet.getRange(sinFee.row, 2).setValue(0); } if (conFee) sheet.getRange(conFee.row, 2).setFormula('=E' + activity.grandRow); if (totalActividad) { const refs = []; if (sinFee) refs.push('B' + sinFee.row); if (conFee) refs.push('B' + conFee.row); if (refs.length) sheet.getRange(totalActividad.row, 2).setFormula('=' + refs.join('+')); } } function parseDateOrNow_(value) { if (value instanceof Date && !isNaN(value.getTime())) return value; if (value) { const d = new Date(value); if (!isNaN(d.getTime())) return d; } return new Date(); } // ============================================================================ // 4) GOOGLE SLIDES // ============================================================================ function generateProposal_(body) { const a = body.artifact || {}; const q = body.quote || {}; const title = safeName_(a.title || q.project || 'Propuesta Walmart Connect', 'Propuesta Walmart Connect'); const pres = SlidesApp.create(title); const file = DriveApp.getFileById(pres.getId()); file.moveTo(outputFolder_()); const slides = Array.isArray(a.slides) && a.slides.length ? a.slides : [ { type: 'cover', title: title, subtitle: 'Walmart Connect' }, { type: 'thankyou', title: 'Thank you!' } ]; // Primero agrega las nuevas diapositivas y al final elimina la inicial automática. const initialSlide = pres.getSlides().length ? pres.getSlides()[0] : null; for (let i = 0; i < slides.length; i++) { addProposalSlide_(pres, slides[i], body, i); } if (initialSlide && pres.getSlides().length > 1) initialSlide.remove(); return { ok: true, artifactType: 'proposal', artifactId: pres.getId(), artifactUrl: pres.getUrl(), title: title, provider: 'Google Slides' }; } function addProposalSlide_(pres, spec, body, index) { const type = String(spec.type || 'content'); const slide = pres.appendSlide(SlidesApp.PredefinedLayout.BLANK); const pageW = pres.getPageWidth(); const pageH = pres.getPageHeight(); const dark = type === 'cover' || type === 'section' || type === 'thankyou'; slide.getBackground().setSolidFill(dark ? WMC.NAVY : '#FFFFFF'); const titleText = String(spec.title || (index === 0 ? (body.title || 'Walmart Connect') : '')); const titleBox = slide.insertTextBox( titleText, dark ? 48 : 44, dark ? pageH * 0.38 : 28, pageW - 96, dark ? 90 : 48 ); titleBox.getText().getTextStyle() .setFontFamily('Arial') .setFontSize(dark ? 28 : 22) .setBold(true) .setForegroundColor(dark ? '#FFFFFF' : WMC.NAVY); titleBox.getText().getParagraphStyle().setParagraphAlignment( dark ? SlidesApp.ParagraphAlignment.CENTER : SlidesApp.ParagraphAlignment.START ); const sub = String(spec.subtitle || spec.body || ''); if (sub) { const subBox = slide.insertTextBox( sub, 52, dark ? pageH * 0.57 : 82, pageW - 104, 70 ); subBox.getText().getTextStyle() .setFontFamily('Arial') .setFontSize(13) .setForegroundColor(dark ? '#FFFFFF' : WMC.TEXT); if (dark) { subBox.getText().getParagraphStyle().setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER); } } if (!dark) { try { const blob = visualBlob_({ imageDriveId: spec.imageDriveId || '', imageUrl: spec.imageUrl || '', url: spec.imageUrl || '' }); if (blob) { const img = slide.insertImage(blob); const maxW = pageW - 96; const maxH = pageH - 185; const scale = Math.min(maxW / img.getWidth(), maxH / img.getHeight()); const finalW = img.getWidth() * scale; const finalH = img.getHeight() * scale; img.setWidth(finalW) .setHeight(finalH) .setLeft((pageW - finalW) / 2) .setTop(150); } } catch (err) { console.warn('No se pudo insertar imagen en Slides: ' + err); } } const footer = slide.insertTextBox( 'PROPRIETARY & CONFIDENTIAL · Walmart Connect / GomezLee Marketing', 30, pageH - 26, pageW - 60, 14 ); footer.getText().getTextStyle() .setFontFamily('Arial') .setFontSize(7) .setForegroundColor(dark ? '#D9E2F3' : '#777777'); } // ============================================================================ // 5) IMAGEN YA GENERADA POR GEMINI EN N8N // ============================================================================ function generateImageArtifact_(body) { const a = body.artifact || {}; const driveId = String(a.imageDriveId || a.driveId || a.fileId || '').trim(); if (driveId) { const file = DriveApp.getFileById(driveId); return { ok: true, artifactType: 'image', artifactId: file.getId(), artifactUrl: file.getUrl(), title: a.title || file.getName() || 'Visual WMC', provider: 'Google Gemini (n8n) + Google Drive' }; } const url = String(a.imageUrl || a.url || '').trim(); if (url) { return { ok: true, artifactType: 'image', artifactId: null, artifactUrl: url, title: a.title || 'Visual WMC', provider: 'Google Gemini (n8n)' }; } throw new Error('Las imágenes se generan en n8n con el nodo nativo Google Gemini; no se recibió imageDriveId/imageUrl.'); }