1310 lines
136 KiB
JSON
1310 lines
136 KiB
JSON
{
|
|
"name": "Cruce de Seguridad Social - Honduras",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"formTitle": "Cruce IHSS vs Nómina Honduras",
|
|
"formDescription": "Sube la nómina mensual de Honduras y una o varias planillas IHSS en PDF. El flujo generará un reporte simple con cruce de identidad y cruce de montos IHSS.",
|
|
"formFields": {
|
|
"values": [
|
|
{
|
|
"fieldLabel": "Mes a validar",
|
|
"fieldType": "dropdown",
|
|
"fieldOptions": {
|
|
"values": [
|
|
{
|
|
"option": "01"
|
|
},
|
|
{
|
|
"option": "02"
|
|
},
|
|
{
|
|
"option": "03"
|
|
},
|
|
{
|
|
"option": "04"
|
|
},
|
|
{
|
|
"option": "05"
|
|
},
|
|
{
|
|
"option": "06"
|
|
},
|
|
{
|
|
"option": "07"
|
|
},
|
|
{
|
|
"option": "08"
|
|
},
|
|
{
|
|
"option": "09"
|
|
},
|
|
{
|
|
"option": "10"
|
|
},
|
|
{
|
|
"option": "11"
|
|
},
|
|
{
|
|
"option": "12"
|
|
}
|
|
]
|
|
},
|
|
"requiredField": true
|
|
},
|
|
{
|
|
"fieldLabel": "Planillas IHSS PDF",
|
|
"fieldType": "file",
|
|
"acceptFileTypes": ".pdf",
|
|
"requiredField": true
|
|
},
|
|
{
|
|
"fieldLabel": "Nómina",
|
|
"fieldType": "file",
|
|
"multipleFiles": false,
|
|
"acceptFileTypes": ".xlsx, .xls, .csv",
|
|
"requiredField": true
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"ignoreBots": true
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.formTrigger",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
-2976,
|
|
2864
|
|
],
|
|
"id": "6cf49d17-8bfb-4735-ac86-5b20d1de54f0",
|
|
"name": "On form submission",
|
|
"webhookId": "68fcb432-b695-4112-b83b-df3eab99f7b2"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const item = $input.all()[0];\nconst now = new Date();\nconst pad = n => String(n).padStart(2, '0');\nconst ts = `${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;\nconst runId = `HN_IHSS_${ts}_${Math.random().toString(36).slice(2, 8)}`;\nconst mes = String(item.json['Mes a validar'] || '').padStart(2, '0');\nconst anio = now.getFullYear();\nconst meses = { '01':'Enero','02':'Febrero','03':'Marzo','04':'Abril','05':'Mayo','06':'Junio','07':'Julio','08':'Agosto','09':'Septiembre','10':'Octubre','11':'Noviembre','12':'Diciembre' };\nconst fechaTitulo = `${pad(now.getDate())}-${pad(now.getMonth()+1)}-${now.getFullYear()}`;\nconst periodoLabel = `${meses[mes] || mes} ${anio}`;\nconst reportTitle = `Cruce Seguridad Social Honduras - ${periodoLabel} - ${fechaTitulo}`;\nconst data = $getWorkflowStaticData('global');\ndata.hnCruceIHSSNomina = data.hnCruceIHSSNomina || {};\ndata.hnCruceIHSSNomina[runId] = {\n startedAt: now.toISOString(),\n mes,\n anio,\n periodoLabel,\n ihss: [],\n ihssPlanillas: [],\n nomina: [],\n payrollIhssSummary: {},\n payrollIhssBlocks: [],\n pdfFiles: [],\n payrollFile: '',\n bamboo: [],\n};\nreturn [{ json: { ...item.json, runId, mes, anio, periodoLabel, reportTitle, emailTo: 'iaracena@gomezleemarketing.com, ymadera@gomezleemarketing.com, msoto@gomezleemarketing.com, asrodriguez@gomezleemarketing.com, iherrera@gomezleemarketing.com, mgomez@gomezleemarketing.com, vparamo@gomezleemarketing.com' }, binary: item.binary }];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-2736,
|
|
2864
|
|
],
|
|
"id": "484c9045-a3ef-4a05-beb9-c48a13dd7d66",
|
|
"name": "Inicializar memoria"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const base = $input.all()[0];\nconst bin = base.binary || {};\nconst out = [];\nfor (const [key, file] of Object.entries(bin)) {\n const fileName = String(file.fileName || key);\n const mime = String(file.mimeType || '').toLowerCase();\n const lower = fileName.toLowerCase();\n const isPdf = mime.includes('pdf') || lower.endsWith('.pdf') || key.toLowerCase().includes('pdf') || key.toLowerCase().includes('planilla');\n if (!isPdf) continue;\n out.push({\n json: { runId: base.json.runId, mes: base.json.mes, anio: base.json.anio, fileName, originalBinaryKey: key },\n binary: { data: file },\n });\n}\nif (!out.length) {\n throw new Error('No encontré planillas IHSS en PDF. Revisa que el campo de planillas acepte .pdf y que hayas adjuntado al menos un PDF.');\n}\nconst data = $getWorkflowStaticData('global');\nif (data.hnCruceIHSSNomina?.[base.json.runId]) {\n data.hnCruceIHSSNomina[base.json.runId].pdfFiles = out.map(i => i.json.fileName);\n}\nreturn out;"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-1664,
|
|
2624
|
|
],
|
|
"id": "df786528-9ebf-4063-8530-6d0989aec394",
|
|
"name": "Separar planillas PDF"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "pdf",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
-1424,
|
|
2624
|
|
],
|
|
"id": "f84a024b-46f4-4626-a927-e61480458cf5",
|
|
"name": "Extraer texto PDF"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "function normalizeText(s) {\n return String(s || '').replace(/\\r/g, '').normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').replace(/Ù/g, 'U').replace(/ù/g, 'u');\n}\nfunction clean(s) { return String(s || '').replace(/\\s+/g, ' ').trim(); }\nfunction cleanNameLine(s) { return clean(String(s || '').replace(/\\s+\\d{1,2}\\s+[\\d,.]+.*$/, '')); }\nfunction toNumber(v) {\n if (v === null || v === undefined || v === '') return 0;\n let s = String(v).replace(/LPS|L|Q|USD|\\$|,/gi, '').replace(/\\s/g, '').trim();\n if (/^\\((.*)\\)$/.test(s)) s = '-' + s.replace(/^\\(|\\)$/g, '');\n const n = Number(s);\n return Number.isFinite(n) ? n : 0;\n}\nfunction money(n) { return Number((Number(n) || 0).toFixed(2)); }\nfunction firstMatch(text, patterns) { for (const p of patterns) { const m = text.match(p); if (m) return m[1] || m[0]; } return ''; }\nfunction numMatch(text, patterns) { const v = firstMatch(text, patterns); return v ? toNumber(v) : 0; }\nfunction unique13Digits(raw) {\n const found = String(raw || '').match(/\\b\\d{13}\\b/g) || [];\n return [...new Set(found)].filter(id => id !== '0000000000000');\n}\nfunction cleanNamesFromBlock(block) {\n return String(block || '').split('\\n')\n .map(cleanNameLine)\n .filter(l => l && !/^\\d+$/.test(l))\n .filter(l => /[A-ZÑ]/i.test(l))\n .filter(l => !/SALARIO|DIAS|TRABAJADOR|PATRONO|ESTADO|PERIODO|RIESGO|PAGINA|CUOTA|RECIBO|MONTO|TOTAL|PERSONA JURIDICA|PRIVADO|PUBLICIDAD|SECTOR|ACTIVIDAD|DIRECCION|TELEFONO|ZONA|CUADRA|NO\\.?\\s*AFILIACION|IDENTIDAD|NOMBRE Y APELLIDO/i.test(l));\n}\nfunction expectedWorkers(text) {\n const nums = [...String(text || '').matchAll(/TRABAJADORES\\s+(\\d{1,4})/gi)].map(m => Number(m[1])).filter(n => Number.isFinite(n) && n > 0);\n return nums.length ? Math.max(...nums) : 0;\n}\nfunction addCandidate(store, ids, names, meta) {\n for (let i = 0; i < ids.length; i++) {\n const id = ids[i];\n if (!id || id === '0000000000000') continue;\n if (!store.has(id)) {\n store.set(id, { identidad: id, nombre_planilla: clean(names[i] || ''), archivo_planilla: meta.fileName, numero_patronal: meta.patronal, recibo: meta.recibo, periodo: meta.periodo, cuota_trabajador: 0 });\n } else {\n const ex = store.get(id);\n if (!ex.nombre_planilla && clean(names[i] || '')) ex.nombre_planilla = clean(names[i]);\n }\n }\n}\nfunction extractCandidatesFromText(text, meta) {\n const store = new Map();\n const reA = /NO\\.?\\s*AFILIACION([\\s\\S]*?)IDENTIDAD([\\s\\S]*?)NOMBRE\\s+Y\\s+APELLIDO/gi;\n let m;\n while ((m = reA.exec(text)) !== null) addCandidate(store, unique13Digits(m[1]), cleanNamesFromBlock(m[2]), meta);\n const reB = /IDENTIDAD([\\s\\S]*?)NO\\.?\\s*AFILIACION[\\s\\S]*?NOMBRE\\s+Y\\s+APELLIDO([\\s\\S]*?)(?:\\n\\s*SALARIO|\\n\\s*30\\s|\\n\\s*Hago\\s+Constar|\\f|$)/gi;\n while ((m = reB.exec(text)) !== null) addCandidate(store, unique13Digits(m[1]), cleanNamesFromBlock(m[2]), meta);\n const reC = /IDENTIDAD([\\s\\S]*?)NOMBRE\\s+Y\\s+APELLIDO/gi;\n while ((m = reC.exec(text)) !== null) {\n const ids = unique13Digits(m[1]);\n if (ids.length) addCandidate(store, ids, [], meta);\n }\n const expected = expectedWorkers(text);\n if ((expected && store.size < Math.ceil(expected * 0.8)) || store.size === 0) addCandidate(store, unique13Digits(text), [], meta);\n return [...store.values()];\n}\nfunction extractPlanillaSummary(text, meta) {\n const trabajadores = numMatch(text, [/TRABAJADORES\\s+(\\d{1,4})/i]);\n const salario = numMatch(text, [/MONTO\\s+DE\\s+SALARIOS\\s+([\\d,.]+)/i]);\n const cuotaTrabajador = numMatch(text, [/CUOTA\\s+TRABAJADOR\\s+([\\d,.]+)/i]);\n const cuotaPatrono = numMatch(text, [/CUOTA\\s+PATRONO\\s+([\\d,.]+)/i]);\n const totalSinRecargo = money(cuotaTrabajador + cuotaPatrono);\n return {\n archivo_planilla: meta.fileName,\n numero_patronal: meta.patronal,\n recibo: meta.recibo,\n periodo: meta.periodo,\n trabajadores,\n salario_reportado: money(salario),\n cuota_trabajador: money(cuotaTrabajador),\n cuota_patrono: money(cuotaPatrono),\n total_planilla: totalSinRecargo,\n };\n}\nfunction periodoKey(v) { return String(v || '').replace(/\\s/g, '').trim(); }\nfunction planillaKey(p) {\n return [p.numero_patronal || '', p.recibo || '', periodoKey(p.periodo), p.trabajadores || '', money(p.cuota_trabajador || 0)].join('|');\n}\nconst init = $items('Inicializar memoria')[0].json;\nconst runId = init.runId;\nconst expectedPeriodo = `${String(init.mes || '').padStart(2, '0')}/${init.anio}`;\nconst items = $input.all();\nconst sourcePdfItems = $items('Separar planillas PDF');\nconst all = [];\nconst planillas = [];\nconst duplicatePlanillas = [];\nconst periodoMismatches = [];\nconst seenPlanillas = new Map();\nfor (let idx = 0; idx < items.length; idx++) {\n const item = items[idx];\n const source = sourcePdfItems[idx]?.json || {};\n const fileName = item.json.fileName || source.fileName || source.originalBinaryKey || item.json.file || item.json.originalBinaryKey || `Planilla PDF ${idx + 1}`;\n const text = normalizeText(item.json.text || item.json.data || item.json.content || '');\n if (!text.trim()) continue;\n const meta = {\n fileName,\n patronal: firstMatch(text, [/([0-9]{3}-[0-9]{4}-[0-9]{4}-[0-9])\\s*NUMERO\\s+PATRONAL/i, /NUMERO\\s+PATRONAL\\s*\\n?\\s*([0-9]{3}-[0-9]{4}-[0-9]{4}-[0-9])/i, /\\b([0-9]{3}-[0-9]{4}-[0-9]{4}-[0-9])\\b/i]),\n periodo: firstMatch(text, [/(\\d{2}\\s*\\/\\s*\\d{4})\\s*PERIODO/i, /PERIODO\\s*\\n?\\s*(\\d{2}\\s*\\/\\s*\\d{4})/i]),\n recibo: firstMatch(text, [/RECIBO\\s+NUMERO\\s*\\n?\\s*(\\d{5,})/i, /RECIBO\\s+DE\\s+PAGO[\\s\\S]{0,80}?\\b(\\d{6,})\\b/i, /\\*(\\d{6,})\\*/]),\n };\n const planilla = extractPlanillaSummary(text, meta);\n const candidates = extractCandidatesFromText(text, meta);\n const uniqueWorkersInPdf = new Set(candidates.map(r => r.identidad).filter(Boolean)).size;\n\n // Producción: algunos PDFs no exponen bien TRABAJADORES / MONTO DE SALARIOS en texto plano.\n if (!planilla.trabajadores || planilla.trabajadores < 1) planilla.trabajadores = uniqueWorkersInPdf;\n if (!planilla.salario_reportado || planilla.salario_reportado < 1000) {\n planilla.salario_reportado = planilla.cuota_trabajador\n ? money(planilla.cuota_trabajador / 0.05)\n : money(uniqueWorkersInPdf * 11903.13);\n }\n\n // Producción: si cargan el mismo PDF/recibo dos veces, NO se suma dos veces.\n const key = planillaKey(planilla);\n if (seenPlanillas.has(key)) {\n duplicatePlanillas.push({ ...planilla, archivo_original: seenPlanillas.get(key).archivo_planilla, archivo_duplicado: fileName });\n continue;\n }\n seenPlanillas.set(key, planilla);\n\n // Control de período: no detiene el flujo, pero queda visible como hallazgo.\n if (planilla.periodo && periodoKey(planilla.periodo) !== expectedPeriodo) {\n periodoMismatches.push({ archivo_planilla: fileName, periodo_detectado: planilla.periodo, periodo_esperado: expectedPeriodo });\n }\n\n planillas.push(planilla);\n const cuotaPorPersona = planilla.trabajadores ? money(planilla.cuota_trabajador / planilla.trabajadores) : 0;\n all.push(...candidates.map(r => ({...r, cuota_trabajador: cuotaPorPersona})));\n}\nconst byId = new Map();\nfor (const r of all) {\n if (!r.identidad) continue;\n if (!byId.has(r.identidad)) {\n byId.set(r.identidad, { ...r, archivos_planilla: r.archivo_planilla ? [r.archivo_planilla] : [], patronales: r.numero_patronal ? [r.numero_patronal] : [], recibos: r.recibo ? [r.recibo] : [] });\n } else {\n const ex = byId.get(r.identidad);\n if (r.nombre_planilla && !ex.nombre_planilla) ex.nombre_planilla = r.nombre_planilla;\n if (r.archivo_planilla && !ex.archivos_planilla.includes(r.archivo_planilla)) ex.archivos_planilla.push(r.archivo_planilla);\n if (r.numero_patronal && !ex.patronales.includes(r.numero_patronal)) ex.patronales.push(r.numero_patronal);\n if (r.recibo && !ex.recibos.includes(r.recibo)) ex.recibos.push(r.recibo);\n if (!ex.cuota_trabajador && r.cuota_trabajador) ex.cuota_trabajador = r.cuota_trabajador;\n }\n}\nconst registros = [...byId.values()].map(r => ({ identidad: r.identidad, nombre_planilla: r.nombre_planilla || '', archivos_planilla: r.archivos_planilla.join(' | '), numero_patronal: r.patronales.join(' | '), recibo: r.recibos.join(' | '), periodo: r.periodo || '', cuota_trabajador: money(r.cuota_trabajador) }));\nif (!registros.length) throw new Error('No pude extraer identidades de las planillas IHSS. Revisa que los PDF sean legibles y que no sean escaneos sin texto.');\nconst data = $getWorkflowStaticData('global');\ndata.hnCruceIHSSNomina = data.hnCruceIHSSNomina || {};\ndata.hnCruceIHSSNomina[runId] = data.hnCruceIHSSNomina[runId] || { ihss: [], nomina: [] };\ndata.hnCruceIHSSNomina[runId].ihss = registros;\ndata.hnCruceIHSSNomina[runId].ihssPlanillas = planillas;\ndata.hnCruceIHSSNomina[runId].duplicatePlanillas = duplicatePlanillas;\ndata.hnCruceIHSSNomina[runId].periodoMismatches = periodoMismatches;\nreturn [{ json: { runId, ihssCount: registros.length, planillasCount: planillas.length, duplicatePlanillasCount: duplicatePlanillas.length, periodoMismatchesCount: periodoMismatches.length, pdfFiles: [...new Set(registros.flatMap(r => String(r.archivos_planilla).split(' | ').filter(Boolean)))] } }];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-1184,
|
|
2624
|
|
],
|
|
"id": "62bf8bce-2475-4396-a5d9-b88099531f04",
|
|
"name": "Guardar IHSS en memoria"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const base = $input.all()[0];\nconst bin = base.binary || {};\nconst candidates = [];\nfor (const [key, file] of Object.entries(bin)) {\n const fileName = String(file.fileName || key);\n const mime = String(file.mimeType || '').toLowerCase();\n const lower = fileName.toLowerCase();\n const isExcel = lower.endsWith('.xlsx') || lower.endsWith('.xls') || lower.endsWith('.csv') || mime.includes('spreadsheet') || mime.includes('excel') || key.toLowerCase().includes('nomina');\n if (isExcel && !lower.endsWith('.pdf')) candidates.push([key, file]);\n}\nif (!candidates.length) {\n throw new Error('No encontré el archivo de nómina. Debe ser .xlsx, .xls o .csv en el campo Nomina.');\n}\nif (candidates.length > 1) {\n // Prefer the binary field whose key includes Nomina; otherwise use the first Excel-like file.\n const idx = candidates.findIndex(([key]) => key.toLowerCase().includes('nomina'));\n const chosen = candidates[idx >= 0 ? idx : 0];\n candidates.length = 0;\n candidates.push(chosen);\n}\nconst [key, file] = candidates[0];\nconst data = $getWorkflowStaticData('global');\nif (data.hnCruceIHSSNomina?.[base.json.runId]) data.hnCruceIHSSNomina[base.json.runId].payrollFile = file.fileName || key;\nreturn [{ json: { runId: base.json.runId, mes: base.json.mes, anio: base.json.anio, payrollFile: file.fileName || key }, binary: { data: file } }];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-1664,
|
|
3056
|
|
],
|
|
"id": "3732ae39-2a05-4588-b619-a4704e70b1bd",
|
|
"name": "Preparar nómina"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "update",
|
|
"fileId": {
|
|
"__rl": true,
|
|
"value": "1LNJtm2ITumAFGQwWmHXpoeSOWXBXjdTDpjSBBwqnHd4",
|
|
"mode": "list",
|
|
"cachedResultName": "Nomina Honduras",
|
|
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1LNJtm2ITumAFGQwWmHXpoeSOWXBXjdTDpjSBBwqnHd4/edit"
|
|
},
|
|
"changeFileContent": true,
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.googleDrive",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
-1424,
|
|
3056
|
|
],
|
|
"id": "3755eba7-480b-4045-af77-760a6f5bf221",
|
|
"name": "Actualizar staging Nómina",
|
|
"credentials": {
|
|
"googleDriveOAuth2Api": {
|
|
"id": "g23xdGLZRzBGqKgH",
|
|
"name": "Isaac - Google Drive"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {},
|
|
"type": "n8n-nodes-base.wait",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
-1200,
|
|
3056
|
|
],
|
|
"id": "246710b3-b5f1-449e-9aae-33e7b2fee840",
|
|
"name": "Esperar conversión",
|
|
"webhookId": "wait-conversion-nomina"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $json.id || '1LNJtm2ITumAFGQwWmHXpoeSOWXBXjdTDpjSBBwqnHd4' }}?fields=sheets(properties(title,sheetId,hidden))",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "googleSheetsOAuth2Api",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
-976,
|
|
3056
|
|
],
|
|
"id": "a87dbc44-c2d2-4214-8b76-fcb2e533fed6",
|
|
"name": "Listar hojas Nómina",
|
|
"credentials": {
|
|
"googleSheetsOAuth2Api": {
|
|
"id": "K0hDZh3a85MpOHCs",
|
|
"name": "Google Sheets account 2"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const init = $items('Inicializar memoria')[0].json;\nconst spreadsheetId = $json.spreadsheetId || $node['Actualizar staging Nómina'].json.id || '1LNJtm2ITumAFGQwWmHXpoeSOWXBXjdTDpjSBBwqnHd4';\nfunction norm(s) { return String(s || '').normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, ''); }\nconst ignoreBase = /DO NOT DELETE|NVScriptsProperties|AutoCrat|Resumen|Presupuesto|Retroactivo|Combustible|Combustibles/i;\nconst sheets = ($json.sheets || []).map(s => s.properties || {}).filter(p => p.title && !p.hidden);\nconst out = [];\nfor (const p of sheets) {\n const title = String(p.title || '');\n const n = norm(title);\n if (n === 'ihssrap' || (n.includes('ihss') && n.includes('rap'))) {\n out.push({ json: { runId: init.runId, spreadsheetId, sheetTitle: title, sheetId: p.sheetId, sheetKind: 'ihss_rap' } });\n continue;\n }\n if (!ignoreBase.test(title) && !/IHSS|RAP/i.test(title)) {\n out.push({ json: { runId: init.runId, spreadsheetId, sheetTitle: title, sheetId: p.sheetId, sheetKind: 'nomina_base' } });\n }\n}\nif (!out.length) throw new Error('No encontré hojas válidas de nómina. Revisa que el archivo tenga hojas de pago/base y la hoja IHSS-RAP.');\nreturn out;"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-736,
|
|
3056
|
|
],
|
|
"id": "51341e3b-f409-475f-9792-072e6839d698",
|
|
"name": "Preparar hojas Nómina"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
-512,
|
|
3056
|
|
],
|
|
"id": "874a37ab-e31c-4edd-87ae-342650237ebc",
|
|
"name": "Loop hojas Nómina"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $json.spreadsheetId }}/values/{{ encodeURIComponent(\"'\" + String($json.sheetTitle).replace(/'/g, \"''\") + \"'!A1:AZ1000\") }}?majorDimension=ROWS&valueRenderOption=FORMATTED_VALUE&dateTimeRenderOption=FORMATTED_STRING",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "googleSheetsOAuth2Api",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
-240,
|
|
3248
|
|
],
|
|
"id": "ecb62bd7-c4fc-4785-a790-15c0881f069c",
|
|
"name": "Leer hoja Nómina",
|
|
"credentials": {
|
|
"googleSheetsOAuth2Api": {
|
|
"id": "K0hDZh3a85MpOHCs",
|
|
"name": "Google Sheets account 2"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "function normHeader(s) { return String(s || '').normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, ''); }\nfunction clean(v) { return String(v ?? '').replace(/\\s+/g, ' ').trim(); }\nfunction cleanName(v) { return clean(v); }\nfunction plain(v) { return clean(v).normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase(); }\nfunction toNumber(v) {\n if (v === null || v === undefined || v === '') return 0;\n let s = String(v).replace(/LPS|L|Q|USD|\\$|,/gi, '').replace(/\\s/g, '').trim();\n if (/^\\((.*)\\)$/.test(s)) s = '-' + s.replace(/^\\(|\\)$/g, '');\n const n = Number(s);\n return Number.isFinite(n) ? n : 0;\n}\nfunction money(n) { return Number((Number(n) || 0).toFixed(2)); }\nfunction normalizeId(v) {\n if (v === undefined || v === null) return '';\n let s = String(v).trim();\n if (/^\\d+(\\.0+)?$/.test(s)) s = s.replace(/\\.0+$/, '');\n const d = s.replace(/\\D/g, '');\n if (d.length !== 13 || d === '0000000000000') return '';\n return d;\n}\nfunction findHeader(values) {\n const maxScan = Math.min(values.length, 40);\n for (let r = 0; r < maxScan; r++) {\n const row = values[r] || [];\n const headers = row.map(normHeader);\n const idCol = headers.findIndex(h => h === 'identidad' || h === 'identida' || h === 'cedula' || h === 'idcedula' || h === 'documento' || h === 'nodocumento');\n const nameCol = headers.findIndex(h => h === 'nombre' || h === 'nombrecompleto' || h === 'empleado' || h === 'beneficiario' || h === 'colaborador' || h.startsWith('nombre'));\n if (idCol >= 0 && nameCol >= 0) return { headerRow: r, idCol, nameCol };\n }\n return null;\n}\nfunction isSpecialContextLine(line) {\n return /visado|walmart|personal\\s+especial|personal\\s+para\\s+visado|personal\\s+temporal/i.test(line);\n}\nfunction extractPersonCount(line) {\n const m = plain(line).match(/(\\d{1,3})\\s*(personas?|colaboradores?|empleados?)/i);\n return m ? Number(m[1]) : 0;\n}\nfunction isLikelySpecialName(s) {\n const raw = clean(s);\n const p = plain(raw);\n if (!raw || raw.length < 3) return false;\n if (!/[A-Za-zÁÉÍÓÚÑáéíóúñ]/.test(raw)) return false;\n if (/empleado|patrono|total|planilla|nomina|diferencia|moneda|monto|ihss|rap|observacion|cuota|lps|salario/i.test(p)) return false;\n return true;\n}\nfunction getSpecialPairsFromRow(cells) {\n const pairs = [];\n // Formato observado en GLM Honduras: columnas J/K = referencia corta + monto.\n const candidateColumns = [9, 7, 8, 10, 11];\n for (const idx of candidateColumns) {\n const name = cells[idx] || '';\n const amount = toNumber(cells[idx + 1] || '');\n if (isLikelySpecialName(name) && amount > 0) {\n pairs.push({ referencia: name, monto: money(amount) });\n }\n }\n // Fallback: buscar cualquier par nombre/monto contiguo al final de la fila.\n for (let i = 0; i < cells.length - 1; i++) {\n const name = cells[i] || '';\n const amount = toNumber(cells[i + 1] || '');\n if (isLikelySpecialName(name) && amount > 0) {\n if (!pairs.some(p => plain(p.referencia) === plain(name) && p.monto === money(amount))) {\n pairs.push({ referencia: name, monto: money(amount) });\n }\n }\n }\n return pairs;\n}\n\nconst runId = $items('Inicializar memoria')[0].json.runId;\nlet sheetTitle = '';\nlet sheetKind = 'nomina_base';\ntry { sheetTitle = $node['Loop hojas Nómina'].json.sheetTitle || ''; sheetKind = $node['Loop hojas Nómina'].json.sheetKind || 'nomina_base'; } catch (e) {}\nsheetTitle = sheetTitle || 'Hoja de nómina';\nconst values = $json.values || [];\nconst data = $getWorkflowStaticData('global');\ndata.hnCruceIHSSNomina = data.hnCruceIHSSNomina || {};\ndata.hnCruceIHSSNomina[runId] = data.hnCruceIHSSNomina[runId] || { ihss: [], nomina: [], payrollIhssSummary: {}, payrollIhssBlocks: [], payrollSpecialEmployees: [] };\n\nif (sheetKind === 'ihss_rap') {\n const blocks = [];\n const specialEmployees = [];\n const summary = {\n totalNominaIhss: 0,\n ajusteIhss: 0,\n totalNominaIhssAjustado: 0,\n totalPlanillasIhss: 0,\n diferenciaIhss: 0,\n observacionIhss: '',\n ajusteIhssMotivo: ''\n };\n\n let inIhss = false;\n let specialContextActive = false;\n let specialContextLabel = '';\n let expectedSpecialCount = 0;\n let collectedInContext = 0;\n let specialBlockAmount = 0;\n\n for (const row of values) {\n const cells = (row || []).map(clean);\n const lineOriginal = cells.join(' ');\n const line = plain(lineOriginal);\n\n if (line.includes('ihss') && !line.includes('rap')) inIhss = true;\n if (line.includes('rap aportacion') || line.includes('rap reserva')) {\n inIhss = false;\n specialContextActive = false;\n }\n\n const concept = cells[1] || cells[0] || '';\n const amount = toNumber(cells[3] || cells[2] || '');\n const conceptNorm = plain(concept);\n\n if (conceptNorm.includes('total nomina')) {\n summary.totalNominaIhss = money(amount);\n specialContextActive = false;\n } else if (conceptNorm.includes('total planilla')) {\n summary.totalPlanillasIhss = money(amount);\n specialContextActive = false;\n } else if (inIhss && conceptNorm.includes('diferencia')) {\n summary.diferenciaIhss = money(amount);\n summary.observacionIhss = cells.slice(4).filter(Boolean).join(' ');\n specialContextActive = false;\n } else if (inIhss && concept && amount && !/ihss|moneda|empleado|total|planilla|diferencia/i.test(concept)) {\n blocks.push({ concepto: concept, moneda: cells[2] || '', monto: money(amount) });\n\n // PRODUCCIÓN: un ajuste de personal solo se suma si el bloque lo declara explícitamente\n // como visado/Walmart/personal especial. Esto evita inflar mayo por observaciones normales.\n if (isSpecialContextLine(lineOriginal) || isSpecialContextLine(concept)) {\n specialBlockAmount = money(specialBlockAmount + amount);\n specialContextActive = true;\n specialContextLabel = concept;\n expectedSpecialCount = extractPersonCount(lineOriginal) || extractPersonCount(concept) || 0;\n collectedInContext = 0;\n }\n }\n\n if (inIhss && isSpecialContextLine(lineOriginal)) {\n specialContextActive = true;\n specialContextLabel = concept || lineOriginal;\n expectedSpecialCount = extractPersonCount(lineOriginal) || expectedSpecialCount || 0;\n collectedInContext = 0;\n }\n\n // Caso Honduras marzo: empleados fuera de hojas Base incluidos explícitamente en IHSS-RAP\n // como \"Personal para visado de Walmart\". Solo se leen nombres/montos si ese contexto existe.\n if (inIhss && specialContextActive) {\n const pairs = getSpecialPairsFromRow(cells);\n for (const pair of pairs) {\n if (!specialEmployees.some(e => plain(e.referencia) === plain(pair.referencia) && e.monto === pair.monto)) {\n specialEmployees.push({ referencia: pair.referencia, monto: pair.monto, fuente: sheetTitle, contexto: specialContextLabel || 'Personal especial / visado Walmart' });\n collectedInContext++;\n }\n }\n if (expectedSpecialCount && collectedInContext >= expectedSpecialCount) {\n specialContextActive = false;\n }\n }\n }\n\n const specialTotal = money(specialEmployees.reduce((a, e) => a + Number(e.monto || 0), 0));\n\n // Regla final de producción:\n // - Si hay empleados especiales explícitos, se suma su total.\n // - Si no se leyeron empleados pero hay bloque explícito de visado/Walmart, se suma el monto del bloque.\n // - Si no hay contexto explícito, ajuste = 0.00.\n summary.ajusteIhss = money(specialTotal || specialBlockAmount || 0);\n summary.ajusteIhssMotivo = summary.ajusteIhss\n ? (specialEmployees.length ? 'Personal especial/visado detectado explícitamente en IHSS-RAP.' : 'Bloque especial/visado detectado explícitamente en IHSS-RAP.')\n : 'Sin ajuste especial explícito.';\n summary.totalNominaIhssAjustado = money(summary.totalNominaIhss + summary.ajusteIhss);\n\n // La diferencia productiva de IHSS debe usar TOTAL Nómina ajustado - Total Planillas.\n // Evita capturar diferencias de RAP Aportación / RAP Reserva cuando la hoja tiene varias secciones.\n if (summary.totalNominaIhssAjustado || summary.totalPlanillasIhss) {\n summary.diferenciaIhss = money(summary.totalNominaIhssAjustado - summary.totalPlanillasIhss);\n }\n if (!summary.observacionIhss && Math.abs(summary.diferenciaIhss || 0) > 0.05) {\n summary.observacionIhss = summary.ajusteIhss\n ? 'La hoja IHSS-RAP incluye ajuste de personal especial/visado. Diferencia restante corresponde a redondeo o ajuste menor.'\n : 'La hoja IHSS-RAP muestra diferencia en IHSS. Revisar soporte/observación interna de nómina con Administración.';\n }\n\n data.hnCruceIHSSNomina[runId].payrollIhssSummary = summary;\n data.hnCruceIHSSNomina[runId].payrollIhssBlocks = blocks;\n data.hnCruceIHSSNomina[runId].payrollSpecialEmployees = specialEmployees;\n return [{ json: { runId, sheetTitle, sheetKind, identitiesFound: 0, ihssSummaryFound: Boolean(summary.totalNominaIhss || summary.totalPlanillasIhss), ajusteIhss: summary.ajusteIhss, specialEmployeesFound: specialEmployees.length } }];\n}\n\nconst header = findHeader(values);\nconst records = [];\nif (header) {\n for (let r = header.headerRow + 1; r < values.length; r++) {\n const row = values[r] || [];\n const identidad = normalizeId(row[header.idCol]);\n if (!identidad) continue;\n const nombre = cleanName(row[header.nameCol]);\n if (!nombre || /^total/i.test(nombre)) continue;\n records.push({ identidad, nombre_nomina: nombre, hoja_nomina: sheetTitle });\n }\n}\ndata.hnCruceIHSSNomina[runId].nomina.push(...records);\nreturn [{ json: { runId, sheetTitle, sheetKind, identitiesFound: records.length, headerDetected: Boolean(header), headerRow: header ? header.headerRow + 1 : '' } }];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
0,
|
|
3248
|
|
],
|
|
"id": "492be91a-d0b4-4d39-9304-94f784f3e45b",
|
|
"name": "Acumular identidades Nómina"
|
|
},
|
|
{
|
|
"parameters": {},
|
|
"type": "n8n-nodes-base.merge",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
528,
|
|
2896
|
|
],
|
|
"id": "30adc4e9-78af-4f92-9bd8-bb4d0280deb8",
|
|
"name": "Esperar ramas"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "function clean(s) { return String(s ?? '').replace(/\\s+/g, ' ').trim(); }\nfunction money(n) { return Number((Number(n) || 0).toFixed(2)); }\nfunction fmt(n) { return Number(n || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }\nfunction normalizeId(v) {\n const d = String(v ?? '').replace(/\\D/g, '');\n if (d.length !== 13 || d === '0000000000000') return '';\n return d;\n}\nfunction plain(s) { return clean(s).normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase(); }\nfunction normalizeName(s) {\n return clean(s)\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/[^A-Z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\nconst BAMBOO_TOKEN_EQUIVALENCES = new Map([\n ['VASQUEZ', 'VAZQUEZ']\n]);\nfunction canonicalBambooName(s) {\n return normalizeName(s)\n .split(' ')\n .map(token => BAMBOO_TOKEN_EQUIVALENCES.get(token) || token)\n .join(' ');\n}\nfunction bambooTokens(name) {\n const stop = new Set(['DE','DEL','LA','LAS','LOS','Y','DA','DAS','DO','VDA']);\n return canonicalBambooName(name).split(' ').filter(t => t.length > 1 && !stop.has(t));\n}\nfunction bambooNameScore(a, b) {\n const A = bambooTokens(a);\n const B = bambooTokens(b);\n if (!A.length || !B.length) return 0;\n const setB = new Set(B);\n const shared = A.filter(token => setB.has(token)).length;\n if (shared < 2) return 0;\n return shared / Math.min(A.length, B.length);\n}\nfunction bambooStatusLabel(value) {\n const status = normalizeName(value);\n if (status === 'ACTIVE' || status === 'ACTIVO') return 'Activo';\n if (status === 'INACTIVE' || status === 'INACTIVO') return 'Inactivo';\n return clean(value);\n}\nfunction isBambooActive(value) {\n return bambooStatusLabel(value) === 'Activo';\n}\nfunction buildBambooNameIndex(rows) {\n const exact = new Map();\n const byToken = new Map();\n for (const row of rows || []) {\n const canonical = canonicalBambooName(row.nombre_bamboo || '');\n if (!canonical) continue;\n if (!exact.has(canonical)) exact.set(canonical, []);\n exact.get(canonical).push(row);\n for (const token of new Set(bambooTokens(canonical))) {\n if (!byToken.has(token)) byToken.set(token, []);\n byToken.get(token).push(row);\n }\n }\n return { exact, byToken };\n}\nfunction indexedBambooCandidates(name, index) {\n const counts = new Map();\n for (const token of new Set(bambooTokens(name))) {\n for (const row of index.byToken.get(token) || []) {\n counts.set(row, (counts.get(row) || 0) + 1);\n }\n }\n return [...counts.entries()]\n .filter(([, shared]) => shared >= 2)\n .map(([row]) => row);\n}\nfunction matchBambooByName(name, index) {\n const target = canonicalBambooName(name);\n if (!target) return { record:null, score:0, method:'sin nombre' };\n\n const exact = [...(index.exact.get(target) || [])];\n if (exact.length) {\n exact.sort((a,b) => Number(isBambooActive(b.estado_bamboo)) - Number(isBambooActive(a.estado_bamboo)));\n return { record:exact[0], score:1, method:'nombre exacto' };\n }\n\n const candidates = indexedBambooCandidates(name, index)\n .map(record => ({ record, score:bambooNameScore(name, record.nombre_bamboo || '') }))\n .filter(item => item.score >= 0.70)\n .sort((a,b) => b.score - a.score);\n\n if (!candidates.length) return { record:null, score:0, method:'sin coincidencia' };\n\n const best = candidates[0];\n const second = candidates[1];\n const margin = second ? best.score - second.score : best.score;\n const bestTokens = new Set(bambooTokens(best.record.nombre_bamboo || ''));\n const sharedTokens = bambooTokens(name).filter(token => bestTokens.has(token)).length;\n\n if (sharedTokens >= 2 && (best.score >= 0.85 || margin >= 0.15)) {\n return { record:best.record, score:best.score, method:'nombre parecido' };\n }\n return { record:null, score:0, method:'coincidencia ambigua' };\n}\nfunction bestGroupName(group) {\n const allNames = [...(group?.names || [])].map(clean).filter(Boolean);\n const preferred = allNames.filter(name => !/^IHSS-RAP:/i.test(name));\n const names = preferred.length ? preferred : allNames;\n names.sort((a,b) => bambooTokens(b).length - bambooTokens(a).length || b.length - a.length);\n return names[0] || '';\n}\nfunction dedupe(records, source) {\n const map = new Map();\n for (const r of records || []) {\n const id = normalizeId(r.identidad);\n if (!id) continue;\n if (!map.has(id)) map.set(id, { identidad: id, names: new Set(), sheets: new Set(), files: new Set(), patronales: new Set(), recibos: new Set(), cuota_trabajador: 0, raw: [] });\n const x = map.get(id);\n if (source === 'ihss') {\n if (clean(r.nombre_planilla)) x.names.add(clean(r.nombre_planilla));\n if (clean(r.archivos_planilla)) String(r.archivos_planilla).split(' | ').forEach(v => v && x.files.add(v));\n if (clean(r.numero_patronal)) String(r.numero_patronal).split(' | ').forEach(v => v && x.patronales.add(v));\n if (clean(r.recibo)) String(r.recibo).split(' | ').forEach(v => v && x.recibos.add(v));\n if (r.cuota_trabajador) x.cuota_trabajador = money(r.cuota_trabajador);\n } else {\n if (clean(r.nombre_nomina)) x.names.add(clean(r.nombre_nomina));\n if (clean(r.hoja_nomina)) x.sheets.add(clean(r.hoja_nomina));\n }\n x.raw.push(r);\n }\n return map;\n}\nfunction referenceMatchesName(reference, fullName) {\n const ref = plain(reference);\n const name = plain(fullName);\n if (!ref || !name) return false;\n const refTokens = ref.split(/\\s+/).filter(t => t.length >= 3 && !['para','personal','visado','walmart'].includes(t));\n if (!refTokens.length) return false;\n return refTokens.every(t => name.includes(t) || name.includes(t.replace(/ck/g, 'k')) || name.includes(t.replace(/k/g, 'ck')));\n}\nconst init = $items('Inicializar memoria')[0].json;\nconst runId = init.runId;\nconst data = $getWorkflowStaticData('global');\nconst slot = data.hnCruceIHSSNomina?.[runId];\nif (!slot) throw new Error(`No encontré memoria temporal para la ejecución ${runId}.`);\nconst ihss = dedupe(slot.ihss, 'ihss');\nconst nomina = dedupe(slot.nomina, 'nomina');\nif (ihss.size === 0) throw new Error('No se extrajo ninguna identidad desde las planillas IHSS.');\nif (nomina.size === 0) throw new Error('No se extrajo ninguna identidad desde la nómina.');\nconst bamboo = slot.bamboo || [];\nif (!bamboo.length) throw new Error('No se recibieron empleados de BambooHR para completar el cruce de identidad.');\nconst bambooNameIndex = buildBambooNameIndex(bamboo);\nconst bambooMatchCache = new Map();\nfunction getBambooMatch(name) {\n const key = canonicalBambooName(name);\n if (!bambooMatchCache.has(key)) bambooMatchCache.set(key, matchBambooByName(name, bambooNameIndex));\n return bambooMatchCache.get(key);\n}\nfunction bambooInfoForGroup(group) {\n const nombre = bestGroupName(group);\n const match = getBambooMatch(nombre);\n return {\n nombre,\n match,\n esta_bamboo: match.record ? 'Sí' : 'No',\n fecha_ingreso: match.record?.fecha_ingreso || '',\n estado_bamboo: bambooStatusLabel(match.record?.estado_bamboo || ''),\n nombre_bamboo: match.record?.nombre_bamboo || ''\n };\n}\n\n// Producción Honduras: resolver empleados especiales de IHSS-RAP (ej. visado Walmart) por nombre contra planilla IHSS.\n// Esto evita que aparezcan como \"Solo IHSS\" cuando la nómina los trae solo en el resumen IHSS-RAP sin identidad.\nconst specialEmployees = slot.payrollSpecialEmployees || [];\nconst resolvedSpecialEmployees = [];\nconst unresolvedSpecialEmployees = [];\nfor (const sp of specialEmployees) {\n const candidates = [...ihss.values()].filter(r => {\n if (nomina.has(r.identidad)) return false;\n const names = [...r.names];\n return names.some(n => referenceMatchesName(sp.referencia, n));\n });\n if (candidates.length === 1) {\n const r = candidates[0];\n nomina.set(r.identidad, {\n identidad: r.identidad,\n names: new Set([...r.names, `IHSS-RAP: ${sp.referencia}`]),\n sheets: new Set([sp.fuente || 'IHSS-RAP', 'Personal especial / visado Walmart']),\n files: new Set(), patronales: new Set(), recibos: new Set(), cuota_trabajador: 0,\n raw: [{ identidad: r.identidad, nombre_nomina: [...r.names][0] || sp.referencia, hoja_nomina: sp.fuente || 'IHSS-RAP', fuente_especial: 'Personal especial / visado Walmart', monto: sp.monto }]\n });\n resolvedSpecialEmployees.push({ referencia: sp.referencia, identidad: r.identidad, nombre: [...r.names][0] || '', monto: sp.monto, estado: 'Resuelto por nombre contra IHSS' });\n } else {\n unresolvedSpecialEmployees.push({ referencia: sp.referencia, monto: sp.monto, coincidencias: candidates.length });\n }\n}\n\n// Fallback de producción: si IHSS-RAP trae un bloque especial con monto, pero no se pudieron leer\n// los nombres de las columnas internas, se resuelve contra los empleados que están solo en IHSS.\n// Solo se aplica cuando la cantidad faltante coincide con el monto especial / cuota IHSS por empleado.\nconst payrollAdjustmentForSpecial = money((slot.payrollIhssSummary || {}).ajusteIhss || 0);\nif (!resolvedSpecialEmployees.length && payrollAdjustmentForSpecial > 0.05) {\n const pendingIHSS = [...ihss.values()].filter(r => !nomina.has(r.identidad));\n const avgCuota = pendingIHSS.length\n ? money(pendingIHSS.reduce((a, r) => a + Number(r.cuota_trabajador || 0), 0) / pendingIHSS.length)\n : 0;\n const expectedSpecialCount = avgCuota ? Math.round(payrollAdjustmentForSpecial / avgCuota) : 0;\n const amountMatches = avgCuota && Math.abs(payrollAdjustmentForSpecial - money(avgCuota * pendingIHSS.length)) <= 1.00;\n if (pendingIHSS.length > 0 && pendingIHSS.length === expectedSpecialCount && amountMatches) {\n for (const r of pendingIHSS) {\n const nm = [...r.names][0] || '';\n nomina.set(r.identidad, {\n identidad: r.identidad,\n names: new Set([...r.names, 'IHSS-RAP: Personal especial / visado Walmart']),\n sheets: new Set(['IHSS-RAP', 'Personal especial / visado Walmart']),\n files: new Set(), patronales: new Set(), recibos: new Set(), cuota_trabajador: 0,\n raw: [{ identidad: r.identidad, nombre_nomina: nm, hoja_nomina: 'IHSS-RAP', fuente_especial: 'Personal especial / visado Walmart', monto: r.cuota_trabajador }]\n });\n resolvedSpecialEmployees.push({ referencia: 'Personal especial / visado Walmart', identidad: r.identidad, nombre: nm, monto: r.cuota_trabajador, estado: 'Resuelto por monto contra empleados solo IHSS' });\n }\n }\n}\n\nconst nominaBamboo = [...nomina.values()].map(group => ({\n identidad: group.identidad,\n hojas: [...group.sheets].join(' | '),\n ...bambooInfoForGroup(group)\n}));\nconst nominaBambooById = new Map(nominaBamboo.map(row => [row.identidad, row]));\nconst nominaSinBamboo = nominaBamboo.filter(row => !row.match.record);\n\nconst soloIHSS = [...ihss.values()].filter(r => !nomina.has(r.identidad));\nconst soloNomina = [...nomina.values()].filter(r => !ihss.has(r.identidad));\nconst ambas = [...ihss.keys()].filter(id => nomina.has(id)).length;\nconst diferenciasIdentidad = soloIHSS.length + soloNomina.length;\nconst planillas = slot.ihssPlanillas || [];\nconst duplicatePlanillas = slot.duplicatePlanillas || [];\nconst periodoMismatches = slot.periodoMismatches || [];\nconst pdfTrabajadores = planillas.reduce((a,p) => a + Number(p.trabajadores || 0), 0);\nconst pdfSalario = money(planillas.reduce((a,p) => a + Number(p.salario_reportado || 0), 0));\nconst pdfCuotaTrabajador = money(planillas.reduce((a,p) => a + Number(p.cuota_trabajador || 0), 0));\nconst pdfCuotaPatrono = money(planillas.reduce((a,p) => a + Number(p.cuota_patrono || 0), 0));\nconst pdfTotalPlanilla = money(planillas.reduce((a,p) => a + Number(p.total_planilla || 0), 0));\nconst payroll = slot.payrollIhssSummary || {};\nconst payrollBaseIhss = money(payroll.totalNominaIhss || 0);\nconst payrollAdjustmentIhss = money(payroll.ajusteIhss || 0);\nconst payrollIhss = money(payroll.totalNominaIhssAjustado || (payrollBaseIhss + payrollAdjustmentIhss));\nconst diffMontos = money(payrollIhss - pdfCuotaTrabajador);\nlet resultadoMontos = 'SIN DIFERENCIAS';\nlet lecturaMontos = 'El total descontado en nómina por IHSS coincide con la cuota trabajador de las planillas IHSS.';\nif (Math.abs(diffMontos) > 0.05) {\n resultadoMontos = 'REVISAR DIFERENCIA';\n lecturaMontos = diffMontos > 0 ? 'La nómina descontó más IHSS que lo reportado como cuota trabajador en las planillas.' : 'La nómina descontó menos IHSS que lo reportado como cuota trabajador en las planillas.';\n}\nfunction titleCaseName(s) {\n return String(s || '')\n .toLowerCase()\n .replace(/\\b([a-záéíóúñü])/gi, c => c.toUpperCase())\n .replace(/\\bDe\\b/g, 'de')\n .replace(/\\bDel\\b/g, 'del')\n .replace(/\\bLa\\b/g, 'la');\n}\nfunction inferEmployeeFromObservation(obs) {\n const text = clean(obs);\n const patterns = [\n /(?:a|de)\\s+([A-ZÁÉÍÓÚÑ][A-Za-zÁÉÍÓÚÑáéíóúñü\\s]{6,80}?)(?:\\s+en\\s+|\\s+por\\s+|\\.|,|$)/i,\n /emplead[oa]\\s+([A-ZÁÉÍÓÚÑ][A-Za-zÁÉÍÓÚÑáéíóúñü\\s]{6,80}?)(?:\\s+en\\s+|\\s+por\\s+|\\.|,|$)/i\n ];\n for (const p of patterns) {\n const m = text.match(p);\n if (m && m[1]) {\n const name = clean(m[1]).replace(/\\s+(1Q|2Q|Colgate|Motorola|Nestle|Whirlpool|GLM).*$/i, '');\n if (name.length >= 6) return titleCaseName(name);\n }\n }\n return '';\n}\nconst observacionMonto = payroll.observacionIhss || '';\nconst empleadoObs = inferEmployeeFromObservation(observacionMonto);\nconst casosMonto = [];\nif (Math.abs(diffMontos) > 0.05) {\n casosMonto.push({\n tipo: Math.abs(diffMontos) <= 1 ? 'Diferencia menor / redondeo' : (diffMontos > 0 ? 'Cobro de más en nómina' : 'Cobro de menos en nómina'),\n empleado: Math.abs(diffMontos) <= 1 ? 'Redondeo total' : (empleadoObs || 'No identificado automáticamente'),\n monto: Math.abs(diffMontos),\n fuente: observacionMonto ? 'Hoja IHSS-RAP' : 'Diferencia total',\n observacion: observacionMonto || 'La nómina y las planillas tienen diferencia, pero la hoja IHSS-RAP no trae una observación con el empleado específico.',\n accion: Math.abs(diffMontos) <= 1 ? 'Aceptar como diferencia menor si Administración confirma redondeo.' : (empleadoObs ? 'Revisar este caso con Nómina/Administración.' : 'Revisar detalle interno de nómina para identificar el colaborador.')\n });\n}\nconst identidadRows = [];\nidentidadRows.push(['Cruce de identidad - Honduras']);\nidentidadRows.push([`Período: ${init.periodoLabel || `${init.mes}/${init.anio}`}`]);\nidentidadRows.push(['Resultado', diferenciasIdentidad ? 'REVISAR DIFERENCIAS' : 'SIN DIFERENCIAS']);\nidentidadRows.push(['']);\nidentidadRows.push(['Resumen', 'Cantidad']);\nidentidadRows.push(['Identidades únicas en planillas IHSS', ihss.size]);\nidentidadRows.push(['Identidades únicas en nómina', nomina.size]);\nidentidadRows.push(['Coinciden en ambas', ambas]);\nidentidadRows.push(['Diferencias encontradas', diferenciasIdentidad]);\nidentidadRows.push(['Personal especial IHSS-RAP resuelto', resolvedSpecialEmployees.length]);\nidentidadRows.push(['']);\nidentidadRows.push(['Lectura rápida']);\nidentidadRows.push([diferenciasIdentidad ? 'Hay empleados que aparecen en una fuente y no en la otra. Revisar las secciones de abajo.' : 'No hay empleados faltantes ni sobrantes entre nómina y planillas IHSS.']);\nidentidadRows.push(['']);\nidentidadRows.push(['Personal especial resuelto desde IHSS-RAP']);\nidentidadRows.push(['Referencia', 'Identidad resuelta', 'Nombre en IHSS', 'Monto', 'Estado']);\nif (resolvedSpecialEmployees.length) for (const r of resolvedSpecialEmployees) identidadRows.push([r.referencia, r.identidad, r.nombre, r.monto, r.estado]); else identidadRows.push(['No aplica', '', '', '', '']);\nif (unresolvedSpecialEmployees.length) {\n identidadRows.push(['']);\n identidadRows.push(['Personal especial NO resuelto automáticamente']);\n identidadRows.push(['Referencia', 'Monto', 'Coincidencias posibles']);\n for (const r of unresolvedSpecialEmployees) identidadRows.push([r.referencia, r.monto, r.coincidencias]);\n}\nidentidadRows.push(['']);\nidentidadRows.push(['Empleados en planilla IHSS que NO aparecen en nómina']);\nidentidadRows.push(['Identidad', 'Nombre en planilla IHSS', 'Número patronal', 'Archivo planilla', 'Qué significa', 'Está en Bamboo?', 'Fecha de Ingreso', 'Activo/Inactivo', 'Nombre en BambooHR']);\nif (soloIHSS.length) {\n for (const r of soloIHSS) {\n const info = bambooInfoForGroup(r);\n identidadRows.push([\n r.identidad,\n [...r.names].join(' | '),\n [...r.patronales].join(' | '),\n [...r.files].join(' | '),\n 'Está en IHSS, pero no aparece en la nómina cargada.',\n info.esta_bamboo,\n info.fecha_ingreso,\n info.estado_bamboo,\n info.nombre_bamboo\n ]);\n }\n} else {\n identidadRows.push(['Sin diferencias', '', '', '', '', '', '', '', '']);\n}\nidentidadRows.push(['']);\nidentidadRows.push(['Empleados en nómina que NO aparecen en planilla IHSS']);\nidentidadRows.push(['Identidad', 'Nombre en nómina', 'Hoja nómina', 'Qué significa', 'Está en Bamboo?', 'Fecha de Ingreso', 'Activo/Inactivo', 'Nombre en BambooHR']);\nif (soloNomina.length) {\n for (const r of soloNomina) {\n const info = nominaBambooById.get(r.identidad) || bambooInfoForGroup(r);\n identidadRows.push([\n r.identidad,\n [...r.names].join(' | '),\n [...r.sheets].join(' | '),\n 'Está en nómina, pero no aparece en las planillas IHSS cargadas.',\n info.esta_bamboo,\n info.fecha_ingreso,\n info.estado_bamboo,\n info.nombre_bamboo\n ]);\n }\n} else {\n identidadRows.push(['Sin diferencias', '', '', '', '', '', '', '']);\n}\nif (duplicatePlanillas.length) {\n identidadRows.push(['']);\n identidadRows.push(['Planillas duplicadas ignoradas']);\n identidadRows.push(['Archivo duplicado', 'Archivo original', 'Número patronal', 'Recibo', 'Período', 'Cuota trabajador']);\n for (const d of duplicatePlanillas) identidadRows.push([d.archivo_duplicado, d.archivo_original, d.numero_patronal, d.recibo, d.periodo, d.cuota_trabajador]);\n}\nif (periodoMismatches.length) {\n identidadRows.push(['']);\n identidadRows.push(['Alertas de período']);\n identidadRows.push(['Archivo', 'Período detectado', 'Período esperado']);\n for (const p of periodoMismatches) identidadRows.push([p.archivo_planilla, p.periodo_detectado, p.periodo_esperado]);\n}\n\nconst nominaSinBambooRows = [];\nnominaSinBambooRows.push(['Nómina sin BambooHR']);\nnominaSinBambooRows.push([`Período: ${init.periodoLabel || `${init.mes}/${init.anio}`}`]);\nnominaSinBambooRows.push(['Resultado', nominaSinBamboo.length ? 'REVISAR EMPLEADOS NO ENCONTRADOS' : 'SIN DIFERENCIAS']);\nnominaSinBambooRows.push(['']);\nnominaSinBambooRows.push(['Resumen', 'Cantidad']);\nnominaSinBambooRows.push(['Empleados únicos de nómina evaluados', nominaBamboo.length]);\nnominaSinBambooRows.push(['Empleados de nómina no encontrados en BambooHR', nominaSinBamboo.length]);\nnominaSinBambooRows.push(['']);\nnominaSinBambooRows.push(['Lectura rápida']);\nnominaSinBambooRows.push([nominaSinBamboo.length\n ? 'Estos empleados aparecen en la nómina de Honduras, pero no se encontró una coincidencia suficientemente clara en BambooHR.'\n : 'Todos los empleados únicos de nómina fueron encontrados en BambooHR.']);\nnominaSinBambooRows.push(['']);\nnominaSinBambooRows.push(['Empleados de nómina que NO aparecen en BambooHR']);\nnominaSinBambooRows.push(['Identidad', 'Nombre en nómina', 'Hoja(s) nómina', 'Está en Bamboo?', 'Fecha de Ingreso', 'Activo/Inactivo', 'Nombre en BambooHR', 'Qué significa']);\nif (nominaSinBamboo.length) {\n for (const row of nominaSinBamboo.sort((a,b) => String(a.nombre).localeCompare(String(b.nombre)))) {\n nominaSinBambooRows.push([\n row.identidad,\n row.nombre,\n row.hojas,\n 'No',\n '',\n '',\n '',\n 'Está en nómina, pero no se encontró en BambooHR por nombre exacto o coincidencia clara.'\n ]);\n }\n} else {\n nominaSinBambooRows.push(['Sin diferencias', '', '', '', '', '', '', '']);\n}\n\nconst montosRows = [];\nmontosRows.push(['Cruce de montos IHSS - Honduras']);\nmontosRows.push([`Período: ${init.periodoLabel || `${init.mes}/${init.anio}`}`]);\nmontosRows.push(['Resultado', resultadoMontos]);\nmontosRows.push(['']);\nmontosRows.push(['Resumen', 'Monto']);\nmontosRows.push(['IHSS descontado en nómina base', payrollBaseIhss]);\nmontosRows.push(['Ajuste IHSS personal especial / visado', payrollAdjustmentIhss]);\nmontosRows.push(['IHSS descontado en nómina ajustado', payrollIhss]);\nmontosRows.push(['Cuota trabajador según planillas IHSS', pdfCuotaTrabajador]);\nmontosRows.push(['Diferencia', diffMontos]);\nmontosRows.push(['']);\nmontosRows.push(['Lectura rápida']);\nmontosRows.push([lecturaMontos]);\nmontosRows.push(['']);\nmontosRows.push(['Observación en nómina']);\nmontosRows.push([payroll.observacionIhss || (Math.abs(diffMontos) > 0.05 ? 'No se encontró observación en la hoja IHSS-RAP. Revisar con Administración.' : 'Sin observación necesaria.')]);\nmontosRows.push(['']);\nmontosRows.push(['Casos con diferencia']);\nmontosRows.push(['Tipo', 'Empleado / referencia', 'Monto diferencia', 'Fuente', 'Acción sugerida']);\nif (casosMonto.length) {\n for (const c of casosMonto) montosRows.push([c.tipo, c.empleado, c.monto, c.fuente, c.accion]);\n} else {\n montosRows.push(['Sin diferencias', '', '', '', '']);\n}\nmontosRows.push(['']);\nmontosRows.push(['Detalle / observación del caso']);\nif (casosMonto.length) {\n for (const c of casosMonto) montosRows.push([c.observacion]);\n} else {\n montosRows.push(['No aplica.']);\n}\nmontosRows.push(['']);\nmontosRows.push(['Qué se compara']);\nmontosRows.push(['Nómina', 'Columna / dato', 'Explicación']);\nmontosRows.push(['Nómina Honduras', 'Hoja IHSS-RAP > TOTAL Nomina + ajustes especiales', 'Total mensual descontado al colaborador por IHSS, incluyendo personal especial si está documentado en IHSS-RAP.']);\nmontosRows.push(['Planillas IHSS', 'CUOTA TRABAJADOR', 'Total que IHSS espera cobrar a los colaboradores. No incluye cuota patronal.']);\nconst resumenRows = [];\nresumenRows.push(['Resumen de planillas IHSS - Honduras']);\nresumenRows.push([`Período: ${init.periodoLabel || `${init.mes}/${init.anio}`}`]);\nresumenRows.push(['']);\nresumenRows.push(['Archivo PDF', 'Número patronal', 'Recibo', 'Trabajadores', 'Salario reportado', 'Cuota trabajador', 'Cuota patrono', 'Total planilla']);\nfor (const p of planillas) resumenRows.push([p.archivo_planilla, p.numero_patronal, p.recibo, p.trabajadores, p.salario_reportado, p.cuota_trabajador, p.cuota_patrono, p.total_planilla]);\nresumenRows.push(['TOTAL', '', '', pdfTrabajadores, pdfSalario, pdfCuotaTrabajador, pdfCuotaPatrono, pdfTotalPlanilla]);\nif (duplicatePlanillas.length) {\n resumenRows.push(['']);\n resumenRows.push(['Duplicados ignorados']);\n resumenRows.push(['Archivo duplicado', 'Archivo original', 'Número patronal', 'Recibo', 'Cuota trabajador']);\n for (const d of duplicatePlanillas) resumenRows.push([d.archivo_duplicado, d.archivo_original, d.numero_patronal, d.recibo, d.cuota_trabajador]);\n}\nconst detalleRows = [];\ndetalleRows.push(['Detalle IHSS según nómina - Honduras']);\ndetalleRows.push([`Período: ${init.periodoLabel || `${init.mes}/${init.anio}`}`]);\ndetalleRows.push(['']);\ndetalleRows.push(['Concepto / bloque', 'Moneda', 'Monto IHSS nómina']);\nfor (const b of (slot.payrollIhssBlocks || [])) detalleRows.push([b.concepto, b.moneda, b.monto]);\ndetalleRows.push(['TOTAL Nómina IHSS base', 'Lps', payrollBaseIhss]);\ndetalleRows.push(['Ajuste personal especial / visado', 'Lps', payrollAdjustmentIhss]);\ndetalleRows.push(['Motivo ajuste especial', '', payroll.ajusteIhssMotivo || (payrollAdjustmentIhss ? 'Ajuste especial detectado.' : 'Sin ajuste especial explícito.')]);\ndetalleRows.push(['TOTAL Nómina IHSS ajustado', 'Lps', payrollIhss]);\ndetalleRows.push(['Total Planillas según nómina', 'Lps', money(payroll.totalPlanillasIhss || 0)]);\ndetalleRows.push(['Diferencia según nómina ajustada', 'Lps', diffMontos]);\ndetalleRows.push(['Observación', '', payroll.observacionIhss || '']);\nif (specialEmployees.length) {\n detalleRows.push(['']);\n detalleRows.push(['Personal especial detectado en IHSS-RAP']);\n detalleRows.push(['Referencia', 'Monto', 'Identidad resuelta', 'Nombre resuelto']);\n for (const sp of specialEmployees) {\n const res = resolvedSpecialEmployees.find(r => plain(r.referencia) === plain(sp.referencia));\n detalleRows.push([sp.referencia, sp.monto, res?.identidad || '', res?.nombre || '']);\n }\n}\nconst summary = { ihss: ihss.size, nomina: nomina.size, ambas, soloIHSS: soloIHSS.length, soloNomina: soloNomina.length, diferenciasIdentidad, empleadosBamboo: bamboo.length, nominaBambooEvaluados: nominaBamboo.length, nominaSinBamboo: nominaSinBamboo.length, pdfFiles: slot.pdfFiles || [], payrollFile: slot.payrollFile || '', planillas: planillas.length, duplicatePlanillas: duplicatePlanillas.length, duplicatePlanillasDetalle: duplicatePlanillas, periodoMismatches: periodoMismatches.length, periodoMismatchesDetalle: periodoMismatches, pdfTrabajadores, pdfCuotaTrabajador, pdfCuotaPatrono, pdfTotalPlanilla, payrollBaseIhss, payrollAdjustmentIhss, payrollIhss, diffMontos, resultadoMontos, observacionIhss: payroll.observacionIhss || '', ajusteIhssMotivo: payroll.ajusteIhssMotivo || '', specialEmployees, resolvedSpecialEmployees, unresolvedSpecialEmployees, casosMonto, casosMontoCount: casosMonto.length, casoMontoPrincipal: casosMonto[0] || null };\ndelete data.hnCruceIHSSNomina[runId];\nreturn [{ json: { runId, reportTitle: init.reportTitle, emailTo: init.emailTo || 'iaracena@gomezleemarketing.com', identidadRows, nominaSinBambooRows, montosRows, resumenRows, detalleRows, summary, rowCounts: { identidad: identidadRows.length, nominaSinBamboo: nominaSinBambooRows.length, montos: montosRows.length, resumen: resumenRows.length, detalle: detalleRows.length } } }];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
752,
|
|
2896
|
|
],
|
|
"id": "a511235b-ed04-4a0a-80ec-7637d18b1fb9",
|
|
"name": "Construir reporte producción"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "https://sheets.googleapis.com/v4/spreadsheets",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "googleSheetsOAuth2Api",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ properties: { title: $json.reportTitle }, sheets: [ { properties: { title: 'Cruce Identidad' } }, { properties: { title: 'Nómina sin BambooHR' } }, { properties: { title: 'Cruce Montos IHSS' } }, { properties: { title: 'Resumen Planillas' } }, { properties: { title: 'Detalle Nómina IHSS' } } ] }) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
992,
|
|
2896
|
|
],
|
|
"id": "8e4dfca4-24d3-4ee9-8c8d-65ad2fa9b6b0",
|
|
"name": "Crear Google Sheet Reporte",
|
|
"credentials": {
|
|
"googleSheetsOAuth2Api": {
|
|
"id": "K0hDZh3a85MpOHCs",
|
|
"name": "Google Sheets account 2"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $json.spreadsheetId }}/values:batchUpdate",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "googleSheetsOAuth2Api",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ valueInputOption: 'USER_ENTERED', data: [ { range: 'Cruce Identidad!A1', majorDimension: 'ROWS', values: $('Construir reporte producción').item.json.identidadRows }, { range: 'Nómina sin BambooHR!A1', majorDimension: 'ROWS', values: $('Construir reporte producción').item.json.nominaSinBambooRows }, { range: 'Cruce Montos IHSS!A1', majorDimension: 'ROWS', values: $('Construir reporte producción').item.json.montosRows }, { range: 'Resumen Planillas!A1', majorDimension: 'ROWS', values: $('Construir reporte producción').item.json.resumenRows }, { range: 'Detalle Nómina IHSS!A1', majorDimension: 'ROWS', values: $('Construir reporte producción').item.json.detalleRows } ] }) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
1216,
|
|
2896
|
|
],
|
|
"id": "a0b3c6f4-538f-43b3-b7b0-7ff75becbad7",
|
|
"name": "Escribir reporte",
|
|
"credentials": {
|
|
"googleSheetsOAuth2Api": {
|
|
"id": "K0hDZh3a85MpOHCs",
|
|
"name": "Google Sheets account 2"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}:batchUpdate",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "googleSheetsOAuth2Api",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ (() => {\n const sheets = $('Crear Google Sheet Reporte').item.json.sheets || [];\n const id = title => (sheets.find(s => s.properties.title === title) || {}).properties.sheetId;\n const requests = [];\n const titles = ['Cruce Identidad','Nómina sin BambooHR','Cruce Montos IHSS','Resumen Planillas','Detalle Nómina IHSS'];\n for (const title of titles) {\n const sid = id(title);\n if (sid === undefined) continue;\n requests.push(\n { repeatCell: { range: { sheetId: sid, startRowIndex: 0, endRowIndex: 1 }, cell: { userEnteredFormat: { textFormat: { bold: true, fontSize: 14 }, backgroundColor: { red: 0.91, green: 0.96, blue: 0.91 } } }, fields: 'userEnteredFormat.textFormat,userEnteredFormat.backgroundColor' } },\n { autoResizeDimensions: { dimensions: { sheetId: sid, dimension: 'COLUMNS', startIndex: 0, endIndex: 10 } } }\n );\n }\n const sidId = id('Cruce Identidad');\n if (sidId !== undefined) {\n requests.push({ repeatCell: { range: { sheetId: sidId, startRowIndex: 4, endRowIndex: 5 }, cell: { userEnteredFormat: { textFormat: { bold: true }, backgroundColor: { red: 0.89, green: 0.95, blue: 1 } } }, fields: 'userEnteredFormat.textFormat,userEnteredFormat.backgroundColor' } });\n requests.push({ repeatCell: { range: { sheetId: sidId, startRowIndex: 14, endRowIndex: 15 }, cell: { userEnteredFormat: { textFormat: { bold: true }, backgroundColor: { red: 0.89, green: 0.95, blue: 1 } } }, fields: 'userEnteredFormat.textFormat,userEnteredFormat.backgroundColor' } });\n requests.push({ repeatCell: { range: { sheetId: sidId, startRowIndex: 19, endRowIndex: 20 }, cell: { userEnteredFormat: { textFormat: { bold: true }, backgroundColor: { red: 0.89, green: 0.95, blue: 1 } } }, fields: 'userEnteredFormat.textFormat,userEnteredFormat.backgroundColor' } });\n }\n const sidMontos = id('Cruce Montos IHSS');\n if (sidMontos !== undefined) {\n requests.push({ repeatCell: { range: { sheetId: sidMontos, startRowIndex: 4, endRowIndex: 8, startColumnIndex: 1, endColumnIndex: 2 }, cell: { userEnteredFormat: { numberFormat: { type: 'NUMBER', pattern: '#,##0.00' } } }, fields: 'userEnteredFormat.numberFormat' } });\n requests.push({ repeatCell: { range: { sheetId: sidMontos, startRowIndex: 4, endRowIndex: 5 }, cell: { userEnteredFormat: { textFormat: { bold: true }, backgroundColor: { red: 0.89, green: 0.95, blue: 1 } } }, fields: 'userEnteredFormat.textFormat,userEnteredFormat.backgroundColor' } });\n requests.push({ repeatCell: { range: { sheetId: sidMontos, startRowIndex: 15, endRowIndex: 17 }, cell: { userEnteredFormat: { textFormat: { bold: true }, backgroundColor: { red: 1, green: 0.95, blue: 0.85 } } }, fields: 'userEnteredFormat.textFormat,userEnteredFormat.backgroundColor' } });\n requests.push({ repeatCell: { range: { sheetId: sidMontos, startRowIndex: 16, startColumnIndex: 2, endColumnIndex: 3 }, cell: { userEnteredFormat: { numberFormat: { type: 'NUMBER', pattern: '#,##0.00' } } }, fields: 'userEnteredFormat.numberFormat' } });\n }\n const sidResumen = id('Resumen Planillas');\n if (sidResumen !== undefined) {\n requests.push({ repeatCell: { range: { sheetId: sidResumen, startRowIndex: 3, endRowIndex: 4 }, cell: { userEnteredFormat: { textFormat: { bold: true }, backgroundColor: { red: 0.89, green: 0.95, blue: 1 } } }, fields: 'userEnteredFormat.textFormat,userEnteredFormat.backgroundColor' } });\n requests.push({ repeatCell: { range: { sheetId: sidResumen, startRowIndex: 4, startColumnIndex: 4, endColumnIndex: 8 }, cell: { userEnteredFormat: { numberFormat: { type: 'NUMBER', pattern: '#,##0.00' } } }, fields: 'userEnteredFormat.numberFormat' } });\n }\n const sidDetalle = id('Detalle Nómina IHSS');\n if (sidDetalle !== undefined) {\n requests.push({ repeatCell: { range: { sheetId: sidDetalle, startRowIndex: 3, endRowIndex: 4 }, cell: { userEnteredFormat: { textFormat: { bold: true }, backgroundColor: { red: 0.89, green: 0.95, blue: 1 } } }, fields: 'userEnteredFormat.textFormat,userEnteredFormat.backgroundColor' } });\n requests.push({ repeatCell: { range: { sheetId: sidDetalle, startRowIndex: 4, startColumnIndex: 2, endColumnIndex: 3 }, cell: { userEnteredFormat: { numberFormat: { type: 'NUMBER', pattern: '#,##0.00' } } }, fields: 'userEnteredFormat.numberFormat' } });\n }\n return JSON.stringify({ requests });\n})() }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
1456,
|
|
2896
|
|
],
|
|
"id": "ae6f453b-8a53-473c-b51f-4a55790e6c53",
|
|
"name": "Formatear reporte",
|
|
"credentials": {
|
|
"googleSheetsOAuth2Api": {
|
|
"id": "K0hDZh3a85MpOHCs",
|
|
"name": "Google Sheets account 2"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"sendTo": "iaracena@gomezleemarketing.com, ymadera@gomezleemarketing.com, msoto@gomezleemarketing.com, asrodriguez@gomezleemarketing.com, iherrera@gomezleemarketing.com, mgomez@gomezleemarketing.com, vparamo@gomezleemarketing.com",
|
|
"subject": "={{ (() => { const init = $('Inicializar memoria').item.json || {}; return `Cruce Seguridad Social Honduras - ${init.periodoLabel || (init.mes + '/' + init.anio)}`; })() }}",
|
|
"message": "={{ (() => {\n const summary = $('Construir reporte producción').item.json.summary || {};\n const sheetUrl = $('Crear Google Sheet Reporte').item.json.spreadsheetUrl || '#';\n const init = $('Inicializar memoria').item.json || {};\n const periodo = init.periodoLabel || `${init.mes}/${init.anio}`;\n const fmt = n => Number(n || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\n const diferenciasIdentidad = Number(summary.diferenciasIdentidad || 0);\n const diffMontos = Number(summary.diffMontos || 0);\n const diffReal = Math.abs(diffMontos) > 0.05;\n const statusText = diferenciasIdentidad || diffReal ? 'COMPLETADO · REVISAR HALLAZGOS' : 'COMPLETADO · SIN DIFERENCIAS';\n const statusBg = diferenciasIdentidad || diffReal ? '#fff7e6' : '#eefaf0';\n const statusBorder = diferenciasIdentidad || diffReal ? '#f59e0b' : '#5fbf47';\n const statusColor = diferenciasIdentidad || diffReal ? '#92400e' : '#2f7d32';\n const logo = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAV4AAABtCAYAAAGetTk7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAQzFJREFUeNpi/P//P8NQAkwMQwwMfQd/+fqd79GzVyowDBJ7+ea9Ioj989dvdhC/edryPej6+uavXweiP3z6IgpS++7DZ3kQH9msZy/fKvz4/ZXzx5+vHCAM0wvj//zznQ3d3P8MaEkWlIZheMWWAx3IfBBOqur/j8x///GzKEisc9aqbTCx8ML2/yCx3ObpL9H1w3BsWc//X79+syCLte6LPIquLmO9Fop9qes0UfgsyI5//e4jOFQSK/vB3mJiYsSIks9fv0uA6Eu3H3rCxOQlxa68+fBJ59uPn/wgh8PE57UVgg1Irp7wf1FnMZgdv1bhPzeLAIqZWRsN4Hr+MzLjTRIoDnaxNpxU0DbzfpS3febHL98kNx04WcfEwMiwfvexGh5OjjeLN++fDrN4SVcJI8ghIM6c1gIw21BDeR0jE8MfVTmp/TAzQeIRnnaZu4+cywDxq+1WWzIxMoMd2HU46gSILrKeZwFJGt8F+o/F73j56b46iL//7vJURX7908huZBzSxdq/f/+GloNbpq/YC6L9sppQgr2wfdYDZH5wXhvBaAF6ngVdDJ++v//+obhl4qKNq4kq1r5++8GzaVodSm7TU1fcElXchRL8EUWdVE1LzExM/1JqJr6H8XNj/MJAdBiwBBo+aXjUwTQAAAE0miRGHYyvaoa1rmBsOSmxO8hiMD4+AFLLw8X5G8T++PUbGzMj439k85BbaRws3D/Q+egtNUYGRtytNfSWGQiDyltk+eev3ymgq4PxsenHZ3bmBn1Uc9Yq/8cnj9FaQ2+pzQU2athZWTBy5VxoYwdGz2rKg5uD3lr7/fsPa2r95F8LoC035JYZOp+dmYf0JAFsqYFaVYwrth+aDlbAzMQIbWmhWDS9PpstoaLv/4KOIsa/f//B24SRXvZJMHZl7/xzz1+/N4z2cQC31lxtjGbAWmYgO/qOJh1H4jMA+SdefX4IDGbMan14FWuD2RMoDvaFttKq+xdeQhYPyGnB8EFqzcRvMPbTl28UDp+5EkGJQ/78/cvcPWfNVpLS8GZgK+3h89fqyGIxpd3//Z3Mp6JrnN2SzwVj985fv/nu4xc6tiY6K8guX5mZ/zIyMo5WzaMOJhUABNCQSxKjKWIUjAbwgLbekQGo3YhPHtq2/ApkcuFT8+LNewVgXX//+89fDMt6yhgJ2QfSA2qTZjRM+YXP3Mjirv9cHGx43ZewRvY/F6swVvlPv18zLAl5yoje3kUGKoLGe4vs5rpgkwPp+/PvF8OswGuMJAfwlMWblsMc+eTFGwM84cwFbAuD2sRM2ALs8fPXuqysLJ9BfAlhwXsgsZlNuczAdgN8FGnhhj29h05fKeoqTRYv6577EiSW2TgVrKcxN0YJl8XLe1EjC3kkFSY2M+AWOza9e24vztp6a1o/iN3vfRyrmrSNmj9vvT/tTJMUjFz3yUiIXEBvvLGzsYIDH1ZZ4rEDblJfZaoyLCB+/PzNsLSnlDGlZuJ/UMpe2l2K0mmBmQm0+z4+Dxw8dTlq0ca9S3HlDnYWzl/YG4ZsfwipYWCgvAGAtxUB7Ep+BSYzLia08PvxC9Uz0SVdwABnQ4ugfwxSYsKnWwvjzUBFRFXfwvvz2hApq2Pmqh23Hz1zn92czwiblAAFcGrtpD8wdVFAczmAEYkt/kBqEip6/zMxMeMpIvIZE9fKYS0i/v7/zZBk1BlqLuezBlcR8fvvV4bZQbcZi7faPvv+57Mkuvy3328ZFoQ8Zhxtpg3GVkTL9BU70cVqJy46g0v9ldsP7L5//8mNSx55muDUpZs+2NSAOrEg+s37T9KUeiwot42slLP7yLl0GBtXZxiUs+C5F1hckhXAsLmU5VsPth49dzUUmoWxVtmgWV4dVYVDnJzsX/F6Oqf1f+uMFbvN9NS34FITVtjxX0SQ7yku+dXbD1ev2X64Elj2BuPNmozkRYyrjdHMzftOFuKST6zs/wsqHkGzzxFFHf9//fpDXiX389dv8HhcpLd9NUwMWJHwYkbEfyYebs5PMP77j5/FBPl5X2FEwvefDDtnN4K9PXXJ5oXZMb7x6GqkxUUerOqvYIQN2WyYUoMRTEu2HGiBse3NdHEGI8g+coGvk3k/pK75jeHf+e2FzJ+/fBPMaJx6HzR6wsbCMtpVHu3JjQbwKCAXAATQaBExmnpHA3cUjAbuaOCOBu4oYCCvx4YYB3ir/PfvX4yeEbDb+pKLkwM8Nvv9x0+u1+8+SsHkODnY/4gK8T9A6sencnNxvLUy0lqHz65py7YsiA9wyQKqhU+mw5bkcLCz/YWNFYOWywgL8D1EXsKDDmBLeJCX3cAAOwvXD9hym7//frP8/vcLIxzQl+hgA1iX7RATuKB+Py8XB06N6eGe0WZ66svwzWyICfFfaS9O1F2+7eAsUNfRwkCDkYkJe2ZZs+NIzdmrd+JBGDSWHFXS/Z+TnRWr2m8/fjIs7y1nbJy67DYuuznZ2d5MqcsSLdpq/R1nZPqfZzxwd2XJ2ms97ThaqmA12GTOPNkRMe9sxfJp/hdIn7EABSywrw0e6Mal5srN+04gGmlgHQ7iynv/v3jzQQdZLLV20n9sakFg++EzzaiBw8qgqSS7pSQ52BeX/ehm7T9xMX7J5v0Lvn7/CZIThQQgds+Dxngrt7s+bvfcI+usGtOBS83nH2+FeTmE31K9WFCSEb/0+PlrA2zLPYT4eR98+f5DBpfe33/+MLCwIwbWJEWFzjx//c4ENIyHPqsASv2/gOpBWYyVBTE4rigtfurJizeKGNkR2PGRlRRFmcUo65p76e3Hz7rcnBwPgAEL14NtvwQ8B/z9Ivrv/1+m339/4QyHDz9eS9AkcMGO7pl/HjJbgFQLMjGCioVQUiz6/fsPx4TKNIGC9lkf2mes3F2ZEe4KEgetHOTmZGdY3FnCCErtyHq2HT7TtPXQ6SZsxcKKvgp4BIUXd/7n4WBncLY0aI7ycahDVlu41RLvcNm5p7v9gdl7Hd0rtH///rOgTxbCUho5lvHycH2UEBE8d+fxcxdgymaes2rnbFDATq3L4sSmPtrHMdvJUn8aPjNBEQIK2Ob8OBUpMaG7mOUq7mIBVKiayHisB2K8arCWxkjrV8kK3Jdv3mtt3neyHJhyP8JyJLBymgIqJoy0VDYCWxEg/QuzG6e+CXC2rIY55N3HL7JcQA+Dymx00FoYbwwa1c+onwIeeVaUEd8LbAlgrZkXbto7FWjHDwa02cQ/f/4yeDqYzgdFMqgYifZ2SLx664ErCMPV/PvH4GlnMuPOm3Pm6ObefH3aAURrCltsef/tpdTbb09lMew+W7sARMsKaFyB7beBgV//frLPOl24gpmRjbzAnVafzZ5cM+Hn+j3HO1DLOwYGPh6ueywszL9BGBiID798+yG/fNuhGcjqkCc5IYt1ESkAVEmCAgaUvWsyI12Qy1LYOFJZSohpy4yVp4HmzsUsFn4wGGgpH4SpXbLlwHx0Nb/+/AYHLmiJKzb/ffn9miHDckLw3ttLKrC3Fv4xsDFygCcBGvcH3kCXZWZkYXBTTq4fHRUb7aGNBu4ooEbgVvUtQFmJA9oCi8+gmLIeosoXYOX3HJv4xAUbdsHYoJlWbGrKu+c9ooan0XcfwkBkMWIDH2ihCzY1B05civn4+asQiD1n9Y4p+PvHOHacfPv+gw+Z//3HTxZcap+9equESw4d//z1mxWb+IT563eB6ML2Wbdw6QV2FB4Saw8+7JvZiHWnTVBOy39QwIPYoJ062NRcunnfgVh7cKbcHz9/CXTNhiyWKO2ccwPYJOLEpba6f9FlEO2b2YQ39abXT/4O7Eyw47GT3c5YZy7ObMbE9Ae0jgGEaZWVN06tZUyrnYSzR6arpnAAMvbS/h9XDiOYct99+CQHokML2sEx+eXrN15s6oBtTqZ5a3dNBuIpIBpfTN59/FwXlxywWNi5eNO+Lohd33kGKuXC2LNX7ejHpiYgq/k/sfbglHj7/hNKtgAGLh82dR4ptSiWJVT0/cBnYd2kxUewibfNWHkCxr5y+4EVNjXAIuMZKNuCMK4AIgY7JVRi1eueXAMX//X7D85iMLd52hOYO/DZM9rOHW2KjQbuKBgN3NHAHQ3cUYAfAARg70qgmrjW8IRkCIEAAQyyI0hZHoJrBbVarc9WlGJdC4oVXBERCgiyHutxQaBsigtCUVCwotatSK2l6nmtolCrqGXzwZOwhD0UlCXbuzcYDBDIzNSnj575zrmHYZL57507//3vf+/8/xfSWyBBWgYSJEjlJUGCVF4SpPKSIEEqLwkSbxY0IhdBjl/v3Uca/nzxUhdFaQgVZ66zl+si9+n2VlnS/3fGpj+q5jbb04EseRTSg+vu4QsQWwvT6xFbXRdKzze2tRuExKbXwsg2kUiMBHp8NneCldltoh3jt/cYDJqV0FsLhSIE8gpKP6upb3ovJP5kOQzZQqk0uane8jZxXnT3INLsZxjvORIBzUiwNjPKC960chE8ds9hi1VRNrbnhggRgbAb0VTRb4r6+EdzVWWNTnj+p/LM8JynB/ZirV8o7kX4oh7EafyWg26TI/zwtr+Qc809pcj/FDwWCHuRkyuqKG9FeZ9V102NSsmRkDTAzocv+GZOtslgMlR4WGWMZWv/Af/y+QLlDZFJPSiVKpElAFrnMNHqJIup1izvuobWdtN7xWWrYFhlVQ33E7egGPGZ2L4IQJEMrwEcAAmZl2/18gXCUzE7cN1jU2u7YWB0Wo1sZDaV+nqCSvn22on7j8s9IH8N3GY0NdD9BSjTDQQD2UuvUNh/vHju+3HKMiRB8kFBONwm65JKTn/IPYxo3LhyYT8twGyTL87QqSoixQonRMua73/QyW814Au72Tvy5nTMMV2V5Dop7Es9DbPimUZLs7D0T1t3I7u4Mf9jJqqD/Kv6vO8Plam+Gcuf41I+CkVJoKzEeDX1U9+e5c27VRQoPWZraZZGBXraEK38t6cVrlBxpdYtQw5DlBx8DsORoAIzUBTh1DfZGuuznw60emJEwpkANBBSxfqs+XTBFFuLnxTOCLuSG/kCARsqLh8omrRtA6zGkwo36fEa53k+82dOOkzk3te6fLRjpM+PfZubdr+4fIM0laJXIBScig4ckpmzxTFuNZ56r5dnfHm5JEHCO3G35ooXVN4JerOvwoJHzroLpmI1mhbCoGkiHF6JrTHL5un/vdtAkcnJotGoEsap1wRMmN2GVcBtGMyQT/RtyRCFB4PqSXTQejtpGsXh7O9v9PD54tMxQXIHR21Ds2V4QmaZ1MIuWzDL12nO1CMeoQkCWQs8GNm5t5Kzvr+ZjLWhg9M85MEjJL4LtEOlr68pyHumBtdCNq9aPNz315wbK1EirKBSUCB34D0V1uQtTy30P4+n0wfWSUHeBWhvQkhyhLeJouwM/6jUapnp96281jsbt5OSf+fhBqBkaXQUpawPSxR7uTotBgOnP1h2Q3giZPNRh23r6u5Fsl+l3wyioJMLp9nT9jlOssLMSQy8IrlPuYbbbBkUm14GXSfYDphU5rPaeYnDROsrimRun566hKqE/TE+rL+9+Pf665tlz5lr2d/ZNC1+CY6uFacU+V1RVlIbXQu2MVoaNdJjTn2zLfyrzWJyFPp74IEwqH1uqQZTVRIhzVCh90dKA8uIefhSX03ncFHEVFVpHOm7YFr/BhY4OzDoykjK2bzcpFNXkCj/dTa7DmWVQP8YOoxbVn6yzHGSzUVFdQMZHcCCSuJkua1t5kZ67CdEO/94Tt7RgoelXtDCQsWFqaHAdaIDf7oXqwxHU5creOosaSjoV1IUQSXJ3TpqhvWg4JJz+N5WBCovBTwENWUWD8+1j+pu9y+0USqdT7T/CMU2wOBgdVUGoQpBfd3p+/37L/5iZxwfTM2EZgBNpmplfOhmCYlnH+fkSQk/2BiWhsRtGOKvPi53Pnom96psSu5wizpoeT3DXrsN0lzkru4exqbIQy/pyvibLE0aL/9PrUN06rkCog8NGoLMA4EUeB8e5w2HpcQdycPqFXUhiYsKtJl0rbb8itOhF/6I2U/ES6NRUN5Bl0It/9xZDV2CDl3FVyvJ6IIQCZ97wcaIZVn61pR30Orc6EV3j95wjg+oQKjBVKvR0mQ2KlzJ8jrH/vnihZEYLEeHkcVnaTA5mupqQ6LvgQ7S6xpa7MWS7Stql+FYnScjWLzDYKE3zXftkqVsbc264fqmqrZhmlTRxxnoFspxA8CgaR0PFlPaWLcYgamWyGnhdRh0vOwyJGgAYG66RM6dqu88lJRQTA9RTVmzwZhl/RuLodske76u/ZldNa9kCsbae3TVx5Wbads9kKWkAAMgkC8aSjw4uAsYNFWumY7dXRPWPx7/VbeBjCojMWpBvmEjMWqB23EDfuW9/QEeDoq+BxMJY3dutCbSKPfgr7tOx+wg5FS7eO8Ry25mwP3ggHWfuUyfaIV5HzP2+Llf3VzmrgWLsUrZ85mX8+Mqq7nvf7V9zRwscgKj0+oqntfpy5v2rx7dhWmB+s/1EQPexIEFKtL58jWdhaa6qoSVBYus+Z5h4vwT+zEvjHcnZ9/c5bN63uDz9U2t5mnnrqdHervNxSoLZrCGJ2SUtrR30lEaTULrs3LhB7Hun84LJqy9eHPq9hw58+OmyKTOwsdli+R9Xlldb+cREt8bkZjxgEjOnmvAAdGj0spZkYmZRUSuX7Z9n9xcPffgWMx5hDB1uYbbNCAvEuYADv55ZUUFJolCApY3kSiq6P7+SrLpcAXu0MBSXddoKXseKmJM6rlcLDL4fAFtqUwy65ssuN0GsOigRgd62rI01Os8Q+M7ZT8LiDr+rLGFNz450msMWK3jtuqbIg+2H9+9XcfeyuxXGPuQcSk/kciAhGzs0uMqDnfycvDAY4M3jiMi6+e7jzxD4048gmnaKEoT4PLJlJSENwuKVwMZ7tLy4OmzD0fLtNzd3fezK3yhUGWFH7HfQtgQmdT73aHw/8lbDKI+r9jCRP/hiagA5gq/KHFTa/tYaJniQzdbOEyyvoQQeAkRFP1NuavTbN/axhar0kqOo4Wp/l1gxR0KHpYsxX1TShSRQCCkLt22V2xmrPf7BdB5ejqs53hkAHeDB7kap06wuMjhNtuW/Lt6BpGBPs/RPvujGRNPS8sUW4vbyCiDuZFe8fmkMMr+o2dvHEjJyaNRqZgHsYOdZVbR4wrnkbZd39mC7XxSKIXDbbId/BvneLDv2Nl8N+cPt82fOTnD2ty4QFp2+7rPyMn7JaGhmWeC25mnUYUXD0dQLt24GxxwIBXXPiKFQhEGf50u+f0LTXU13unYIJqJPvvJkm178HY0JBUVIH8ThG39fEHIllVOPnuOcLDel/dq57UisUi0Ehg5bnOb8ZtsD7lVJgfwRQRDhd5F9sTwgFRHdGWU/y7bQCoviVELcp+XBKm8JEiQykuCBKm8JP7u+K8A7F0HWBTXFp7tjaoCUqRJULEQVBTbU4kGxfpiw1hAjS2xEKIi8iRGUYoNS4xdBDWiBnvUgBpLBAtiQ1RARBHpS12WLey7d5fBBXbZmcH63v35LjDlnrkz899zzz0zcw6asCEgzYuAgMiLgIDIi4DIi4CAyIuAgMiLgIDIi4DIi4CAyIuAgMiLgMiLgPApo1mB9mAEGGFZpWWlqIqPEfxuTaFQ0GwszdI0bSsuKW9dJqrSp2uRZSDglxoZ6hVo2iaRSFnbDp45hNFpMl/v0RObe2GeZmb3PHLu2upuHdqe9BzYY4umfaolUk5ZhcgcJmJWW03T4/M0xhrjcdmlPC6nBJynSYWoypD09Qay25i1Smcw6MrrI62RMOU1UkL3kIbR5RwmT+OXD2JpJQ/sQOj+cZh8Ma2ZUSEVaodqjixKL+acjE9c/PufV8LZMCo6nY6pQiIRk1NVLVHG68KXkx6lea7bd/wsk0HHmAxGk7JgU2HcXGUKXV/vjjaWpo/xbTMCI6rBH2WAA4lMLo8O+4lyxxSWVJguCNmex2YylccK9p3qZGHaMhXfHrIj5vSDZy+Gs5SR3OkNhi8apu2ajnJ3WzN6cK/AyOPxO67efjiLwu3CKqrEWMwGfzq4Too/HqyPuJCxh3BkchjRXF5TjQ1x+D7c68sAf3z9t0dMFHoko6sbsE2FwR4XHAw4LYrJnsW3R1oq+KxWSmnLBsR2tzfunPRBzIZLCfe9T11ODBfwOBgMQgejLEK+QdIRLThe5xU6bD54+iwMqsGuDenflCy4HYb+h7GAl0XsTwGaj1t3IjRaXdhxDovBCN919DRV8s5d+auSuGqjRV3Uj+Wbom6lv3wzHAYzgcGnGY3aqYrMrqmA7XgE8xoy10tdtgBcK1GVWBkTDEYYZ9LZGNHCYehhMCjf1ayYJdtuzI+tG37pXMIyYERzPssYkymkxvNOOxfVKOSkOcRi6CvlwALOqobqfSKtnSJPxEfikROlMjnmaGt55Qtr8zuqUU03JHJ5HXs37T95nklXnbsMmCB2FmY32ttZJYKbJG9YDyhTdvKTzGHC0nIHuAwJvPPwn1Hzp44ar0lDP8nMHn7/yXN35/b2l8ic37yV217DjqkNGa9yXWGoVJWGl2GDe7ts4jCZRURkO1i3vgb/OrW1juOxVeFFmwKbxao6eSkxSD2tABgBbgn4PBhTGLMydLzbw2IYoXD8xVW5Zg8LLg/iMQ2Uy4k5J/79PaayhvpYjSeUGkChqGE9KbrVq1xS0AbqPRjaP/LO8r3TXdf4fBY2LyQsTt4Fk0cM797Z8SzVg+cUFLfl1EZUHzWgZ9A3Hn1WNbX/pJGY7/KIqLugngtcBgQdrHWABT0gbPexiwfCF4GRnU7Ipjl85kowMGsstG0Htr0h1PxKe1cqxQ6EL6ZksPXo4hgLi679fJZuEOPEhaZIT+d2O2dP8JxdJ8d62CFYyBz7u+PtFFDjcYH2E0sr+FyWnohsaoBN12adf1p8ywP+/yjv6ojPxtuAJ+uBJHZxanuuWRM+NdsQyDpJpE4fF6dINftZ46RHWpu4BGrIqUs3EIovkFsgtD1/PSlQzVRotE9ZRVVL3OyBOTTeF4A5xIbZggBxOfixJnr2n6lOXKqQyEXKvwwaAxNJK0yoyOhpPSKmrkPLSo0+O28DnDECjVZTKCyzE2shkRZvA9bG3OSehvVyIvX5vHqRtGmaiLt2yXemi8J257PBCAE0JT1ke8z5gDkThjQl1zdkZyZuLhjo8dNBqczOLXTW2olBLwbbbTESCRmMDQQFYMivaGqflzn57Zeuj0zF2yKWSLGg7ye6trOzutO488vpUrmESU5b0bGG169aVkUqp5asRvpJuFibnZPiwKnLmx48fU546GjobXgfaGEgKPQa2s83Nu6GMtZZ2sscj7sp6R5dOzpc0LT/rKAtxThZYGj9Pat9v1i2ITK1qWNw2WwsaHN0Jpl2+YweNPVfPTpHa9t+OfHBlH3H46J4tVEhYVt2rpzfAnQkoab9k17/5b3r9o97ybQBTrYaYvpx22oySVlgQhaYmOVjo9k9iKZQ0OEI21TBbdCG3ob3BgXMc9Zjk7Ghfip+7HX7Ys/L5Y1nxruPXdgG1hur7FgZtnvVAoOPcSN2xpzbEX3qYhQ+nxCJJdjhDf40bcSt9ZHKVZ4C4kWjBgM2MBkZnwJx34nmnTHOw0cqkzUZSzfzdW7PXw+ePfqhT27dkhlOdTnbau3fg2vfprN6np3b6Z+klLn0Wo/Hd9987a0n4JUTkQ2H87CffDqTaY+xgV62pvX+a/feKywpc1br2GW/r1+i0xSzM+58eVrXNaSy+Gy/vfAUh1E/i8/c7ptJZRR6Vpjs/s/Lows/W/LSVM54GrjZhbr2zc4r+kL3UEQjNApUievZeDq9CHtX+wpmLt9cCb0aPDaLtmrb75eWfz/RHW4L3Lj/ISR2rQvq5gC3LlGEJ5s1NVhzMgHhmBawsZJBp/HxZdMWhvdCfprmQqRuS4HlK1jIHG/LzdlYQ/KSzSgEJtpGgLxa5x2frNlQU0sXBhjeXmTnOROpk3gv1Qv/n6GWVZKuZkKkvXjdj4is5NSM0W/tTt2+Uh6XI5rjNdSrppbpmdl5A++lZgyavmyjCCcumHBiqxZOcdMlS1/AFeJeCFxbU571S2VM6FEAEz++AlOZV906OkQSJS5VMGoTCNaAHxadVUpFRpYwpe6+c5kCEVXSwWspYOqXfTDNy6z1O8LfQVsPJvfv3nGXrYVZshYtyHr07MXgB2kvRuA326KV8bO3WsboZWlFpTJ86YEzf/+a8epNLwdri5twQtvYG4Fx7j5OH/Ys67U7Prw62FheJtLmvt06xcTfuD/vVW5BX1h1U9TJOPhYW1E7gYwKJfYoWU/AF1ZL5cAEoSsfkgCNXj7K3W0Fh8Uk9IjUztr8voO1+d18Yan1wuAdWeoPQ1q3Mk50tLFMiLt+d44uOQI+J693147HCyuy25SI8y0IjVjSCuND91Zu4zFVni2Yykqf21JpTz8ruO1GpzGI+MJpz4se9InL2OPHYqgsRVvDLgnQ65FXnuVIx5rOJJlbkWW/+86iI3gbpAoxZqLXJuODkdfLs//82Lh/lI9moMM+8f7TmaA03dPUnqItmDqyzjMxb9LwEat+O3wfup3gROVOSvpkWHSYF7jmwmZPGDqFaLtXzJ/Ub/LitQpoPtBriQt7/uLpYwbBWL5E5bQxa5kK7NMOtaaD3vH4G+uI1h050C0YktcvZFdWw4zveUVCt9//vOJGRI6FaYsESN6rmUfnxj/fF0BlkDXh2tQlcAy+MjpBj0QuN5y4UoUEm9p1xdTK6lJB0CXPVAaNTcZNB0YBtpSgtfhuzIZhA1y3/su101roxoHOc12eBmVeAplc6faZNdZjvKVZqzrNa29t/gC6j+A2/IUbXbIkQBaciQfMGt9HwOeWq2lmOr6PNkSG+jFgXRyd2tqccHFqe1GzP1qZ3LGRvLDF050geaDGhnav7vOv53WhqUwnuvIRB5m66qXmrZ2pUN1CIkVlLFTLKzEug/863POyRX0aEJcDXfJl0gLM23mVd0uBxRuVBIaSnE39vPUTS0ARS7ePesjGmoFmhXuCQZjzikvtoftQq2qn00vBkJihS7tBGzC/SNhWVqPQ6qpiMRhCcxPjdE325suc/G7gptLh+cCEf9pccrlFJTZbo08dtW/TOmH6mK+1zphz8os6gY7CgwMhIGsKi8VsZNuVlVe2EJZVWCtoxHxHRnr8HCMDvdfwfzBf6F7P6CcBNpNRAd9yyxI+cnlTltmFoKNBbGZg/9TOuFOjB0TXM2N9mMQTEeZbGbZLMuab1SWFrJZVcS6lHwxomJC7sUKQi8z07VKdTN0uclnkbeV3Sl4EhI8J9CUFAiIvAgIiLwLC+yAvfKJGYl/UMRDeK0j5eeF3U8fOX/957JC+vzS1H8waOdCtyz4qDbp578kYe2vzRJMWhq/J1t115Hzk2at3vKELC0cnB+tbRHIlqwPmW4MZLxuu95gZpDixJVCPx+VU6pIB2sActzBEir9brNapsS2Bc3rZWpkl6pJxPenRhPA9sYfxZQ6LpXQp4ucHXZVHNvobC/i8El2y/voneZFMJiv37O+6g8g12Blz7sCMsR5T8I891QEziv5BMqvlb4fO7j53/e4M+GwAfoHSupWRMNTPx9HYUL+QMnvJ5nsd57tGIaoS87Rthxd0zII1CpUPlHw+WZjr1nfNjiwqdffF/rXj+cscZ/V1d1PSB40hma8XvszTcN3cn7fmJqWkeRCVAXMO+6/dm/kuc+0eOHlxLTi/3lTqAoUy/69rSdOI7h99In6rl1+o5jzOi9eSup4TfgxRHDp9OVh9XUFxqcVYwJO0rByXD5Z7mMflyqcHRmj10U0L3CjS53MoJZfLeJXr7GhjmZhbKLQG5KfkAxVLpPXeOnFxahvv3N7uem5RiR3VDn4iLsHftJVRWlcnze8DfyjTCWhddsPze1+AD0KcHe2OzgraUtAcOUGbo2/NHDtk+sThA/6jvr6VsUHO0U0BNAdr8+QPNmFjMWiMkQN7hm6MPP5Hw22Hz/y9plt7+z8FPG45lcb8svXgrSXfjXFfMHnk2E1Rp2Lf1Y0QllVYcllMSm0qrxAZRp++HLpi3qR+2P8Z/v117zBbc5P74buOnqEqIyX9letXvb/c9z7aR5q88JkG6EUByanPPV/m5Heou8mVIsNjcQkBC7xHjwVak7Tc4pJyM2BMlsO0qb1cOvxx6eaD0c09IXG1hAtsrUiJRMoxMtCjZFv5LIsoiQ7z06NSV1RVLQDD9SQ843vc9eTJnxN55TI5c9lcr0HPsnL6Xria9APZ+oXCMsuuTvbXPokJmzr2h/rx4MQGT5jtExBREhX6I+WvEJZvOXDv5wWTv8SXxw3pE3H279u+wwa4RpAa7mg07NTFm4tizl1Za9bSKGtQb5ewud8O86HUpoioW1wOC+PzuJWUbj6YtBWXlNUl/Za9x4823yd2By80gvZp+7ZWl2wszVKJT/Dh3FJRj2P4fKLWAYCtmj9lgIOtxZUPSl544GUzx33jF7orw8qs1UOvof1WgFkvpaEZasaSsgozYUmZFSxwXY/O7Q79B5CHLHnh0DDyq57rHqdnfdXCSD8bzK5/o9Km5McZXxeXllsFzhrfb6r/+qqosJ94ZGXoC3ilY4f2C/lfMCEOrV/CHucbIondHEh4VG1pZPAm6XFGL/V16t8v7jp6PpJOp8motqlZEwo3lw7HmXSGOCU9y33c0H6/UJUTvP3wjb4uTpEJ955MfltSve0sTTOeZGb3oCJz6ezxQ28+eDr+5v0no8jWZbGY0uDtMRd+/fkHC6cvbK4P7uOyOXDj/tv/zz5VNrgmEUtndp7iv66Gw2ZJiNZz7eRw6WR8wiIt7kTGRzEbcIQvmd5RBmwjyq46lVHfdeWCKd0aeQ6AzTp7xdaS/SF+XCqy96z2NRw9L1ixb/VCEzL+xOpqKSsiYGY7fHnKSHf/wA2Rdw6Bidu3IwYuJTE6/U+99WTXpvWjiZ79fzx87upGonUC53h9BQgvzi0SOsye4FnvRXsGgy77oOQViasbC2EyZA32IRzHIXRHTLyfT+OQTRBcDlsMhwYY+M7YSC9fp/khlfEVwM6s5wFZv4Q3+ofgqtO/BXGBRq0m0qYenR2PtTE3eaa+brWfT/fJS9ZV21ma3erV1YmQJwRMdGzgvKCB+wzbvGy2m62V2U3S5pVUxmt4fiTcbBwwASOsMaXgWJo04wj3nhF3HqWNI3Ps6LBF3MNnrvwC/e0ytQdIbDYTG9C9SyRl0xW9EtnIZIZv2dagK6HzOsFpz0clDyIvwmcL9PIMAiIvAgIiLwICIi8CIi8CAiIvAgIiLwICIi8CIi8CAiIvAgIZ/FeA9q4ELsb0j79zX9V0X7ovonKkHJGyJJSb0pLbrvta/sSuKywtLRa5Ulkqd0mO0jpKKUoHyVWppLvpmJr7/zyRnUa5mpksz/fzmY6Zed/n9z7H9/n+fs+FhocREBAQkGpAQEBAQMSLgICAgICIFwEBAQERLwICAgICIl4EBAQERLwICAgICIh4ERAQEBDxIiAgICDiRUBAQEBAxIuAgICAiBcBAQEB4dNBlHeC8HTL1IfP3B+/KBhcXF5lWVvfoM3l8RV4fD5ZKBThRDjpp9nA4WIrpo3xtrfpHNbWdzhcHuVB9nOXrKf5rgXF5T1YdfV6DY1cpkAopPL5Avzn2AXP/SGRCAICgcBWoFKqlZmKeUa6Gne7W5pe6mZmmID7wL3giaH7T0QF/5OSNYVKJmI4PL7pZHd46OBUd+fl7oP7+HdkhXn4JG+AX+C5qxwejw6er+lQGA6Hh/XuZnp5xczxo+HRXx+6/tGzl30zHue65RWV9KmorjFmN3JUeAIBncsXED6Sn0I8Hv/JG4vAw348XAb+PnKw/btj1Y6di/nrcnzqXDqVLJe8gnaDshboaqimzfMa6d1JS/2Z+OfR2Qd9Yp4HzaERlaSWphATYQQcgccgKtYyyKolWoqGOcaqVokW6nY3VejaJW1dF597dt7ph3+sYkjfFn6zLRoMvaeGqt2Sumj0uanG0C2Sa70tSXAJT9++hUKki8QthIf6zLbbNrUT0zznmyNe2DgPhl8+fP1u+mw6lYIR8a0LbTweJ5P0CW+YrtVEb6VkTT146nIwHofDkUlE6dgFvi4QCEngRa3k8lQra+pMXhQUD467m7EGdgIKdFr5ylnjXcwNdd875Ly0kmWYnpM7galAazphRxynr8bvupaQunTDoil2asqKpfKsKID8cTsDz0WmPX7hRqeQMQKB8u4zCsi3py+Lh5dWVncD5PJA8tr8opJu2w+fjqmuY+vQKOQPldGH8rPp9ang8fiwUbUoUJCfBJAO6YNpSZV5m36Sikor+vkGhKfsWDHDRFGBXtX8cSOfrVrVWGjEIalJPekqrLjpd05l0vBb+eFLBSIeVs+vxMyV+99d7LDfXYWmVdZCePAbmNAWnkxtwVzjC84sgraweZWYibJd5mKHgBFqdN1CWRdFA69WNb82ozeDqIy1JF4+xhVy6N+c4oVKdp1/yINXQN0q0WmtkjJQO02qTlY7pbEbubDRvtfiA89e23v1TtpCoEpbsavJdnjc9RfZBdQZPNMRIxFaCjlIPAKBQP3X3SGpC7zcJg/sbSWpwkVvX++BRCRAJWewYNP+konDBqwdP8xhqzwqSfqj50P9gs5fIRMJeGj/B3LjvY+SM3JG+QdfiIAdbmuky4UE2USo0i37ugYOBjwVqgQZ0+D70kyLRCRiIF8+eMd6UP/srC0ixEm3yR4hh8rmF2EiHEFq9kB1gccRgbgBnSOOLNaxkTAlkhZWUv+8z/LL/UrXDAwfYKFhl9D8uUDIo9QDW3ByskWRrIWVsV9aA1sKVvQLHm6j63Tlewo1yHxbyH8S02cfOnPlMF2C3KB6UWEqPJ3v5ebRmvKTNYpKys1/2/P3I/D8RHFlKRSK4DmilT97jPDoYWka2x5x1NDIoV2ITVx76VbKWgqJ1LJDEggwHTWVnC3LplmJn3f7urzK6Nc/Qx4BwqfhPpA4D3RWygr0wvVv1O9rWeQRIC7C9kOnrzx88XII/QOEK3p7/uWmxVN6AMWbLpaXuPV7jqflF5d1h52GRPnzp40ZMtOpj/VxnLwUqJTxJK/INiAs+mQVq9YC34oXVw9IvpelycV5k0dOBaTLkqdtNZxKlas5R3+58uyQD4XAaNnZCesxM2X7xNXOf/dvluWyRD23Winu6d+LIp7s3UzG03AtbWnA9BWtsn79Ibw7IGmZHTV7r/CK5967P4e2pnjXOJ3qZaxiJVcOkrnifV5Y3FeyYUFyU6BTK0BDtVdk0Ks7otEUFJf1AG4/UVKFQUJcOXX8uK5mBjfbmwaNSmmY7Oa0DlRucuQ/d1eKxxZhuKW8usakpKLaqJOW2rPPV1oEoKQ4ekD9Fk8aNuC3ccMcNkszf4BSdd9zPDIShl8+onLbREV1jV5JJcscCGUJD4SDTRo2cKNzX5vj/0XCPX359q/nrydugiEWSLjipAvrNofHw0YN7rvBc4Tjxo6yUYmiWjXRZuVaEp6CXXjs50MXCyGQcFSsuP55typ2qbYKXeu1rG1hkJVr3Lst3EIk0HihWRu2K4jZQgT2lbJfWJTWFRprKxo9/14Ur8yJt7GRqyDZp/KFAkxVWbGoo0i3mRdbzRA8XkQlk8qlmZCeltqL1txboBRJQqBsP3Y9DMOoqzIzVRQY+Y9eFLhRKSSs2VFh0EDDikvaFHs3/aeNC3+001BlFrcrNMTjk7YeDI97kv9qAMiHFuHKBi4PszY3imbV1hnD0FFbsXoxxQyfr1XfVYUp3xh1e1HNqlPffzLqRMbTfBeY5+IdNtQVHC4fo5BJ1fO9Rkyxs7a49LXYTSMzi9qQkVQRJqLK0xY1unYxDvd+nQGqk8gTNFKx7wjyGFwTteWeNruoe4IjIhOznriLN3RpAg5oLfcePdHepvOZjshk8Jzt8ucEIJ/wGA7vM8/TPft5Qb9th07dwBPw5OZBIqh+ORxup0W+B16NG9J/46QRjhu+JJ2E+w8n7Au9dJpKJmPiZQHTx4Qi7rp5no6WJvp3fXYFPRYBZfelkxFh2Ol45PV9wZHXd8sqz4Gqxi3wGDHD0d46tD33SX/8wulAWHRoHbtBG+YJJF1J9W6sq3V/kfeoiToaqrmfc++M4ptjg1N/9aMSGTKL93F4dcp0kspH2yWwxQXYsl+WtnD5bCUakYkhdMB0sra4qSkQL6ObfyOTlUVAjeIsTfUTQ7avoIDOKjQx47GneOycQaNiUbdS1t+6lzV3/Rv1+0lTdho5XKrvgbBbuUUldpJKDsYp+/fsemLRFPcpTSQsaFKw0micRLwM69/bMv/ikaKzV+LXno2949scToAx+mYvA/6GhOtkb3V01vhh8z42ha5NIhI0KJY3vjRtGXeURV7gP8UWxtdiCyJeeRlBInDIZBJbcgDqcyEUiQg8Pp/yPRTc4mmjJw/Pt9uxNSA8HlABvTnOCGdRAIWvs8g3oHDckH6+QP3++qH73EzOnHow/HIIjdpS5cLwBnAL67YuneZgrK+dISNPQGbq6ku8DFZNndr+0Et/p+fkuUqGE952OnCWi2CKm/OCYY62B9tro1DEJzbwXmPSntEhDgqehpHBS/iRNORhCxlHxSgE+kdtQcQrQ8AQQ/Og23wvt0nSuGdu0eu+6/ecSJQcQe9oyGrUHs4GObZtGeNgWPThuJTM2YpA/YreqV8KFn0rZd2N5Mw56xd62WmpqxS0cJEbOIzN+08mFJRUdKdJLCiAKndIv+4HZk90nS+jMAD206QRc536WB/+Gson80meY0DopTBWHVsHxs/FwwnNsW1VJYWCBT+6Tehiop8srXTt9d0CT3k2Bsry2a4/OzEn+MGqQwokjY/ZchrYItMpDvcLYzz23J0ZxiCqYd87ZE68FAqpQbJ/g+qMVctWB42fTqdR2NJKq/BVhQmfz8daI15Afo0tengM47R2DwFQSlw+X6r+VnFZlU4bhMyTtOtL8JPniDnDHXvv3LQ/NIkvEDCbB73gyjIOj6e1ZOuhl+6D7bf96ObsA9+PiU+dE3gu5hDI+6bFD+KKDk6l+2PVrD562urP2msXgYDn4/E4OBX6PdTU1at2dOU/dzVhzdmYO1vJJMKbcAL533BCcwdkbWF0deGPbl5MRUblf7GB8wRc1daDbUIB+MGXpy313GrlNtqBCHhXApmmzWExRaI2kxDKu1xkTryGOlopIlHGjBaJgkpewarV3RNy4ey8ySMnM5UU2j27Ie3Rs6HBETGHqBLuIfRm6VQK10BHI0v8fX0ttQzgWgvhuJW4IIUDVodPXQlaMXPcMF1NOBuhnYrjzoOpUbeS10rOY4bTjjRVlfI11ZTzpJHPBrqaj4/4LlEOOh/jf+V26lJx1Qb/jolPW5OcnjONTCSySypZZnSJQSJIMm7O9tunuDuvllbZqykrFaorM3MLS8q7iXeG0IW/eCN5rbmR7i1LU4NEeVZ4Vm296oGTl0Ie5OSOhPkCFW7L+tL0q8Hd2X7D2KH9d/yXVVVZ3UvDmGeByySXJPNEPEyLYpavTFMrlpctFfWvNKMfB/hQCYoS7YCPqVH1itQZnfJllXZOWbJDeNa2nXSSsoTIEmBMsnqZBl12aXcY8Q7qY33s6p3UlWWVLGPxxvd2makrUGNVQGG2a9Xam8EPYovwxb9uLRcbPqDXQeBqtyA4fR3N7D7dOwcBV3ymOCnC1WbVtfVma/2Dn3N47bMLKk5ol+TqNYhGLhcbNsB2G/kLB2bawvSxQ5eB++7e+NfJ5EYOV4NAwL97rjp2oy78W7wc4EIMJQbt1bZfZthrqjClun4epCkcOaj35r0nLoaJpwnLSCgUKvodPXuHA9x4oQxCvXVsDjZt9A9+E1wdVjU9J49P2nboVFz2i4IBsLwlZyf8a1tTZ007fTV++6krt7dLyx64Qs9YTyvTb+Usm+Y6ejrDb9uF7K2raWRNqT47XJIrAoKWTGA0zZOFK8Va1D1BLTbYxNMfjyO9k4AxT4J9AtOWbmFI2RaoMgVCHrCFBmyhtVjB1tQ+BSxsgsnKv6hERkMNp1JxY+yYR2xelQ4BRxS1Lw/4xEZBPZCyAoxOUGpakYeXGGtt4FdjLiYzAxUoKtXfHPEC961xxy8zzHcHRYQnZuSMZzCoLdblQ0KgEaS/aQkkFLjkd/Z4lzlDHHoeadVF9xgxS09LI/14ZNxu4GK3IEhI5m3tK9AeNAKiAenU+4DEbTob35JFnutoqOYFbFyoGRZ1c9OFuMRf4WyHVt2vhkbMw9XRZ6xL/22yKn8H227haipKeTuOnLkGOjKlFnODQT2gUmS3YQ0g+3fhpLTs58OaSfdjgHZJu+yJoOMB96xr0THhiEISQaFp8Eu6oInN5/iXv/jCRowjbBAt6XNknJ3+8AstxAuOICNbsDZs4TZ1APN6/+XtYDS2aSENruljEUGICQm4dg7AQaJta/Mh2BmwuK8wLyvftW5d58ll2b3cifet8hMsnzVuAvy76HW5RVJ6zvgnuYXOpVUsC+DiqgE1Ajep+MJ5JnAuMJ5PoZBqmQx6sY6marqNhfFlexuLCAadWvOxq0c62e2Br5raepV7Wc/GZubkuhaVVljXsBt0uFw+QySCG63gvsguQN5c0NhqmIqMQn0djfs9LU0ibbuZR4svEW414CQUEWEoorl/gn8LRKLPHjH0dBv0G+h0DmwJCIsvKq00oZCJb8mfD0Mt2bv+N3sgU0mh4vPVBGgcwCb4ag7nvLW11YzqYqJ/N3DrMqZAIMTff/jUFZDgmILiMtvqmnr9Bg6XCdQvWdpLVwXCpnni7+qUvU3nKG/3wYvPxt7ZxOcLlOW9TLm1MuQJOZQ6biEmwkl/ihVUdyQ8FWOQlCt0GCaPrLQdI/sZjQ5RoWmVtp5fvCZbcDKzhYzRSMpVmnSjbGttx6j+hqNC1Bl6Eh6WCNfAZzHreOXvKeMvT/sNCZMJdJEKVS+vi5pdjKPppIMmqt1TOzIMJPO9GhC+Hrwpaxz2H90aAQHhmwERZcH3AxxiXASErwJoGQkCAgICIl4EBAQERLwICAgICFKETGO8cEOVCzF35o8a0vcgiUjkSuOeEdeT5rkOtA2ikEkN8sokdiNH8WZyptdwx94H5Vk4j18UOhUWl1sRie/3j3CcjEjAV+lqqz006qT1gICXbR+a+Th39OtKln7/Hp1DGHTaR2eLwDPsktIeT+NwuYze1p3DVZUVXknLFjhbITkjx6uezVH+lGOZmseP7azNwxUYtDJp2VFaUW2cnpM3kkT4srwXvdl0n92nR5e/iQQCV1p2FZVU2GTk5DpaWxhe19PWyJZ2XYiJT53Vy8osWk1Z6ZMXYLAbOEpJD7K9VZUV83tYml6Uddt5XVFtmP30pVNBSVnPKladFlwdB9IuNNbVTIZ7baswFcuwDoRMiZdAwAsePi8Yezb2jv+mxVPtzAx00tqRkfprdwVlaqkpF4wa3OeAvDIIzgTYGXgu6v6j546g8miMH+bgK6+0k9Ife166de+n0U52WzXUVOAS3maWaaKS2npup8QH2VNTs18Mg8t9J490XD3OxWG7LGyJS0pfcDfrydCupnpRHyPelIwcd79j5yI1VZivNi6eagtI97WUywR/ITZpU209W9nNuc860Kg+uOQTJ3o37U2qU3iE4H48Hk8gFBJau68oJTPHI/tFofPwAb32gPJ7hEnOmYNTBvE4vrT3jHmSWzToQFj0nkVebjNlQbwlFVU9p6/588hEV4ed3qN/+OVTrqmpZ6seOx+718bCKFpWxJuY9mh84LnYgyUVLDVDHfXi3tbmp7sY6d1mKim8gpsmlVeyDB89f+kaEhl35FVpFdO4k2buT5Ncp3WzMLr9TRFvUywDj+Ph8XiCj39w6ghH293Txw5d+rn3iPonefHfF+N2k0hESObP5JlBgHQv5uQVORpoq+eHX7m9WY2pmO8kt5MTRDghIFRbK/NLlqYGd1r7xqgf+jYta4UbvWwJCL/xIPvFpPWLfrQHClgg78oED8T0O3I2OuFBtuvkEY7bJrs5+cgwLQKDRq11ceh5CNQvPtYB0FZXyRsxyK5NEVBcVtE1LTvXGZRfFCi/mG/JVVagU7FrCakrYu88WLp8+hhXeExWR9mSk1vY2zcgPEEE6t88zxEeDrbdzrf6RVP9hIF2VifnYNhs+O9FwCs3kjNnG3TSzJT3oQxEOTRGIpNB44we3HdpcGTcgXuZTycA9dtL9RNOyeVyeeTNB0Ljs58X2nmOcPxfQuqjRa0dWikrBIRFB9+6/9BtxfSx3oPsrY9v3h96+6+TUSHqKooFVp2Nb3xNDcHawujWwF5dT91OfeRR9Lq8q4GuZqY80wdKoj8g/ptwdVbAhgVmuppq380xLt8bgHuBg6c4b1jobZWUnjN7w77QmM6Gullrf/ZwVFJkVMnTluT0x+5bD5+J7N+9y+VVcyaM+Jxr3Z3t93RUHsplcK2Ry6P062kZeWzrMjJTgVE6c93ukpj41LkfuiYrJ9dp2ppdnPLqWv3Dmxdpjf6hrz+Hy1WUV8YEX4j1v3Tznvfcia7LIenC91bNnjAEuCfPfz96NraguKzz19QYTkTGbb+W+MADkO9JeZPu/pNRx//3R1CCs7114BHfJSpyIl0cjNt1lNr93gGjNgK+QHHamB+WHduyRIsvEFC81/hXBp2P/VNeNsBxhJOXbgYYaKuVLvYeNe6/lH9yW0ABCoaoSCLytiyf1uvSjZQl+8KiD8anPpqxdp6no+RGMQfDowOjbtybMXqw/cHZE11/hu/Vsxsg6cplmd25qwnrzlxNWDrFzXmLm7O9f/P7FDKJs26eZ59VO4898z0QlrTjlxlmX7Lk9nNBJhK5ryuqdQ6FRQcBsiHj8TgRny9gVLLq9MqqWLpwQ5jOhjoP/dfM6WGip50urzLNLyq13LjvRDKPLyCDtLubGehkyCttEonIzi0q6Txq/mZRW9WCw+HBAbV436XeAxFVyoZ/4Q94ft7O1XMs4AGpf4ZERMQmpi1c5j3GzdbKXKZHtleyavXKq2p0e1qaXqJSyK1ur8qqqVeNvHF3E+goyJIqUwQ6bhhnHz7Iboe6ipJcPbQOWbk20slud28bizPrdx9PnbZ6F2fN3ImDbYDrXlLJ0vttd0habX2D6vYV0x26mrUe15Qlrt66Pz8oIm6zh+sAf4+RjuskP4dE67vE23qV39EnkHx9l03rBgiZK0ubuHw+2VRNufi3BV7Dgs5e233xRspikDfJmxb9aAnsqZFn/jSvfgMewa6I2KRloNziq2pqjXz8g9MXerl5OdpZhcrDDri/h76Wet7vv8wwRvz3dcDepvPFk3+swh85fSVgc0D4ZXN97WwgrAYqy0icALJ8qaWm8vJZQXF/dkOjAp1GrZP8Dp1OrXOysz4KB2RFEis3z16J35yS9WS4Q6+uId8F8UJoqTKLAjYu1DoMCmnT/tB/DHU1nxaXVZpbGHa689c6j0FA0cjdhbx9L8szKCJ2nyKDikXeTFl24frdZW2qUDIRKyytMNt59OzVNT97OstrNe708S5LxgzpvxnGvmf4/MkCrt7/Rg/pJ5d9Y2GZvC6r0tl++Mzt4vIqvTU/TRppZ20RDT+LTUibs/t45Mmwy7f8fpvn6aCtoSrrPU7hJAU8HENA4YavC9BLHe/i8OvmA2Hx033+LB/v0n+X2yC7LdJesk4iEnlTRjnP8g0Ij9kVeP7K2vmeAyTTIBEJXH0djVZnUzHolIpWd+n/lom3GXNAIQ207Rb0294TN2eMHbJguGPv/R1hR+rDZ8P8gyNC7a3MY1f/NGnop1wTfz/Lwy/wfFhAaNTxeV5uU+VlqzJToRy4dl3upj8evSsk4sLFG8k+Pj97DDTR05ZZbBdUaAGfLyD6BZ69AzrHJL+VM03JYkp/iEPPw4P7dT+yJzji9Nz1f+UNtrM+s3ja6Il4PFqj8z0CzpPdtXpO5+SMHLc9xyMjr8WnLm86dUDKs216djWN9V89u8vGfSeTJ6/YLpo+dugc14G2R772/PkqNsnpYqKfdMp/dYcdUpn9oqDv70fOXLEyM0hZNXeiy6deN8DWKryyus7g8JlrO9SUlfI+drCktNGne5eI8J1dcAFhl44t3Xooo3/3LldXzBo3EigBWUwlw8FTKpZOHeXt1Kf16XSgUYmWzhg7wdPNyWjz/tCESUt/F831GD7HpY39kNsDPB4nZDdwFO9lPR0LO4VPuaabmWEcnUapxhDkGX6I+ttvJf7omav7Iv9Jhmf4SX2cxkBXM+folqXMzCd5gwLPXDuyL/TSYQ1VZr1tF5MLlqb6NxUV6IXYm2OOxOWwqLK6VpdI6JjzGQkbNmyQaQICgZCqr62RDTLgOiCE9pwvBufBk80NdJPMDHWTpGUfaLwKiWmPZvfobHx91sRhM0FBfBZpdTbRu2Ooo/68nFVroMZULFBSoJdLLe+EQrKGGjPf2sI4jkGjstr6Xm8riwhIbqzaeoOsp/nDtdSUsxn0tr//2T494LkXBcU2v8wcN+ZTjupRYNCqRzrZ7TI31E3Je1ViV1BcbmOsp5VCwOOF0ssbEQk8Z05tfUMnVi1b74Ovmnrwu17PqJNmKpVCrpVX43pbfgVvy09uZ7YJRUISk0Ert7Yw+kdVWemVLJ5LU5X50srCKO5T61mvrmbRQ/r1CCwsKTfu3sX0uixCc6A+5AO1u9dr5KCNQ/r22AvqYQkcgC6vqulWU8c2A3XBROxlClfeWRrrJ1iaGcTRqPLtkNF+vAgICAhyBgrAISAgICDiRUBAQEDEi4CAgICAiBcBAQEBES8CAgICAiJeBAQEBES8CAgICAiIeBEQEBAQ8SIgICAg4kVAQEBAkC7+Dz4XgX7Ufiv2AAAAAElFTkSuQmCC';\n const caso = summary.casoMontoPrincipal || null;\n const row = (label, value) => `\n <tr>\n <td style=\"padding:7px 0;color:#333;font-size:14px;\">${label}</td>\n <td style=\"padding:7px 0;color:#4f7186;font-size:14px;font-weight:700;text-align:right;white-space:nowrap;\">${value}</td>\n </tr>`;\n const section = (title, rowsHtml) => `\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"border:1px solid #d8e1e8;border-radius:14px;margin:0 0 14px 0;border-collapse:separate;\">\n <tr><td style=\"padding:20px 22px;\">\n <h2 style=\"margin:0 0 12px;color:#4f7186;font-size:20px;line-height:1.2;\">${title}</h2>\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"border-collapse:collapse;\">${rowsHtml}</table>\n </td></tr>\n </table>`;\n const casoHtml = caso ? section('Caso de monto a revisar',\n row('Tipo', caso.tipo || 'Diferencia de monto') +\n row('Empleado / referencia', caso.empleado || 'No identificado') +\n row('Monto diferencia', `L ${fmt(caso.monto || Math.abs(diffMontos))}`) +\n `<tr><td colspan=\"2\" style=\"padding-top:10px;color:#555;font-size:13px;line-height:1.5;\"><strong>Observación:</strong> ${caso.observacion || summary.observacionIhss || 'Revisar con Administración.'}</td></tr>`\n ) : '';\n return `\n <div style=\"background:#f4f7f9;padding:24px;font-family:Arial,Helvetica,sans-serif;color:#333;\">\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:760px;margin:0 auto;background:#ffffff;border:1px solid #d8e1e8;border-radius:18px;border-collapse:separate;\">\n <tr><td style=\"padding:38px 42px;\">\n <img src=\"${logo}\" alt=\"GomezLee Marketing\" style=\"width:230px;max-width:100%;height:auto;display:block;margin:0 0 36px 0;\" />\n <div style=\"color:#48b63f;letter-spacing:5px;font-size:13px;font-weight:700;text-transform:uppercase;margin-bottom:18px;\">SEGURIDAD SOCIAL · HONDURAS</div>\n <h1 style=\"margin:0;color:#4f7186;font-size:30px;line-height:1.2;\">Cruce mensual completado</h1>\n <p style=\"margin:12px 0 28px;color:#4f7186;font-size:16px;\">${periodo}</p>\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"border:1px solid ${statusBorder};background:${statusBg};border-radius:12px;border-collapse:separate;margin-bottom:22px;\">\n <tr><td style=\"padding:16px 20px;color:${statusColor};font-size:15px;font-weight:700;\">${statusText}</td></tr>\n </table>\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin-bottom:18px;border-collapse:collapse;\">\n <tr>\n <td width=\"33.33%\" style=\"padding:0 6px 10px 0;\"><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"border:1px solid #d8e1e8;border-top:4px solid #4f7186;border-radius:12px;border-collapse:separate;\"><tr><td style=\"padding:16px;\"><div style=\"font-size:12px;color:#7d98aa;font-weight:700;letter-spacing:1px;\">PLANILLAS IHSS</div><div style=\"font-size:30px;color:#4f7186;font-weight:700;line-height:1.2;\">${summary.ihss || 0}</div></td></tr></table></td>\n <td width=\"33.33%\" style=\"padding:0 6px 10px 6px;\"><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"border:1px solid #d8e1e8;border-top:4px solid #4f7186;border-radius:12px;border-collapse:separate;\"><tr><td style=\"padding:16px;\"><div style=\"font-size:12px;color:#7d98aa;font-weight:700;letter-spacing:1px;\">NÓMINA</div><div style=\"font-size:30px;color:#4f7186;font-weight:700;line-height:1.2;\">${summary.nomina || 0}</div></td></tr></table></td>\n <td width=\"33.33%\" style=\"padding:0 0 10px 6px;\"><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"border:1px solid #d8e1e8;border-top:4px solid ${diffReal ? '#ff7a00' : '#5fbf47'};border-radius:12px;border-collapse:separate;\"><tr><td style=\"padding:16px;\"><div style=\"font-size:12px;color:#7d98aa;font-weight:700;letter-spacing:1px;\">DIF. MONTO</div><div style=\"font-size:30px;color:#4f7186;font-weight:700;line-height:1.2;\">L ${fmt(diffMontos)}</div></td></tr></table></td>\n </tr>\n </table>\n ${section('Identidad',\n row('Resultado', diferenciasIdentidad ? 'Revisar diferencias' : 'Sin diferencias') +\n row('Coinciden en ambas', summary.ambas || 0) +\n row('Solo IHSS / Solo nómina', `${summary.soloIHSS || 0} / ${summary.soloNomina || 0}`)\n )}\n ${section('Montos IHSS',\n row('IHSS descontado en nómina', `L ${fmt(summary.payrollIhss)}`) +\n row('Cuota trabajador planillas', `L ${fmt(summary.pdfCuotaTrabajador)}`) +\n row('Diferencia', `L ${fmt(diffMontos)}`)\n )}\n ${casoHtml}\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:26px 0 22px 0;\"><tr><td style=\"background:#5fbf47;border-radius:28px;\"><a href=\"${sheetUrl}\" style=\"display:inline-block;padding:14px 24px;color:#ffffff;text-decoration:none;font-weight:700;font-size:14px;\">Abrir reporte en Google Sheets</a></td></tr></table>\n <div style=\"color:#6b8798;font-size:12px;line-height:1.5;\"><strong>Archivos procesados</strong><br/>Nómina: ${summary.payrollFile || ''}<br/>Planillas PDF: ${(summary.pdfFiles || []).join(', ')}</div>\n </td></tr>\n </table>\n </div>`;\n})() }}",
|
|
"options": {
|
|
"appendAttribution": false
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.gmail",
|
|
"typeVersion": 2.2,
|
|
"position": [
|
|
2128,
|
|
2896
|
|
],
|
|
"id": "306326b1-84c3-45b0-baa3-8a0c7379d13e",
|
|
"name": "Enviar correo",
|
|
"executeOnce": true,
|
|
"webhookId": "gmail-cruce-ihss-hn",
|
|
"credentials": {
|
|
"gmailOAuth2": {
|
|
"id": "UDcO1FLJqA453V2D",
|
|
"name": "Gmail account 3"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"content": "## INICIO DEL CRUCE HONDURAS\n\nEl usuario sube el mes a validar, la nómina mensual de Honduras y una o varias planillas IHSS en PDF. El flujo inicializa la ejecución, guarda el período seleccionado y prepara la memoria temporal para acumular los datos de identidad, planillas y montos IHSS.",
|
|
"height": 560,
|
|
"width": 800
|
|
},
|
|
"type": "n8n-nodes-base.stickyNote",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-3056,
|
|
2512
|
|
],
|
|
"id": "3151efbf-ed93-484a-a068-f6f132833033",
|
|
"name": "Sticky Note"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"content": "## LECTURA DE PLANILLAS Y NÓMINA\n\nEl flujo extrae identidades desde las planillas IHSS y desde las hojas válidas de la nómina. También lee la hoja IHSS-RAP para obtener el total mensual descontado por IHSS, separando el cruce de identidad del cruce de montos.",
|
|
"height": 976,
|
|
"width": 1968,
|
|
"color": 5
|
|
},
|
|
"type": "n8n-nodes-base.stickyNote",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-1792,
|
|
2480
|
|
],
|
|
"id": "7577fb2e-75b2-46e1-b423-4b24e25fb896",
|
|
"name": "Sticky Note1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"content": "## REPORTE FINAL Y CORREO\n\nCuando ambas ramas terminan, el flujo genera un reporte en Google Sheets con cruce de identidad, cruce de montos IHSS, resumen de planillas y detalle de nómina. El correo resume los hallazgos y muestra si hay diferencias o casos puntuales para revisar.",
|
|
"height": 1344,
|
|
"width": 2816,
|
|
"color": 4
|
|
},
|
|
"type": "n8n-nodes-base.stickyNote",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
384,
|
|
2384
|
|
],
|
|
"id": "7b17e3b0-a2ba-4558-ba65-284cf32d9b6b",
|
|
"name": "Sticky Note2"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const init = $items('Inicializar memoria')[0].json;\n\nconst workflowName = (typeof $workflow !== 'undefined' && $workflow.name)\n ? $workflow.name\n : 'Cruce de Seguridad Social Honduras';\n\nconst workflowId = (typeof $workflow !== 'undefined' && $workflow.id)\n ? String($workflow.id)\n : '';\n\nconst executionId = (typeof $execution !== 'undefined' && $execution.id)\n ? String($execution.id)\n : '';\n\nreturn [{\n json: {\n id_ejecucion: init.runId,\n nombre_flujo: workflowName,\n id_flujo_n8n: workflowId || null,\n id_ejecucion_n8n: executionId || null,\n\n pais_codigo: 'HN',\n pais_nombre: 'Honduras',\n entidad_seguridad_social: 'IHSS',\n\n mes: Number(init.mes || 0),\n anio: Number(init.anio || 0),\n periodo: init.periodoLabel || `${init.mes}/${init.anio}`,\n\n estado: 'procesando',\n resultado: 'Cruce iniciado',\n fecha_inicio: new Date().toISOString(),\n\n destinatarios_correo: String(init.emailTo || '')\n .split(',')\n .map(e => e.trim())\n .filter(Boolean),\n\n moneda: 'HNL',\n archivos_procesados: {},\n conteos_fuente: {},\n metricas: {\n etapa: 'inicio',\n origen: 'n8n',\n formulario: 'Cruce IHSS vs Nómina Honduras'\n },\n resumen_completo: {\n reportTitle: init.reportTitle,\n runId: init.runId\n }\n }\n}];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-2736,
|
|
2640
|
|
],
|
|
"id": "79fe75f9-432c-4a43-821f-d296c6af452b",
|
|
"name": "Preparar auditoría inicial Supabase",
|
|
"continueOnFail": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://dbit.digitalcompass.agency/rest/v1/auditoria_cruces_seguridad_social?on_conflict=id_ejecucion",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "apikey",
|
|
"value": "=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
|
},
|
|
{
|
|
"name": "Authorization",
|
|
"value": "=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
|
},
|
|
{
|
|
"name": "Content-Type",
|
|
"value": "application/json"
|
|
},
|
|
{
|
|
"name": "Prefer",
|
|
"value": "resolution=merge-duplicates,return=minimal"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify($json) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
-2496,
|
|
2640
|
|
],
|
|
"id": "177f9c58-b722-4975-b6c2-10c26d82c932",
|
|
"name": "Supabase - Crear auditoría inicial",
|
|
"continueOnFail": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "function num(v) {\n const n = Number(v || 0);\n return Number.isFinite(n) ? Number(n.toFixed(2)) : 0;\n}\nfunction arr(v) {\n return Array.isArray(v) ? v : [];\n}\n\nconst cruce = $('Construir reporte producción').item.json;\nconst init = $('Inicializar memoria').item.json;\nconst sheet = $('Crear Google Sheet Reporte').item.json;\nconst summary = cruce.summary || {};\n\nconst spreadsheetId = sheet.spreadsheetId || '';\nconst spreadsheetUrl = sheet.spreadsheetUrl || (spreadsheetId ? `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit` : '');\nconst recipients = String(cruce.emailTo || init.emailTo || '')\n .split(',')\n .map(e => e.trim())\n .filter(Boolean);\n\nconst identityDiffs = Number(summary.diferenciasIdentidad || 0);\nconst monetaryDiffs = Math.abs(Number(summary.diffMontos || 0)) > 1\n ? Math.max(Number(summary.casosMontoCount || 0), 1)\n : 0;\nconst documentAlerts = Number(summary.duplicatePlanillas || 0)\n + Number(summary.periodoMismatches || 0)\n + arr(summary.unresolvedSpecialEmployees).length;\n\nconst resultStatus = (identityDiffs || monetaryDiffs || documentAlerts)\n ? 'COMPLETADO · REVISAR HALLAZGOS'\n : 'COMPLETADO · SIN DIFERENCIAS';\n\nconst manualMinutes = 45;\nconst automationMinutes = 5;\n\nconst workflowName = (typeof $workflow !== 'undefined' && $workflow.name)\n ? $workflow.name\n : 'Cruce de Seguridad Social Honduras';\n\nconst workflowId = (typeof $workflow !== 'undefined' && $workflow.id)\n ? String($workflow.id)\n : '';\n\nconst executionId = (typeof $execution !== 'undefined' && $execution.id)\n ? String($execution.id)\n : '';\n\nreturn [{\n json: {\n id_ejecucion: cruce.runId,\n nombre_flujo: workflowName,\n id_flujo_n8n: workflowId || null,\n id_ejecucion_n8n: executionId || null,\n\n pais_codigo: 'HN',\n pais_nombre: 'Honduras',\n entidad_seguridad_social: 'IHSS',\n\n mes: Number(init.mes || 0),\n anio: Number(init.anio || 0),\n periodo: init.periodoLabel || `${init.mes}/${init.anio}`,\n\n estado: 'exitoso',\n resultado: resultStatus,\n fecha_fin: new Date().toISOString(),\n\n url_reporte: spreadsheetUrl,\n id_google_sheet: spreadsheetId || null,\n destinatarios_correo: recipients,\n\n moneda: 'HNL',\n\n empleados_nomina: Number(summary.nomina || 0),\n empleados_entidad: Number(summary.ihss || 0),\n empleados_coincidentes: Number(summary.ambas || 0),\n\n solo_nomina: Number(summary.soloNomina || 0),\n solo_entidad: Number(summary.soloIHSS || 0),\n\n diferencias_total: identityDiffs + monetaryDiffs + documentAlerts,\n diferencias_monetarias: monetaryDiffs,\n diferencias_identidad: identityDiffs,\n alertas_documentales: documentAlerts,\n registros_duplicados: Number(summary.duplicatePlanillas || 0),\n alertas_periodo: Number(summary.periodoMismatches || 0),\n\n total_nomina: num(summary.payrollIhss),\n total_entidad: num(summary.pdfCuotaTrabajador),\n diferencia_monto: num(summary.diffMontos),\n\n minutos_estimados_manual: manualMinutes,\n minutos_automatizacion: automationMinutes,\n minutos_ahorrados: Math.max(manualMinutes - automationMinutes, 0),\n\n archivos_procesados: {\n nomina: summary.payrollFile || '',\n planillas_pdf: arr(summary.pdfFiles)\n },\n conteos_fuente: {\n identidades_nomina: Number(summary.nomina || 0),\n identidades_ihss: Number(summary.ihss || 0),\n coincidentes: Number(summary.ambas || 0),\n solo_nomina: Number(summary.soloNomina || 0),\n solo_ihss: Number(summary.soloIHSS || 0),\n planillas_pdf: Number(summary.planillas || 0)\n },\n metricas: {\n ihss_nomina_base: num(summary.payrollBaseIhss),\n ajuste_ihss_personal_especial: num(summary.payrollAdjustmentIhss),\n ihss_nomina_ajustado: num(summary.payrollIhss),\n cuota_trabajador_planillas: num(summary.pdfCuotaTrabajador),\n cuota_patrono_planillas: num(summary.pdfCuotaPatrono),\n total_planillas_pdf: num(summary.pdfTotalPlanilla),\n diferencia_ihss: num(summary.diffMontos),\n trabajadores_pdf: Number(summary.pdfTrabajadores || 0),\n personal_especial_resuelto: arr(summary.resolvedSpecialEmployees).length,\n personal_especial_no_resuelto: arr(summary.unresolvedSpecialEmployees).length,\n planillas_duplicadas: Number(summary.duplicatePlanillas || 0),\n alertas_periodo: Number(summary.periodoMismatches || 0)\n },\n resumen_completo: {\n summary,\n rowCounts: cruce.rowCounts || {},\n reporte: {\n spreadsheetId,\n spreadsheetUrl\n }\n },\n\n mensaje_error: null,\n nodo_error: null\n }\n}];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2128,
|
|
2672
|
|
],
|
|
"id": "f2cf79c4-1e28-4999-9a84-3b6dfdd0c227",
|
|
"name": "Preparar auditoría final Supabase",
|
|
"continueOnFail": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "PATCH",
|
|
"url": "=https://dbit.digitalcompass.agency/rest/v1/auditoria_cruces_seguridad_social?id_ejecucion=eq.{{$json.id_ejecucion}}",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "apikey",
|
|
"value": "=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
|
},
|
|
{
|
|
"name": "Authorization",
|
|
"value": "=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
|
},
|
|
{
|
|
"name": "Content-Type",
|
|
"value": "application/json"
|
|
},
|
|
{
|
|
"name": "Prefer",
|
|
"value": "resolution=merge-duplicates,return=minimal"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify($json) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
2368,
|
|
2672
|
|
],
|
|
"id": "e40c7b0b-5e98-4f76-89be-40ca9cadb33c",
|
|
"name": "Supabase - Actualizar auditoría final",
|
|
"continueOnFail": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "function clean(v){ return String(v ?? '').replace(/\\s+/g,' ').trim(); }\nfunction n(v){ const x = Number(v || 0); return Number.isFinite(x) ? Number(x.toFixed(2)) : null; }\nfunction push(rows, obj){ rows.push({ json: obj }); }\nfunction parseSection(rows, title){\n const data = Array.isArray(rows) ? rows : [];\n const start = data.findIndex(r => r && clean(r[0]) === title);\n if (start < 0) return [];\n const out = [];\n for (let i = start + 2; i < data.length; i++) {\n const row = data[i] || [];\n if (!row.length || row.every(x => clean(x) === '')) break;\n if (String(row[0] || '').toLowerCase().startsWith('sin diferencias')) continue;\n if (String(row[0] || '').toLowerCase().startsWith('no aplica')) continue;\n out.push(row);\n }\n return out;\n}\n\nconst cruce = $('Construir reporte producción').item.json;\nconst summary = cruce.summary || {};\nconst common = {\n id_ejecucion: cruce.runId,\n pais_codigo: 'HN',\n pais_nombre: 'Honduras',\n moneda: 'HNL'\n};\nconst out = [];\n\nfor (const r of parseSection(cruce.identidadRows, 'Empleados en planilla IHSS que NO aparecen en nómina')) {\n push(out, {\n ...common,\n categoria: 'Solo en IHSS',\n severidad: 'revision',\n identificador_empleado: clean(r[0]),\n nombre_empleado: clean(r[1]),\n fuente_a: 'Planilla IHSS',\n fuente_b: 'Nómina Honduras',\n diferencia_monto: null,\n descripcion: clean(r[4]) || 'Está en IHSS, pero no aparece en la nómina cargada.',\n accion_sugerida: 'Validar si el empleado debe estar en nómina o si pertenece a personal especial documentado.',\n datos_originales: { fila_reporte: r, numero_patronal: r[2], archivo_planilla: r[3] }\n });\n}\n\nfor (const r of parseSection(cruce.identidadRows, 'Empleados en nómina que NO aparecen en planilla IHSS')) {\n push(out, {\n ...common,\n categoria: 'Solo en nómina',\n severidad: 'revision',\n identificador_empleado: clean(r[0]),\n nombre_empleado: clean(r[1]),\n fuente_a: 'Nómina Honduras',\n fuente_b: 'Planilla IHSS',\n diferencia_monto: null,\n descripcion: clean(r[3]) || 'Está en nómina, pero no aparece en las planillas IHSS cargadas.',\n accion_sugerida: 'Validar si falta planilla IHSS, si existe error de identidad o si no corresponde reportarlo en IHSS.',\n datos_originales: { fila_reporte: r, hoja_nomina: r[2] }\n });\n}\n\nfor (const c of Array.isArray(summary.casosMonto) ? summary.casosMonto : []) {\n if (!c || String(c.tipo || '').toLowerCase().startsWith('sin diferencias')) continue;\n push(out, {\n ...common,\n categoria: Math.abs(Number(summary.diffMontos || 0)) <= 1 ? 'Diferencia menor / redondeo' : 'Diferencia de monto IHSS',\n severidad: Math.abs(Number(summary.diffMontos || 0)) <= 1 ? 'info' : 'revision',\n identificador_empleado: null,\n nombre_empleado: clean(c.empleado),\n fuente_a: 'Nómina Honduras / IHSS-RAP',\n fuente_b: 'Planillas IHSS PDF',\n diferencia_monto: n(c.monto),\n descripcion: clean(c.observacion) || `Diferencia de IHSS detectada: ${clean(c.monto)}.`,\n accion_sugerida: clean(c.accion) || 'Revisar diferencia con Nómina/Administración.',\n datos_originales: { caso: c, diferencia_total: summary.diffMontos }\n });\n}\n\nfor (const d of Array.isArray(summary.duplicatePlanillasDetalle) ? summary.duplicatePlanillasDetalle : []) {\n push(out, {\n ...common,\n categoria: 'Planilla duplicada ignorada',\n severidad: 'advertencia',\n identificador_empleado: null,\n nombre_empleado: null,\n fuente_a: 'Planillas IHSS PDF',\n fuente_b: 'Planillas IHSS PDF',\n diferencia_monto: null,\n descripcion: `Se ignoró una planilla duplicada: ${clean(d.archivo_duplicado)}.`,\n accion_sugerida: 'Evitar subir el mismo PDF/recibo dos veces en próximos cruces.',\n datos_originales: d\n });\n}\n\nfor (const p of Array.isArray(summary.periodoMismatchesDetalle) ? summary.periodoMismatchesDetalle : []) {\n push(out, {\n ...common,\n categoria: 'Planilla de período distinto',\n severidad: 'advertencia',\n identificador_empleado: null,\n nombre_empleado: null,\n fuente_a: 'Planilla IHSS PDF',\n fuente_b: 'Formulario',\n diferencia_monto: null,\n descripcion: `La planilla ${clean(p.archivo_planilla)} tiene período ${clean(p.periodo_detectado)} y se esperaba ${clean(p.periodo_esperado)}.`,\n accion_sugerida: 'Validar que todas las planillas correspondan al mes seleccionado.',\n datos_originales: p\n });\n}\n\nfor (const sp of Array.isArray(summary.unresolvedSpecialEmployees) ? summary.unresolvedSpecialEmployees : []) {\n push(out, {\n ...common,\n categoria: 'Personal especial no resuelto automáticamente',\n severidad: 'revision',\n identificador_empleado: null,\n nombre_empleado: clean(sp.referencia),\n fuente_a: 'IHSS-RAP',\n fuente_b: 'Planilla IHSS',\n diferencia_monto: n(sp.monto),\n descripcion: 'El flujo detectó personal especial, pero no pudo resolverlo automáticamente contra una identidad única.',\n accion_sugerida: 'Revisar manualmente la referencia del personal especial en IHSS-RAP.',\n datos_originales: sp\n });\n}\n\nreturn out;"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2368,
|
|
2496
|
|
],
|
|
"id": "6608f12c-681d-4b7e-a670-551c9cb51a1a",
|
|
"name": "Preparar hallazgos Supabase",
|
|
"continueOnFail": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://dbit.digitalcompass.agency/rest/v1/auditoria_cruces_hallazgos",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "apikey",
|
|
"value": "=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
|
},
|
|
{
|
|
"name": "Authorization",
|
|
"value": "=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
|
},
|
|
{
|
|
"name": "Content-Type",
|
|
"value": "application/json"
|
|
},
|
|
{
|
|
"name": "Prefer",
|
|
"value": "return=minimal"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify($json) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
2608,
|
|
2496
|
|
],
|
|
"id": "93bbccd0-f37b-4fdf-90e9-c646b9eac07c",
|
|
"name": "Supabase - Insertar hallazgos",
|
|
"continueOnFail": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "function clean(v){ return String(v ?? '').replace(/\\s+/g,' ').trim(); }\nfunction n(v){ const x = Number(v || 0); return Number.isFinite(x) ? Number(x.toFixed(2)) : null; }\nconst cruce = $('Construir reporte producción').item.json;\nconst summary = cruce.summary || {};\nconst common = { id_ejecucion: cruce.runId, pais_codigo: 'HN' };\nconst out = [];\n\nif (clean(summary.payrollFile)) {\n out.push({ json: {\n ...common,\n rol_archivo: 'Nómina Honduras',\n nombre_archivo: clean(summary.payrollFile),\n tipo_archivo: clean(summary.payrollFile).toLowerCase().endsWith('.csv') ? 'csv' : 'excel',\n registros_detectados: n(summary.nomina),\n monto_total: n(summary.payrollIhss),\n datos_originales: { key: 'nomina', summary }\n }});\n}\n\nconst pdfFiles = Array.isArray(summary.pdfFiles) ? summary.pdfFiles : [];\nfor (const name of pdfFiles) {\n if (!clean(name)) continue;\n out.push({ json: {\n ...common,\n rol_archivo: 'Planilla IHSS PDF',\n nombre_archivo: clean(name),\n tipo_archivo: 'pdf',\n registros_detectados: null,\n monto_total: null,\n datos_originales: { key: 'planilla_pdf', summary }\n }});\n}\n\nreturn out;"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2368,
|
|
2864
|
|
],
|
|
"id": "11a04947-73d4-4152-abf9-91ccdb1d8798",
|
|
"name": "Preparar archivos Supabase",
|
|
"continueOnFail": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://dbit.digitalcompass.agency/rest/v1/auditoria_cruces_archivos",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "apikey",
|
|
"value": "=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
|
},
|
|
{
|
|
"name": "Authorization",
|
|
"value": "=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
|
},
|
|
{
|
|
"name": "Content-Type",
|
|
"value": "application/json"
|
|
},
|
|
{
|
|
"name": "Prefer",
|
|
"value": "return=minimal"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify($json) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
2608,
|
|
2864
|
|
],
|
|
"id": "335cf21c-fc50-4898-95a6-7c190a636c8a",
|
|
"name": "Supabase - Insertar archivos procesados",
|
|
"continueOnFail": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "googleDriveOAuth2Api",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'iaracena@gomezleemarketing.com' }) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
1680,
|
|
2896
|
|
],
|
|
"id": "d8926dbf-bbc7-4199-a76a-92fac0a4ee39",
|
|
"name": "Compartir con Destinatario 1",
|
|
"credentials": {
|
|
"googleDriveOAuth2Api": {
|
|
"id": "g23xdGLZRzBGqKgH",
|
|
"name": "Isaac - Google Drive"
|
|
}
|
|
},
|
|
"onError": "continueRegularOutput"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "googleDriveOAuth2Api",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'ymadera@gomezleemarketing.com' }) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
1904,
|
|
2896
|
|
],
|
|
"id": "89367720-3e43-4371-9c69-0fdc034d32bc",
|
|
"name": "Compartir con Destinatario 2",
|
|
"credentials": {
|
|
"googleDriveOAuth2Api": {
|
|
"id": "g23xdGLZRzBGqKgH",
|
|
"name": "Isaac - Google Drive"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "https://glm.bamboohr.com/api/v1/employees?fields=fullName5,displayName,firstName,middleName,lastName,hireDate,originalHireDate,status,employeeNumber,location,country,workEmail&page%5Blimit%5D=2500",
|
|
"authentication": "genericCredentialType",
|
|
"genericAuthType": "httpBasicAuth",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Accept",
|
|
"value": "application/json"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"response": {
|
|
"response": {
|
|
"responseFormat": "json"
|
|
}
|
|
},
|
|
"pagination": {
|
|
"pagination": {
|
|
"parameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "page[after]",
|
|
"value": "={{ $response.body.meta.page.nextCursor }}"
|
|
}
|
|
]
|
|
},
|
|
"paginationCompleteWhen": "other",
|
|
"completeExpression": "={{ !$response.body?.meta?.page?.nextCursor }}",
|
|
"limitPagesFetched": true,
|
|
"requestInterval": 500
|
|
}
|
|
},
|
|
"timeout": 300000
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.4,
|
|
"position": [
|
|
528,
|
|
3472
|
|
],
|
|
"id": "a452436d-301e-4648-96cd-54cec4c28d48",
|
|
"name": "Consultar empleados BambooHR",
|
|
"retryOnFail": true,
|
|
"maxTries": 3,
|
|
"waitBetweenTries": 2000,
|
|
"credentials": {
|
|
"httpBasicAuth": {
|
|
"id": "7VrpNZ2jBLmiJ35q",
|
|
"name": "BambooHR GLM Full Access"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const init = $items('Inicializar memoria')[0].json;\nconst inputItems = $input.all();\n\nfunction clean(value) {\n return String(value ?? '').replace(/\\s+/g, ' ').trim();\n}\nfunction normalizeName(value) {\n return clean(value)\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/[^A-Z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\nfunction normalizeDate(value) {\n const raw = clean(value);\n if (!raw) return '';\n const match = raw.match(/^(\\d{4}-\\d{2}-\\d{2})/);\n return match ? match[1] : raw;\n}\nfunction unwrapResponse(item) {\n const payload = item?.json || {};\n // Permite que el nodo siga funcionando aunque en el futuro se active\n // \"Include Response Headers and Status\".\n if (payload.body && typeof payload.body === 'object') return payload.body;\n return payload;\n}\n\nconst responses = inputItems.map(unwrapResponse);\nconst rows = [];\n\nfor (const response of responses) {\n if (Array.isArray(response.data)) {\n rows.push(...response.data);\n } else if (Array.isArray(response.employees)) {\n rows.push(...response.employees);\n } else if (Array.isArray(response)) {\n rows.push(...response);\n }\n}\n\nconst totals = responses\n .map(response => Number(response?.meta?.total || 0))\n .filter(value => Number.isFinite(value) && value > 0);\nconst totalReported = totals.length ? Math.max(...totals) : rows.length;\n\n// Comprobación independiente de integridad antes de normalizar nombres.\nconst rawByEmployee = new Map();\nrows.forEach((row, index) => {\n const key =\n clean(row.employeeId || row.id) ||\n clean(row.employeeNumber) ||\n `${normalizeName(row.fullName5 || row.displayName || [row.firstName, row.middleName, row.lastName].filter(Boolean).join(' '))}|${normalizeDate(row.hireDate || row.originalHireDate)}` ||\n `fila-${index}`;\n\n if (!rawByEmployee.has(key)) rawByEmployee.set(key, row);\n});\n\nconst uniqueRawRows = [...rawByEmployee.values()];\n\nif (totalReported > uniqueRawRows.length) {\n throw new Error(\n `BambooHR devolvió ${uniqueRawRows.length} empleados únicos de ${totalReported}. ` +\n `La paginación no terminó correctamente y el cruce no sería completo.`\n );\n}\n\nconst normalized = [];\nfor (const row of uniqueRawRows) {\n const nombre = clean(\n row.fullName5 ||\n row.displayName ||\n row.fullName1 ||\n [row.firstName, row.middleName, row.lastName].map(clean).filter(Boolean).join(' ')\n );\n if (!nombre) continue;\n\n normalized.push({\n employee_id: clean(row.employeeId || row.id),\n employee_number: clean(row.employeeNumber),\n nombre_bamboo: nombre,\n nombre_norm: normalizeName(nombre),\n fecha_ingreso: normalizeDate(row.hireDate || row.originalHireDate),\n estado_bamboo: clean(row.status),\n ubicacion: clean(row.location),\n pais: clean(row.country),\n correo: clean(row.workEmail)\n });\n}\n\nconst byEmployee = new Map();\nfor (const row of normalized) {\n const key =\n row.employee_id ||\n row.employee_number ||\n `${row.nombre_norm}|${row.fecha_ingreso}`;\n\n if (!byEmployee.has(key)) byEmployee.set(key, row);\n}\n\nconst bamboo = [...byEmployee.values()];\n\nif (!bamboo.length) {\n throw new Error(\n 'BambooHR no devolvió empleados legibles. Revisa la credencial y los permisos para nombres y fecha de ingreso.'\n );\n}\n\nconst data = $getWorkflowStaticData('global');\ndata.hnCruceIHSSNomina = data.hnCruceIHSSNomina || {};\ndata.hnCruceIHSSNomina[init.runId] = data.hnCruceIHSSNomina[init.runId] || {};\ndata.hnCruceIHSSNomina[init.runId].bamboo = bamboo;\n\nreturn [{\n json: {\n runId: init.runId,\n empleadosBamboo: bamboo.length,\n totalReportadoBamboo: totalReported,\n paginasBamboo: responses.length,\n empleadosRecibidosBamboo: rows.length,\n empleadosUnicosBamboo: uniqueRawRows.length\n }\n}];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
752,
|
|
3472
|
|
],
|
|
"id": "98d575e0-97cd-4ea2-ad9c-026527f2d863",
|
|
"name": "Guardar BambooHR en memoria"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "googleDriveOAuth2Api",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'msoto@gomezleemarketing.com' }) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
2128,
|
|
3184
|
|
],
|
|
"id": "dab0678c-b102-42c4-b151-3b86a2833d4c",
|
|
"name": "Compartir con Destinatario 3",
|
|
"credentials": {
|
|
"googleDriveOAuth2Api": {
|
|
"id": "g23xdGLZRzBGqKgH",
|
|
"name": "Isaac - Google Drive"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "googleDriveOAuth2Api",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'asrodriguez@gomezleemarketing.com' }) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
2352,
|
|
3184
|
|
],
|
|
"id": "f61bbb7c-2b7b-464c-ae17-a3ce42f826db",
|
|
"name": "Compartir con Destinatario 4",
|
|
"credentials": {
|
|
"googleDriveOAuth2Api": {
|
|
"id": "g23xdGLZRzBGqKgH",
|
|
"name": "Isaac - Google Drive"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "googleDriveOAuth2Api",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'iherrera@gomezleemarketing.com' }) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
2576,
|
|
3184
|
|
],
|
|
"id": "706ec0e8-5d5c-4786-b769-91ae4c24f8da",
|
|
"name": "Compartir con Destinatario 5",
|
|
"credentials": {
|
|
"googleDriveOAuth2Api": {
|
|
"id": "g23xdGLZRzBGqKgH",
|
|
"name": "Isaac - Google Drive"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "googleDriveOAuth2Api",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'mgomez@gomezleemarketing.com' }) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
2800,
|
|
3184
|
|
],
|
|
"id": "c9ec0743-dcec-46aa-8009-33f28fbf2ede",
|
|
"name": "Compartir con Destinatario 6",
|
|
"credentials": {
|
|
"googleDriveOAuth2Api": {
|
|
"id": "g23xdGLZRzBGqKgH",
|
|
"name": "Isaac - Google Drive"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "googleDriveOAuth2Api",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'vparamo@gomezleemarketing.com' }) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
3024,
|
|
3184
|
|
],
|
|
"id": "9bc51788-7f60-41c5-bc25-2f1b9f5e432b",
|
|
"name": "Compartir con Destinatario 7",
|
|
"credentials": {
|
|
"googleDriveOAuth2Api": {
|
|
"id": "g23xdGLZRzBGqKgH",
|
|
"name": "Isaac - Google Drive"
|
|
}
|
|
}
|
|
}
|
|
],
|
|
"pinData": {},
|
|
"connections": {
|
|
"On form submission": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Inicializar memoria",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Inicializar memoria": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Separar planillas PDF",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Preparar nómina",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Preparar auditoría inicial Supabase",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Separar planillas PDF": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Extraer texto PDF",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extraer texto PDF": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Guardar IHSS en memoria",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Guardar IHSS en memoria": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Esperar ramas",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Preparar nómina": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Actualizar staging Nómina",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Actualizar staging Nómina": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Esperar conversión",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Esperar conversión": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Listar hojas Nómina",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Listar hojas Nómina": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Preparar hojas Nómina",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Preparar hojas Nómina": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Loop hojas Nómina",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Loop hojas Nómina": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Esperar ramas",
|
|
"type": "main",
|
|
"index": 1
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Leer hoja Nómina",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Leer hoja Nómina": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Acumular identidades Nómina",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Acumular identidades Nómina": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Loop hojas Nómina",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Esperar ramas": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Consultar empleados BambooHR",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Crear Google Sheet Reporte": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Escribir reporte",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Escribir reporte": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Formatear reporte",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Formatear reporte": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compartir con Destinatario 1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Construir reporte producción": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Crear Google Sheet Reporte",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Preparar auditoría inicial Supabase": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Supabase - Crear auditoría inicial",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Preparar auditoría final Supabase": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Supabase - Actualizar auditoría final",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Preparar hallazgos Supabase",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Preparar archivos Supabase",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Preparar hallazgos Supabase": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Supabase - Insertar hallazgos",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Preparar archivos Supabase": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Supabase - Insertar archivos procesados",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compartir con Destinatario 1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compartir con Destinatario 2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compartir con Destinatario 2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compartir con Destinatario 3",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Consultar empleados BambooHR": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Guardar BambooHR en memoria",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Guardar BambooHR en memoria": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Construir reporte producción",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compartir con Destinatario 3": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compartir con Destinatario 4",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compartir con Destinatario 4": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compartir con Destinatario 5",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compartir con Destinatario 5": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compartir con Destinatario 6",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compartir con Destinatario 6": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compartir con Destinatario 7",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compartir con Destinatario 7": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Enviar correo",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Preparar auditoría final Supabase",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"active": true,
|
|
"settings": {
|
|
"executionOrder": "v1",
|
|
"binaryMode": "separate",
|
|
"availableInMCP": true,
|
|
"timeSavedMode": "fixed",
|
|
"errorWorkflow": "puF4LUczoSz3hcek",
|
|
"timezone": "America/Santo_Domingo",
|
|
"callerPolicy": "workflowsFromSameOwner"
|
|
},
|
|
"versionId": "f6c01e0c-c22b-41c4-bd6a-1bbbe99c482d",
|
|
"meta": {
|
|
"instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
|
|
},
|
|
"id": "j04wjKihKPzBIqW8",
|
|
"tags": []
|
|
} |