Update project and dist

This commit is contained in:
2026-08-29 08:39:57 -04:00
commit 1d2b8858d6
527 changed files with 27976 additions and 0 deletions
@@ -0,0 +1,977 @@
/**
* 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.');
}