diff --git a/cruce-de-seguridad-social-panama.json b/cruce-de-seguridad-social-panama.json index 9049d96..dc23060 100644 --- a/cruce-de-seguridad-social-panama.json +++ b/cruce-de-seguridad-social-panama.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-08-21T21:02:04.174Z", + "updatedAt": "2026-08-26T12:26:15.130Z", "createdAt": "2026-08-20T19:14:26.133Z", "id": "qxcXg0Z1YASkWgxL", "name": "Cruce de Seguridad Social - Panamá", @@ -44,24 +44,24 @@ "type": "n8n-nodes-base.formTrigger", "typeVersion": 2.3, "position": [ - 20768, - 26736 + 53744, + 34736 ], - "id": "ee56d788-e286-4575-9e02-d8e31bf76837", + "id": "595b95d8-2d86-40b4-a418-1b9964bc76f5", "name": "On form submission", - "webhookId": "4ba38e33-3ab3-4a2d-a66e-59457eefae48" + "webhookId": "9c032e76-4e26-46ce-ab02-1a0d628dd1fa" }, { "parameters": { - "jsCode": "const item = $input.first();\nconst now = new Date();\nconst pad = n => String(n).padStart(2,'0');\nconst runId = `PA_CSS_${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}_${Math.random().toString(36).slice(2,8)}`;\nconst recipients = [\n 'iaracena@gomezleemarketing.com'\n];\nconst logoUrl = 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png';\nconst formUrl = 'https://agenteit.digitalcompass.agency/form/86e543a4-206d-474b-ab14-4fa697897d8c';\nconst config = {\n amountTolerance: 0.10,\n cssIntegrityTolerance: 0.10,\n defaultRates: { ssRegular: 0.0975, se: 0.0125, ssDtm: 0.0725 },\n minRateSamples: 20,\n hardFailUnreadablePdf: true,\n maxSupplementalFilesSafety: 500\n};\nreturn [{json:{runId,startedAt:now.toISOString(),recipients,logoUrl,formUrl,config},binary:item.binary||{}}];" + "jsCode": "const item = $input.first();\nconst now = new Date();\nconst pad = n => String(n).padStart(2,'0');\nconst runId = `PA_CSS_${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}_${Math.random().toString(36).slice(2,8)}`;\nconst recipients = [\n 'iaracena@gomezleemarketing.com'\n];\nconst logoUrl = 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png';\nconst formUrl = 'https://agenteit.digitalcompass.agency/form/0feede33-4a67-4d98-8ae0-16f411726460';\nconst config = {\n amountTolerance: 0.10,\n cssIntegrityTolerance: 0.10,\n defaultRates: { ssRegular: 0.0975, se: 0.0125, ssDtm: 0.0725 },\n minRateSamples: 20,\n hardFailUnreadablePdf: true,\n maxSupplementalFilesSafety: 500\n};\nreturn [{json:{runId,startedAt:now.toISOString(),recipients,logoUrl,formUrl,config},binary:item.binary||{}}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 21008, - 26736 + 53984, + 34736 ], - "id": "ad4f589a-65cf-4e2f-af7c-dcd3c6337929", + "id": "cab81d5c-e00e-4957-af5a-21ab5dae0b09", "name": "Inicializar ejecución" }, { @@ -71,10 +71,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 21248, - 26832 + 54224, + 34832 ], - "id": "58ddefa4-0d02-449f-9beb-af8ac36fef1e", + "id": "3dff7255-5e27-4c05-b126-5b233af765a7", "name": "Preparar Q1" }, { @@ -90,10 +90,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 21728, - 26688 + 54704, + 34688 ], - "id": "420c890d-ad31-4500-b0ff-c0999df7795d", + "id": "85440a47-e842-43d6-9015-76690b89712f", "name": "Crear staging Q1" }, { @@ -103,10 +103,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 21968, - 26688 + 54944, + 34688 ], - "id": "9691c16a-26ae-4ef3-9231-507ab11c5231", + "id": "2fe967fa-afc8-48df-87b1-d73ba67e825d", "name": "Reunir staging Q1" }, { @@ -116,10 +116,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 23408, - 26688 + 56384, + 34688 ], - "id": "17fa00ac-b777-44b6-a84a-1c3d9649d998", + "id": "693f40c8-f19f-4ab6-a526-f0ad006e7402", "name": "Parsear Q1 Excel" }, { @@ -129,10 +129,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 23888, - 26832 + 56864, + 34832 ], - "id": "647bdb88-d7ed-46df-a1dc-7b80d00817f1", + "id": "e3a1ef29-d141-4b50-b35d-2ba175a41d4e", "name": "Preparar Q2" }, { @@ -148,10 +148,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 24368, - 26688 + 57344, + 34688 ], - "id": "e4610d47-633c-4527-b6e8-8db8f69dd84a", + "id": "70365021-6de6-43f7-b3c7-0a41fc275d5d", "name": "Crear staging Q2" }, { @@ -161,10 +161,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 24608, - 26688 + 57584, + 34688 ], - "id": "6d6ca2c5-c6bd-4d4a-838a-6c9ee056ae7e", + "id": "b182e631-7af4-4666-b74a-609f29c78e69", "name": "Reunir staging Q2" }, { @@ -174,10 +174,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 26048, - 26688 + 59024, + 34688 ], - "id": "b5fa883c-f78d-4e32-87d0-53ff44b914a2", + "id": "c5ce7b48-654e-4749-94f9-06311f088443", "name": "Parsear Q2 Excel" }, { @@ -187,10 +187,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 26288, - 26832 + 59264, + 34832 ], - "id": "fe97274d-5538-4382-be55-100f474ba51f", + "id": "0a9b3b13-da91-49d4-a018-3f72cc0209a1", "name": "Preparar CSS" }, { @@ -206,10 +206,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 26528, - 26832 + 59504, + 34832 ], - "id": "93d515a6-580d-4c36-b4bb-875309694d42", + "id": "3daecbfd-ae7c-4cbb-ad6b-564c1523972e", "name": "Crear staging CSS" }, { @@ -219,10 +219,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 26768, - 26832 + 59744, + 34832 ], - "id": "ad126f54-1ed2-433b-8695-0cd83f9e26ec", + "id": "0456e69e-4dbf-4568-8ab9-1f1efcc4d730", "name": "Reunir staging CSS" }, { @@ -232,40 +232,42 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 28208, - 26832 + 61184, + 34832 ], - "id": "b7c40654-7c89-4c6e-82bc-8bac76dd1221", + "id": "c487313c-347e-4b81-ac82-f9f2f13bc945", "name": "Parsear CSS" }, { "parameters": { "operation": "pdf", - "options": {} + "options": { + "joinPages": true + } }, "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 28688, - 26832 + 61664, + 34832 ], - "id": "7b3089a6-fbbf-403f-b4fb-c02321004c8c", + "id": "a08a8ee5-7dd5-46a0-bf76-288870e633ae", "name": "Extraer texto PDFs" }, { "parameters": { - "jsCode": "const extracted=$input.all();\nconst metaItems=$items('Normalizar PDFs descargados');\nconst css=($items('Parsear CSS')[0]||{}).json||{};\nconst init=(($items('Inicializar ejecución')[0]||{}).json)||{};\nconst cfg=init.config||{};\nconst rates=cfg.defaultRates||{ssRegular:0.0975,se:0.0125,ssDtm:0.0725};\nconst targetMonth=Number(css.period?.month||0),targetYear=Number(css.period?.year||0);\n\nfunction clean(v){return String(v??'').replace(/\\s+/g,' ').trim();}\nfunction norm(v){return clean(v).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').replace(/[¿?]/g,'').toUpperCase().replace(/[^A-Z0-9 ]/g,' ').replace(/\\s+/g,' ').trim();}\nfunction round(n){return Math.round((Number(n)||0)*100)/100;}\nfunction parseDMY(s){const m=String(s||'').match(/(\\d{1,2})\\/(\\d{1,2})\\/(20\\d{2})/);if(!m)return null;return new Date(Number(m[3]),Number(m[2])-1,Number(m[1]));}\nfunction dateKey(d){return d?`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`:'';}\nfunction fmtDate(d){return d?`${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}/${d.getFullYear()}`:'';}\nfunction inTarget(d){return !!(d&&d.getMonth()+1===targetMonth&&d.getFullYear()===targetYear);}\nfunction overlapTarget(a,b){if(!a||!b)return null;const x=[a,b].sort((p,q)=>p-q),first=new Date(targetYear,targetMonth-1,1),last=new Date(targetYear,targetMonth,0,23,59,59);return x[0]<=last&&x[1]>=first;}\nfunction regexAll(text,re){const flags=(re.flags||'').includes('g')?re.flags:(re.flags||'')+'g',rg=new RegExp(re.source,flags),out=[];let m;while((m=rg.exec(String(text||'')))!==null){out.push(m);if(m[0]==='')rg.lastIndex++;}return out;}\nfunction markers(text,re){return regexAll(text,re).map(m=>({index:m.index,end:m.index+m[0].length,match:m}));}\n\n// PDF.js a veces separa el decimal en dos líneas (66.\\n54 o 66 \\n.54).\n// Solo se reparan fragmentos que contienen un punto decimal explícito para no unir cifras distintas.\nfunction healNumericFragments(src){\n let s=String(src||'').replace(/\\r/g,'');\n for(let pass=0;pass<5;pass++){\n s=s.replace(/(\\d[\\d,]*)\\.\\s+(\\d{1,2})(?=(?:\\s|$|[^\\d]))/g,'$1.$2');\n s=s.replace(/(\\d[\\d,]*)\\s+\\.\\s*(\\d{1,2})(?=(?:\\s|$|[^\\d]))/g,'$1.$2');\n s=s.replace(/(\\d{1,3}),\\s+(\\d{3})(?=(?:\\.\\d{1,2})?(?:\\s|$|[^\\d]))/g,'$1,$2');\n }\n return s;\n}\nfunction moneyNum(v){\n if(v===null||v===undefined)return null;\n let s=String(v).replace(/B\\/\\.?|\\$/gi,'').replace(/,/g,'').trim();\n if(/^\\(.*\\)$/.test(s))s='-'+s.slice(1,-1);\n if(!/^-?\\d+(?:\\.\\d+)?$/.test(s))return null;\n const n=Number(s);return Number.isFinite(n)?round(n):null;\n}\nfunction pureMoney(line){\n const s=String(line||'').trim();\n if(!/^[\\s$()\\-]*[\\d,]+(?:\\.\\d+)?\\s*$/.test(s))return null;\n return moneyNum(s);\n}\nfunction inlineNumbers(s){\n const src=String(s||''),out=[];\n const re=/[-(]?\\$?\\d[\\d,]*(?:\\.\\d+)?\\)?/g;let m;\n while((m=re.exec(src))!==null){const v=moneyNum(m[0]);if(v!==null)out.push({value:v,index:m.index,end:re.lastIndex,raw:m[0]});if(m[0]==='')re.lastIndex++;}\n return out;\n}\nfunction splitLines(block){return String(block||'').replace(/\\r/g,'').split('\\n').map(x=>x.trim());}\n\n// Lee el importe que pertenece a una etiqueta concreta. Prioridad:\n// 1) cifra en la MISMA línea de la etiqueta; 2) línea monetaria inmediatamente anterior;\n// 3) línea monetaria inmediatamente posterior. Nunca busca importes arbitrarios a distancia.\nfunction valueAtLabel(block,labelRe,opts={}){\n const {prefer='before',steps=3}=opts,ls=splitLines(block),hits=[];\n for(let i=0;i{const a=inlineNumbers(str);if(!a.length)return null;return fromEnd?a[a.length-1].value:a[0].value;};\n if(prefer==='after'){\n let v=first(after,false);if(v!==null)return {found:true,value:v,source:'INLINE_AFTER'};\n v=first(before,true);if(v!==null)return {found:true,value:v,source:'INLINE_BEFORE'};\n }else{\n let v=first(before,true);if(v!==null)return {found:true,value:v,source:'INLINE_BEFORE'};\n v=first(after,false);if(v!==null)return {found:true,value:v,source:'INLINE_AFTER'};\n }\n }\n const before=[],after=[];\n for(let d=1;d<=steps;d++){if(i-d>=0)before.push(i-d);if(i+d0?ms[j-1].end:0,ms[j].index);}\nfunction segPost(text,ms,j){return text.slice(ms[j].end,j+1({txt:x[1],d:parseDMY(x[1])})).filter(x=>x.d);\n if(dates.length<2)return null;\n const pair=dates.slice(-2).map(x=>x.d).sort((a,b)=>a-b),startDate=pair[0],endDate=pair[1];\n return {startDate,endDate,start:dateKey(startDate),end:dateKey(endDate),display:`${fmtDate(startDate)} - ${fmtDate(endDate)}`};\n}\nfunction close(a,b,tol){return Number.isFinite(a)&&Number.isFinite(b)&&Math.abs(a-b)<=tol;}\nfunction recordFingerprint(r){return [r.category,clean(r.code),norm(r.name),clean(r.date),clean(r.transaction),round(r.ss),round(r.se),round(r.gross),round(r.regularBase),round(r.dtmBase)].join('|');}\nfunction samePerson(a,b){const ac=clean(a.code).replace(/^0+(?=\\d)/,''),bc=clean(b.code).replace(/^0+(?=\\d)/,'');if(ac&&bc&&ac===bc)return true;return !!(norm(a.name)&&norm(a.name)===norm(b.name));}\n\nconst records=[],validations=[],supportDocs=[],files=[];\nfunction validation(status,category,fileName,detail){validations.push({status,severity:(status==='OK'||status==='IGNORADO'||status==='EXCLUIDO')?'INFO':'WARNING',category,fileName,detail});}\nfunction addRecord(r){records.push({...r,ss:round(r.ss||0),se:round(r.se||0),gross:round(r.gross||0),regularBase:round(r.regularBase||0),dtmBase:round(r.dtmBase||0),parserMode:'LINE_ANCHOR_V7'});}\n\nfor(let i=0;i({code:clean(x.match[1]).replace(/^0+(?=\\d)/,''),name:clean(x.match[2]).replace(/\\s+(?:A[nñ]o|C[eé]dula:).*$/i,'')}));\n const seen=new Set(),employees=[];for(const e of raw){const k=`${e.code}|${norm(e.name)}`;if(!seen.has(k)){seen.add(k);employees.push(e);}}\n supportDocs.push({fileName,employees});validation('IGNORADO',category,fileName,`Documento de cálculo/soporte (${employees.length} empleado(s)); no se suma para evitar duplicidad.`);continue;\n }\n const ms=markers(text,/Empleado\\s*#\\s*(\\d+)\\s*:\\s*([^\\n\\r]+?)(?=\\s+Clave\\s*ISR|$)/i);\n if(!ms.length){validation('REVISAR',category,fileName,'No se detectaron empleados en la Planilla de Liquidación resumida.');continue;}\n let inc=0,out=0,review=0;\n for(let j=0;j!x.found))reason='No se pudieron leer todos los campos impresos de la liquidación dentro del bloque del empleado.';\n let regularBase=0;\n if(!reason){\n regularBase=round(sv.value-dtm.value);\n const compSV=round(sal.value+vac.value+dtm.value),compGross=round(sv.value+pai.value),expSS=round(regularBase*Number(rates.ssRegular||0.0975)+dtm.value*Number(rates.ssDtm||0.0725)),expSE=round(regularBase*Number(rates.se||0.0125));\n if(regularBase<0||!close(sv.value,compSV,.08)||!close(gross.value,compGross,.08)||!close(ss.value,expSS,.08)||!close(se.value,expSE,.08))reason=`Control contable no cuadra: Sal/Vac/13 ${sv.value} vs ${compSV}; bruto ${gross.value} vs ${compGross}; S.S. ${ss.value} (~${expSS}); S.E. ${se.value} (~${expSE}).`;\n }\n if(reason){review++;addRecord({category:'Liquidación',fileName,code,name,ss:ss.found?ss.value:0,se:se.found?se.value:0,gross:gross.found?gross.value:0,regularBase,dtmBase:dtm.found?dtm.value:0,salaryVac13:sv.found?sv.value:0,salaryReg:sal.found?sal.value:0,vacationBase:vac.found?vac.value:0,date,transaction,planillaNumber,generatedAt,included:false,reason});continue;}\n const included=inTarget(parseDMY(date));if(included)inc++;else out++;\n addRecord({category:'Liquidación',fileName,code,name,ss:ss.value,se:se.value,gross:gross.value,regularBase,dtmBase:dtm.value,salaryVac13:sv.value,salaryReg:sal.value,vacationBase:vac.value,date,transaction,planillaNumber,generatedAt,included,reason:included?'':'Fecha de liquidación fuera del mes de la Planilla CSS',ssMethod:'SOURCE_LINE',seMethod:'SOURCE_LINE'});\n }\n validation(review?'REVISAR':out?'EXCLUIDO':'OK',category,fileName,`${ms.length} liquidaciones: ${inc} incluidas, ${out} fuera de período y ${review} no legibles. Lectura por línea/etiqueta + identidades contables del mismo empleado.`);\n }else validation('REVISAR',category||'desconocida',fileName,'Categoría complementaria no reconocida; archivo excluido del cálculo.');\n}\n\n// Duplicados: nunca se suman dos veces.\nconst seenExtra=new Map();\nfor(const r of records){if(r.category!=='Extraordinaria'||!r.included)continue;const k=r.transaction?`T:${r.transaction}`:`F:${r.code||norm(r.name)}|${r.periodStart}|${r.periodEnd}|${r.ss}|${r.se}|${r.gross}`;if(!seenExtra.has(k)){seenExtra.set(k,r);continue;}const p=seenExtra.get(k);if(recordFingerprint(p)===recordFingerprint(r)){r.included=false;r.reason=`Duplicado exacto; ya incluido desde ${p.fileName}`;}else{r.included=false;r.reason=`Transacción ${r.transaction} repetida con datos distintos; revisar.`;validation('REVISAR','extraordinaria',r.fileName,r.reason);}}\nconst seenVac=new Map(),vacGroups=new Map();\nfor(const r of records){if(r.category!=='Vacaciones'||!r.included)continue;const person=r.code||norm(r.name),ek=`${person}|${r.periodStart}|${r.periodEnd}|${r.ss}|${r.se}|${r.gross}`;if(seenVac.has(ek)){r.included=false;r.reason=`Duplicado exacto; ya incluido desde ${seenVac.get(ek).fileName}`;continue;}seenVac.set(ek,r);const gk=`${person}|${r.periodStart}|${r.periodEnd}`;if(!vacGroups.has(gk))vacGroups.set(gk,[]);vacGroups.get(gk).push(r);}\nfor(const arr of vacGroups.values()){const active=arr.filter(x=>x.included);if(active.length<=1)continue;active.sort((a,b)=>(Number(b.planillaNumber)||0)-(Number(a.planillaNumber)||0)||String(b.generatedAt).localeCompare(String(a.generatedAt)));for(const r of active.slice(1)){r.included=false;r.reason=`Sustituida por planilla de vacaciones más reciente (${active[0].planillaNumber||active[0].fileName}).`;}}\nconst seenLiq=new Map();\nfor(const r of records){if(r.category!=='Liquidación'||!r.included)continue;const k=r.transaction?`T:${r.transaction}`:`F:${r.code||norm(r.name)}|${r.date}|${r.ss}|${r.se}|${r.gross}`;if(!seenLiq.has(k)){seenLiq.set(k,r);continue;}const p=seenLiq.get(k);if(recordFingerprint(p)===recordFingerprint(r)){r.included=false;r.reason=`Duplicado exacto; ya incluido desde ${p.fileName}`;}else{r.included=false;r.reason=`Transacción ${r.transaction} repetida con datos distintos; revisar.`;validation('REVISAR','liquidaciones',r.fileName,r.reason);}}\n\n// Documentos de cálculo: son soporte y nunca alteran importes ni provocan doble suma.\nconst liqAll=records.filter(r=>r.category==='Liquidación');\nfor(const doc of supportDocs){const missing=(doc.employees||[]).filter(e=>!liqAll.some(r=>samePerson(e,r)));if(missing.length)validation('REVISAR','liquidaciones',doc.fileName,`Documento de cálculo contiene persona(s) sin planilla resumida: ${missing.map(x=>`${x.name}${x.code?` (#${x.code})`:''}`).join(', ')}.`);}\n\nconst includedRecords=records.filter(r=>r.included),catCount=cat=>includedRecords.filter(r=>r.category===cat).length;\nconst RESULT={records,validations,supportDocs,includedRecords,files:[...new Set(files)],counts:{extraordinarias:catCount('Extraordinaria'),vacaciones:catCount('Vacaciones'),liquidaciones:catCount('Liquidación')},totals:{extraSS:round(includedRecords.filter(r=>r.category==='Extraordinaria').reduce((s,r)=>s+r.ss,0)),vacSS:round(includedRecords.filter(r=>r.category==='Vacaciones').reduce((s,r)=>s+r.ss,0)),liqSS:round(includedRecords.filter(r=>r.category==='Liquidación').reduce((s,r)=>s+r.ss,0)),totalSS:round(includedRecords.reduce((s,r)=>s+r.ss,0)),totalSE:round(includedRecords.reduce((s,r)=>s+r.se,0))},parserVersion:'LINE_ANCHOR_V7_SIN_IA',quality:{status:validations.some(v=>v.status==='REVISAR')?'REVISAR':'VALIDADO',rule:'Los complementarios se leen por líneas ancladas a etiquetas dentro del bloque exacto de cada empleado. No se alinean listas globales, no se buscan importes por proximidad amplia y no se reconstruyen descuentos cuando el PDF trae el valor impreso.'}};\nreturn [{json:RESULT}];\n" + "jsCode": "function parseSupplementals(extracted, metaItems, css, init={}) {\nconst cfg=init.config||{};\nconst rates=cfg.defaultRates||{ssRegular:0.0975,se:0.0125,ssDtm:0.0725};\nconst targetMonth=Number(css.period?.month||0),targetYear=Number(css.period?.year||0);\nfunction clean(v){return String(v??'').replace(/\\s+/g,' ').trim();}\nfunction norm(v){return clean(v).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').replace(/[¿?]/g,'').toUpperCase().replace(/[^A-Z0-9 ]/g,' ').replace(/\\s+/g,' ').trim();}\nfunction round(n){return Math.round((Number(n)||0)*100)/100;}\nfunction parseDMY(s){const m=String(s||'').match(/(\\d{1,2})\\/(\\d{1,2})\\/(20\\d{2})/);if(!m)return null;return new Date(Number(m[3]),Number(m[2])-1,Number(m[1]));}\nfunction dateKey(d){return d?`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`:'';}\nfunction fmtDate(d){return d?`${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}/${d.getFullYear()}`:'';}\nfunction inTarget(d){return !!(d&&d.getMonth()+1===targetMonth&&d.getFullYear()===targetYear);}\nfunction overlapTarget(a,b){if(!a||!b)return null;const x=[a,b].sort((p,q)=>p-q),first=new Date(targetYear,targetMonth-1,1),last=new Date(targetYear,targetMonth,0,23,59,59);return x[0]<=last&&x[1]>=first;}\nfunction regexAll(text,re){let flags=re.flags||'';if(!flags.includes('g'))flags+='g';const rg=new RegExp(re.source,flags),out=[];let m;while((m=rg.exec(String(text||'')))!==null){out.push(m);if(m[0]==='')rg.lastIndex++;}return out;}\nfunction markers(text,re){return regexAll(text,re).map(m=>({index:m.index,end:m.index+m[0].length,match:m}));}\nfunction segPre(text,ms,j){return text.slice(j>0?ms[j-1].end:0,ms[j].index);}\nfunction segPost(text,ms,j){return text.slice(ms[j].end,j+1({raw:m[0],index:m.index,val:moneyNum(m[0])})).filter(x=>x.val!==null);}\nfunction moneyNearLabel(block,labelRe){\n const lines=String(block||'').split('\\n'), candidates=[];\n for(let i=0;i=lines.length)break;const p=lines[j].trim();if(!p)continue;if(/^\\(?-?\\d[\\d,]*(?:\\.\\d+)?\\)?$/.test(p)){found={line:i,val:moneyNum(p),method:`NEXT_${d}`};break;}else break;}}\n if(found)candidates.push(found);\n }\n return candidates.length?candidates[candidates.length-1]:{line:-1,val:null,method:''};\n}\nfunction textNearLabel(block,labelRe,valueRe){\n const lines=String(block||'').split('\\n'),out=[];for(let i=0;ip-q);return {a:x[0],b:x[1],start:dateKey(x[0]),end:dateKey(x[1]),display:`${fmtDate(x[0])} - ${fmtDate(x[1])}`};}\nfunction periodLibre(post){\n const lines=String(post||'').split('\\n');for(let i=0;ix[0]);if(same.length>=2)return mkPeriod(same[0],same[1]);const prev=[...lines.slice(Math.max(0,i-4),i+1).join(' ').matchAll(/\\d{1,2}\\/\\d{1,2}\\/20\\d{2}/g)].map(x=>x[0]);if(prev.length>=2)return mkPeriod(prev[prev.length-2],prev[prev.length-1]);const next=[...lines.slice(i,Math.min(lines.length,i+5)).join(' ').matchAll(/\\d{1,2}\\/\\d{1,2}\\/20\\d{2}/g)].map(x=>x[0]);if(next.length>=2)return mkPeriod(next[0],next[1]);}return null;\n}\nfunction mkPeriod(a,b){const d1=parseDMY(a),d2=parseDMY(b);if(!d1||!d2)return null;const x=[d1,d2].sort((p,q)=>p-q);return {startDate:x[0],endDate:x[1],start:dateKey(x[0]),end:dateKey(x[1]),display:`${fmtDate(x[0])} - ${fmtDate(x[1])}`};}\nfunction recordFingerprint(r){return [r.category,clean(r.code),norm(r.name),clean(r.date),clean(r.transaction),round(r.ss),round(r.se),round(r.gross),round(r.regularBase),round(r.dtmBase)].join('|');}\nfunction samePerson(a,b){const ac=clean(a.code).replace(/^0+(?=\\d)/,''),bc=clean(b.code).replace(/^0+(?=\\d)/,'');if(ac&&bc&&ac===bc)return true;return !!(norm(a.name)&&norm(a.name)===norm(b.name));}\nfunction vacFields(block){return {vac:moneyNearLabel(block,/Vacaciones(?!\\s+Correspondientes)/i),gross:moneyNearLabel(block,/TOTAL\\s+BRUTO\\s*:?/i),ss:moneyNearLabel(block,/Seg\\.\\s*Soc\\.?/i),se:moneyNearLabel(block,/Seg\\.\\s*Edu\\.?/i)};}\nfunction vacCandidateOK(f){if([f.vac.val,f.gross.val,f.ss.val,f.se.val].some(v=>v===null))return false;const g=f.gross.val;return close(f.vac.val,g,.08)&&close(f.ss.val,round(g*Number(rates.ssRegular||.0975)),.16)&&close(f.se.val,round(g*Number(rates.se||.0125)),.13);}\nfunction extraFields(block){return {gross:moneyNearLabel(block,/\\bTotales\\b/i),ss:moneyNearLabel(block,/Seg\\.\\s*Soc\\.?/i),se:moneyNearLabel(block,/Seg\\.\\s*Edu\\.?/i)};}\nfunction extraCandidateOK(f){if([f.gross.val,f.ss.val,f.se.val].some(v=>v===null))return false;const g=f.gross.val;return close(f.ss.val,round(g*Number(rates.ssRegular||.0975)),.16)&&close(f.se.val,round(g*Number(rates.se||.0125)),.13);}\nfunction liqFields(block){return {date:dateNearLabel(block,/FECHA\\s+DE\\s+LIQUIDACI[ÓO]N\\s*:?/i),transaction:txnNearLabel(block),ss:moneyNearLabel(block,/Seg\\.\\s*Soc\\.?/i),se:moneyNearLabel(block,/Seg\\.\\s*Edu\\.?/i),sv:moneyNearLabel(block,/Salario,?\\s*Vac,?\\s*13\\s*Mes\\s*:?/i),sal:moneyNearLabel(block,/Salario\\s+Reg\\.?\\s*:?/i),vac:moneyNearLabel(block,/^\\s*Vacaciones\\s*:?/i),dtm:moneyNearLabel(block,/^\\s*XIII\\s+Mes\\s*:?/i),pai:moneyNearLabel(block,/Preav,?\\s*P\\s*Ant,?\\s*Indem\\s*:?/i),gross:moneyNearLabel(block,/Total\\s+Bruto\\s*:?/i)};}\nfunction liqCandidateOK(f){if(!f.date||!f.transaction||[f.ss.val,f.se.val,f.sv.val,f.sal.val,f.vac.val,f.dtm.val,f.pai.val,f.gross.val].some(v=>v===null))return false;const rb=round(f.sal.val+f.vac.val),compSV=round(rb+f.dtm.val),compGross=round(f.sv.val+f.pai.val),es=round(rb*Number(rates.ssRegular||.0975)+f.dtm.val*Number(rates.ssDtm||.0725)),ee=round(rb*Number(rates.se||.0125));return close(f.sv.val,compSV,.14)&&close(f.gross.val,compGross,.14)&&close(f.ss.val,es,.17)&&close(f.se.val,ee,.13);}\nconst records=[],validations=[],supportDocs=[],files=[];\nfunction validation(status,category,fileName,detail){validations.push({status,severity:(status==='OK'||status==='IGNORADO'||status==='EXCLUIDO')?'INFO':'WARNING',category,fileName,detail});}\nfunction addRecord(r){records.push({...r,ss:round(r.ss||0),se:round(r.se||0),gross:round(r.gross||0),regularBase:round(r.regularBase||0),dtmBase:round(r.dtmBase||0),parserMode:'STRUCTURE_AWARE_V10'});}\nfor(let i=0;i\\n'):String(raw||''));if(!text.trim()){validation('REVISAR',category,fileName,'PDF sin texto extraíble; archivo excluido.');continue;}\n if(category==='vacaciones'){\n const ms=markers(text,/Empleado\\s*#\\s*(\\d+)\\s*:\\s*(.+?)(?=\\s+(?:Clave\\s+ISR|Vacaciones\\s+Correspondientes|Periodo\\s+Libre)|[\\n\\r]|$)/i);if(!ms.length){validation('REVISAR',category,fileName,'No se detectaron empleados.');continue;}\n const firstPre=vacFields(segPre(text,ms,0)),orientation=vacCandidateOK(firstPre)?'PRE':'POST',planillaNumber=globalPlanilla(text),generatedAt=globalGenerated(text);let inc=0,out=0,review=0;\n for(let j=0;j({code:clean(x.match[1]).replace(/^0+(?=\\d)/,''),name:clean(x.match[2]).replace(/\\s+(?:A[nñ]o|C[eé]dula:).*$/i,'')}));const seen=new Set(),employees=[];for(const e of rawm){const k=e.code+'|'+norm(e.name);if(!seen.has(k)){seen.add(k);employees.push(e);}}supportDocs.push({fileName,employees});validation('IGNORADO',category,fileName,`Documento de cálculo/soporte (${employees.length} empleado(s)); no se suma.`);continue;}\n const ms=markers(text,/Empleado\\s*#\\s*(\\d+)\\s*:\\s*(.+?)(?=\\s+(?:FECHA\\s+DE\\s+LIQUIDACI[ÓO]N|Clave\\s+ISR|N[uú]mero\\s+de\\s+Transacci[oó]n)|[\\n\\r]|$)/i);if(!ms.length){validation('REVISAR',category,fileName,'No se detectaron empleados resumidos.');continue;}const firstPre=liqFields(segPre(text,ms,0)),orientation=liqCandidateOK(firstPre)?'PRE':'POST',planillaNumber=globalPlanilla(text),generatedAt=globalGenerated(text);let inc=0,out=0,review=0;\n for(let j=0;j(Number(b.planillaNumber)||0)-(Number(a.planillaNumber)||0)||String(b.generatedAt).localeCompare(String(a.generatedAt)));for(const r of arr.slice(1)){r.included=false;r.reason=`Sustituida por planilla de vacaciones más reciente (${arr[0].planillaNumber||arr[0].fileName}).`;}}\nconst liqAll=records.filter(r=>r.category==='Liquidación');for(const doc of supportDocs){const missing=(doc.employees||[]).filter(e=>!liqAll.some(r=>samePerson(e,r)));if(missing.length)validation('REVISAR','liquidaciones',doc.fileName,`Documento de cálculo contiene persona(s) sin planilla resumida: ${missing.map(x=>`${x.name}${x.code?` (#${x.code})`:''}`).join(', ')}.`);}\nconst includedRecords=records.filter(r=>r.included),catCount=cat=>includedRecords.filter(r=>r.category===cat).length;return {records,validations,supportDocs,includedRecords,files:[...new Set(files)],counts:{extraordinarias:catCount('Extraordinaria'),vacaciones:catCount('Vacaciones'),liquidaciones:catCount('Liquidación')},totals:{extraSS:round(includedRecords.filter(r=>r.category==='Extraordinaria').reduce((s,r)=>s+r.ss,0)),vacSS:round(includedRecords.filter(r=>r.category==='Vacaciones').reduce((s,r)=>s+r.ss,0)),liqSS:round(includedRecords.filter(r=>r.category==='Liquidación').reduce((s,r)=>s+r.ss,0)),totalSS:round(includedRecords.reduce((s,r)=>s+r.ss,0)),totalSE:round(includedRecords.reduce((s,r)=>s+r.se,0))},parserVersion:'STRUCTURE_AWARE_V10_SIN_IA',quality:{status:validations.some(v=>v.status==='REVISAR')?'REVISAR':'VALIDADO',rule:'La orientación PRE/POST se determina con el primer empleado del archivo mediante controles matemáticos. La misma orientación se aplica a todo el archivo. Vacaciones lee Periodo Libre siempre del bloque posterior. No hay alineación global por listas.'}};\n}\n\nconst __result = parseSupplementals($input.all(), $items('Normalizar PDFs descargados'), (($items('Parsear CSS')[0]||{}).json||{}), (($items('Inicializar ejecución')[0]||{}).json||{}));\nreturn [{json:__result}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 28928, - 26832 + 61904, + 34832 ], - "id": "ed614c7e-da77-4670-86ac-ba83d855ba21", + "id": "14f0d70e-f329-49c3-b500-4fdad7c2112f", "name": "Parsear PDFs", "notesInFlow": true, - "notes": "Parser V7 SIN IA: lectura determinista por líneas ancladas a etiquetas dentro del bloque exacto de cada empleado; sin alineación global ni búsqueda por proximidad amplia." + "notes": "Parser complementarios V8 SIN IA: extracción directa por campos del mismo empleado, detección PRE/POST por archivo, validación contable/tasas, soporte para PDF.js y decimales fragmentados. No alinea listas globales ni toma montos de empleados vecinos." }, { "parameters": { @@ -274,10 +276,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 29168, - 26832 + 62144, + 34832 ], - "id": "cae896d0-72d3-424a-ae45-3c3483b9fbd3", + "id": "6e10d4ed-e0ff-4f3c-bbd4-503735047765", "name": "Construir reporte producción" }, { @@ -294,10 +296,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 29408, - 26832 + 62384, + 34832 ], - "id": "6fa8dc8f-bd83-445f-97d7-e1acdedddc97", + "id": "bac4af9b-bb77-4456-9e17-be8fa3af8587", "name": "Crear Google Sheet Reporte", "credentials": { "googleSheetsOAuth2Api": { @@ -320,10 +322,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 29648, - 26832 + 62624, + 34832 ], - "id": "fe78de18-a2d4-4866-8e99-4a87819129fc", + "id": "acc027fa-0b8e-42ae-81d3-e8fb393bf755", "name": "Escribir reporte", "credentials": { "googleSheetsOAuth2Api": { @@ -339,10 +341,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 29888, - 26832 + 62864, + 34832 ], - "id": "6e359784-7724-4dcc-82f3-e8ec5867288b", + "id": "023c71e6-974a-4ac4-9c19-cdeb97719dd6", "name": "Preparar formato reporte" }, { @@ -359,10 +361,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 30128, - 26832 + 63104, + 34832 ], - "id": "d4092b24-112e-4cbc-89a8-757458c3baec", + "id": "176ef0a3-091f-493a-ac08-53addee2e6b1", "name": "Formatear reporte", "credentials": { "googleSheetsOAuth2Api": { @@ -378,10 +380,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 30368, - 26832 + 63344, + 34832 ], - "id": "c569cb6f-97bc-42ef-bfc0-0636ef6f98b1", + "id": "f12bf3d7-f5c9-4b12-93bc-a0da50c9c629", "name": "Preparar permisos" }, { @@ -398,10 +400,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 30608, - 26832 + 63584, + 34832 ], - "id": "164892df-85f8-4265-bb47-a21c7700f6f4", + "id": "63765f02-d4c8-4911-bf9d-7f768ed382ff", "name": "Compartir reporte como editor", "credentials": { "googleDriveOAuth2Api": { @@ -418,10 +420,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 30848, - 26832 + 63824, + 34832 ], - "id": "bdee4a88-ed77-41b1-a4ea-c0321f5587d2", + "id": "40a9e530-9b60-4450-bfa3-0d7d5e2e95d3", "name": "Preparar correo HTML GLM" }, { @@ -436,10 +438,10 @@ "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ - 31088, - 26832 + 64064, + 34832 ], - "id": "1cf118be-34f7-46ff-9a83-414bdb9bdf5a", + "id": "33c125b7-9b00-4e68-b197-2cfcd8651f55", "name": "Enviar correo resumen", "webhookId": "ae5f87e7-df2b-4349-8eb5-aac208accb96", "executeOnce": true, @@ -457,10 +459,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 31808, - 26832 + 64784, + 34832 ], - "id": "b99cc3cd-a57e-401b-aab3-b671895bdc05", + "id": "034abc56-cf5b-4a09-a6c5-c110c46683eb", "name": "Preparar confirmación" }, { @@ -472,10 +474,10 @@ "type": "n8n-nodes-base.form", "typeVersion": 2.5, "position": [ - 32048, - 26832 + 65024, + 34832 ], - "id": "716a9401-cf32-4eac-b329-8d318f0c686b", + "id": "b949ed7f-046d-4d13-beaf-d064a8b6e5bf", "name": "Confirmación y regreso al formulario", "webhookId": "d4a102a8-3d72-4c5b-83b2-0e0d04c71099", "notesInFlow": true, @@ -508,10 +510,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 21488, - 26912 + 54464, + 34912 ], - "id": "5abe74ca-8b12-4ee2-8289-ff31fa149407", + "id": "1c33ef12-3450-4ad1-b9f9-f742d02bd5c6", "name": "¿Q1 es PDF?" }, { @@ -522,10 +524,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 21728, - 27056 + 54704, + 35056 ], - "id": "34048913-682f-42a2-bc3f-8b02746dc92b", + "id": "c291f41f-cb07-4314-9e34-cb20c98f63e5", "name": "Extraer texto Q1 PDF" }, { @@ -535,10 +537,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 21984, - 27024 + 54960, + 35024 ], - "id": "c0af9861-2562-41d1-a15b-00b2e42b7bf2", + "id": "e256e1d6-2774-462c-9d55-5a031485d6cb", "name": "Parsear Q1 PDF" }, { @@ -548,10 +550,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 23648, - 26832 + 56624, + 34832 ], - "id": "321d3b2f-837e-48b3-af92-8662b625adc7", + "id": "3d6d60e8-487a-41cc-b256-46d813aace5c", "name": "Resultado Q1" }, { @@ -581,10 +583,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 24128, - 26912 + 57104, + 34912 ], - "id": "ed7c9435-b43d-4e4c-a702-23127d54fe1f", + "id": "b4e4e628-7ddb-475a-8983-892aac060cf7", "name": "¿Q2 es PDF?" }, { @@ -595,10 +597,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 24368, - 27056 + 57344, + 35056 ], - "id": "e7dfdc83-f288-48e3-82ef-72bc1ca65ca4", + "id": "3223cc16-c235-4286-ab4b-6caa1bc402b7", "name": "Extraer texto Q2 PDF" }, { @@ -608,10 +610,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 24608, - 27056 + 57584, + 35056 ], - "id": "c8d62ff5-5098-4509-b79d-0b2a670cbbec", + "id": "dd41d8e5-d079-4422-a22c-ca2010fa31c2", "name": "Parsear Q2 PDF" }, { @@ -621,10 +623,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 25568, - 26832 + 58544, + 34832 ], - "id": "012df9a7-6095-4322-8f93-8b4d24986e54", + "id": "22ae7500-d025-4854-aeab-7b6827c4fb14", "name": "Resultado Q2" }, { @@ -634,10 +636,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 31328, - 26832 + 64304, + 34832 ], - "id": "e83d7ddc-c884-4034-acc3-f6331f491608", + "id": "a358393b-3223-450e-a6db-4a146838f266", "name": "Preparar limpieza temporales" }, { @@ -651,10 +653,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 31568, - 26832 + 64544, + 34832 ], - "id": "d7d69f76-df90-4f5c-bad4-536410e6fc9d", + "id": "c20af9b0-54e4-44c9-be47-5c277d575807", "name": "Eliminar staging temporal", "alwaysOutputData": true, "notesInFlow": true, @@ -681,10 +683,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 21248, - 27216 + 54224, + 35216 ], - "id": "b16c3231-7cda-4395-b4a7-6b17cc5be53d", + "id": "09fa4c3c-7a56-46ae-a0ef-2bd5030a1417", "name": "Crear carpeta temporal PDFs", "retryOnFail": true, "maxTries": 3, @@ -728,10 +730,10 @@ "type": "n8n-nodes-base.form", "typeVersion": 2.5, "position": [ - 21488, - 27216 + 54464, + 35216 ], - "id": "b784edea-6563-4f63-b8cb-900f2850753e", + "id": "8f7476cf-6957-4714-9cd3-9f71feb1e687", "name": "Definir Extraordinarias", "webhookId": "2d4fba32-6a24-46e8-a9c7-a38ca80b106a" }, @@ -762,10 +764,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 21728, - 27216 + 54704, + 35216 ], - "id": "534cbd73-479a-4797-848c-a012e29bbc58", + "id": "953ada8a-80d8-40ee-a130-c040a9376a09", "name": "¿Hay extraordinarias?" }, { @@ -804,10 +806,10 @@ "type": "n8n-nodes-base.form", "typeVersion": 2.5, "position": [ - 21984, - 27024 + 54960, + 35024 ], - "id": "c0214ec8-c0ef-4e9d-bf81-b1cc55eaffef", + "id": "4bf3e422-a9fe-4f98-9122-c3c4edff7f0f", "name": "Cargar Extraordinarias", "webhookId": "237dd241-cbba-4455-beec-b4b63e45827d" }, @@ -818,10 +820,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 22224, - 27024 + 55200, + 35024 ], - "id": "693a4ce9-c131-4bb1-9809-c455482b0b3a", + "id": "d7cbe170-a566-42bf-9653-d5282de9a8c7", "name": "Preparar lote Extraordinarias", "notesInFlow": true, "notes": "Valida PDF y prepara cada archivo del lote para guardarlo temporalmente." @@ -845,10 +847,10 @@ "type": "n8n-nodes-base.googleDrive", "typeVersion": 3, "position": [ - 22464, - 27024 + 55440, + 35024 ], - "id": "bc86f7bb-749b-4c13-9a20-3411dcb14f94", + "id": "88420c7c-55f7-46e5-8a22-fe6f5fb91182", "name": "Guardar lote Extraordinarias", "retryOnFail": true, "maxTries": 3, @@ -867,10 +869,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 22704, - 27024 + 55680, + 35024 ], - "id": "4c14fe7c-7ee1-4ba6-b214-1f7f4b450c06", + "id": "11655d6b-b8c4-4931-bfe5-7cc6e21d65be", "name": "Cerrar lote Extraordinarias" }, { @@ -880,7 +882,7 @@ "caseSensitive": true, "leftValue": "", "typeValidation": "strict", - "version": 2 + "version": 3 }, "conditions": [ { @@ -900,10 +902,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 22944, - 27024 + 55920, + 35024 ], - "id": "801c384f-0904-474c-a036-f90f13861d57", + "id": "b7f00446-e4a5-4314-a0dc-f27916f57120", "name": "¿Agregar más Extraordinarias?" }, { @@ -942,10 +944,10 @@ "type": "n8n-nodes-base.form", "typeVersion": 2.5, "position": [ - 23184, - 27184 + 56160, + 35184 ], - "id": "cdcf55f9-3935-4775-bbd1-0f7af2053895", + "id": "cacdbf95-a924-469f-8b6b-5ec5ed9043c4", "name": "Cargar Vacaciones", "webhookId": "4405d87c-973b-4828-8f0b-e75ec83473f3" }, @@ -956,10 +958,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 23424, - 27184 + 56400, + 35184 ], - "id": "db348186-5aa6-48ca-b987-b608371a6135", + "id": "615b13f0-62b6-41d9-b73c-df8156f4fbd4", "name": "Preparar lote Vacaciones" }, { @@ -981,10 +983,10 @@ "type": "n8n-nodes-base.googleDrive", "typeVersion": 3, "position": [ - 23664, - 27184 + 56640, + 35184 ], - "id": "765675c4-1b52-47cd-8548-c7ee8e672795", + "id": "8ef5b6f6-e0c0-46e6-9063-ac981022c548", "name": "Guardar lote Vacaciones", "retryOnFail": true, "maxTries": 3, @@ -1003,10 +1005,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 23888, - 27216 + 56864, + 35216 ], - "id": "a132a6cb-3992-489b-992d-391f1b25d939", + "id": "cd731fba-163e-4102-867f-93fdd05161c9", "name": "Cerrar lote Vacaciones" }, { @@ -1036,10 +1038,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 24128, - 27216 + 57104, + 35216 ], - "id": "42d058c1-e95d-4f50-97eb-92c8b676f519", + "id": "fb6e392f-5d4a-4474-baf9-bb4079018fdc", "name": "¿Agregar más Vacaciones?" }, { @@ -1078,10 +1080,10 @@ "type": "n8n-nodes-base.form", "typeVersion": 2.5, "position": [ - 24368, - 27216 + 57344, + 35216 ], - "id": "b1b680ab-5f7e-46eb-ac89-217fc6e1c5bb", + "id": "4a7d5715-a5fe-4a86-84d4-78ad63edb8ea", "name": "Cargar Liquidaciones", "webhookId": "f04eb408-97b4-4c5c-9c90-53e58021ea20" }, @@ -1092,10 +1094,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 24608, - 27216 + 57584, + 35216 ], - "id": "73add3fa-2c1f-4b49-90a2-aeb8946e6407", + "id": "1aacdced-cc5a-459e-9314-cbe0c48535ec", "name": "Preparar lote Liquidaciones" }, { @@ -1117,10 +1119,10 @@ "type": "n8n-nodes-base.googleDrive", "typeVersion": 3, "position": [ - 24848, - 27216 + 57824, + 35216 ], - "id": "65feccb0-75ee-4dac-a387-d104b3eb6b77", + "id": "a68ad67c-236c-4417-a527-f570dcf6803a", "name": "Guardar lote Liquidaciones", "retryOnFail": true, "maxTries": 3, @@ -1139,10 +1141,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 25088, - 27216 + 58064, + 35216 ], - "id": "4cbe541e-cdcc-4f61-a8b3-3ed8f4fa5915", + "id": "6ef08c3a-b33f-4cd7-af93-584be7575f7f", "name": "Cerrar lote Liquidaciones" }, { @@ -1172,10 +1174,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 25328, - 27216 + 58304, + 35216 ], - "id": "eb344e25-564f-47fe-9c2f-78196123900c", + "id": "435f9ae2-7ccb-479f-8078-6f895bd4726a", "name": "¿Agregar más Liquidaciones?" }, { @@ -1188,10 +1190,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 28448, - 27056 + 61424, + 35056 ], - "id": "e3645976-82e4-4464-ac32-8a1be494f3df", + "id": "1b450788-edf6-472c-8497-338312040c0d", "name": "Listar PDFs temporales", "retryOnFail": true, "maxTries": 3, @@ -1210,10 +1212,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 28688, - 27056 + 61664, + 35056 ], - "id": "dc199c71-3269-4587-92fa-404ad7815521", + "id": "b525df3e-0968-489a-80d7-436a9697f00a", "name": "Preparar descarga PDFs", "notesInFlow": true, "notes": "Reconstruye las tres categorías desde la carpeta temporal y valida mínimos antes de descargar." @@ -1234,10 +1236,10 @@ "type": "n8n-nodes-base.googleDrive", "typeVersion": 3, "position": [ - 28928, - 27056 + 61904, + 35056 ], - "id": "f0ce2d8b-da54-4aa9-9c1a-560b90f5e2c1", + "id": "b5e9a2fb-a85c-45fc-b819-431840e1e007", "name": "Descargar PDFs temporales", "retryOnFail": true, "maxTries": 3, @@ -1256,10 +1258,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 29168, - 27056 + 62144, + 35056 ], - "id": "b169ad1c-478f-4fc9-b80d-2d4d020c86b8", + "id": "833ab32f-3fad-4271-8a6d-81923f4eda84", "name": "Normalizar PDFs descargados" } ], @@ -1951,9 +1953,9 @@ }, "staticData": null, "meta": null, - "versionId": "6b4a28c8-b9fc-42c1-a440-6bd1aae0ab20", - "activeVersionId": "6b4a28c8-b9fc-42c1-a440-6bd1aae0ab20", - "versionCounter": 122, + "versionId": "d6cac1fb-fb4b-4bef-a2b3-146b21e84ac4", + "activeVersionId": "d6cac1fb-fb4b-4bef-a2b3-146b21e84ac4", + "versionCounter": 144, "triggerCount": 1, "shared": [ { @@ -1976,9 +1978,9 @@ ], "tags": [], "activeVersion": { - "updatedAt": "2026-08-21T21:02:10.000Z", - "createdAt": "2026-08-21T21:02:01.330Z", - "versionId": "6b4a28c8-b9fc-42c1-a440-6bd1aae0ab20", + "updatedAt": "2026-08-26T01:49:22.000Z", + "createdAt": "2026-08-26T01:48:51.760Z", + "versionId": "d6cac1fb-fb4b-4bef-a2b3-146b21e84ac4", "workflowId": "qxcXg0Z1YASkWgxL", "nodes": [ { @@ -2018,24 +2020,24 @@ "type": "n8n-nodes-base.formTrigger", "typeVersion": 2.3, "position": [ - 20768, - 26736 + 53744, + 34736 ], - "id": "ee56d788-e286-4575-9e02-d8e31bf76837", + "id": "595b95d8-2d86-40b4-a418-1b9964bc76f5", "name": "On form submission", - "webhookId": "4ba38e33-3ab3-4a2d-a66e-59457eefae48" + "webhookId": "9c032e76-4e26-46ce-ab02-1a0d628dd1fa" }, { "parameters": { - "jsCode": "const item = $input.first();\nconst now = new Date();\nconst pad = n => String(n).padStart(2,'0');\nconst runId = `PA_CSS_${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}_${Math.random().toString(36).slice(2,8)}`;\nconst recipients = [\n 'iaracena@gomezleemarketing.com'\n];\nconst logoUrl = 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png';\nconst formUrl = 'https://agenteit.digitalcompass.agency/form/86e543a4-206d-474b-ab14-4fa697897d8c';\nconst config = {\n amountTolerance: 0.10,\n cssIntegrityTolerance: 0.10,\n defaultRates: { ssRegular: 0.0975, se: 0.0125, ssDtm: 0.0725 },\n minRateSamples: 20,\n hardFailUnreadablePdf: true,\n maxSupplementalFilesSafety: 500\n};\nreturn [{json:{runId,startedAt:now.toISOString(),recipients,logoUrl,formUrl,config},binary:item.binary||{}}];" + "jsCode": "const item = $input.first();\nconst now = new Date();\nconst pad = n => String(n).padStart(2,'0');\nconst runId = `PA_CSS_${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}_${Math.random().toString(36).slice(2,8)}`;\nconst recipients = [\n 'iaracena@gomezleemarketing.com'\n];\nconst logoUrl = 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png';\nconst formUrl = 'https://agenteit.digitalcompass.agency/form/0feede33-4a67-4d98-8ae0-16f411726460';\nconst config = {\n amountTolerance: 0.10,\n cssIntegrityTolerance: 0.10,\n defaultRates: { ssRegular: 0.0975, se: 0.0125, ssDtm: 0.0725 },\n minRateSamples: 20,\n hardFailUnreadablePdf: true,\n maxSupplementalFilesSafety: 500\n};\nreturn [{json:{runId,startedAt:now.toISOString(),recipients,logoUrl,formUrl,config},binary:item.binary||{}}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 21008, - 26736 + 53984, + 34736 ], - "id": "ad4f589a-65cf-4e2f-af7c-dcd3c6337929", + "id": "cab81d5c-e00e-4957-af5a-21ab5dae0b09", "name": "Inicializar ejecución" }, { @@ -2045,10 +2047,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 21248, - 26832 + 54224, + 34832 ], - "id": "58ddefa4-0d02-449f-9beb-af8ac36fef1e", + "id": "3dff7255-5e27-4c05-b126-5b233af765a7", "name": "Preparar Q1" }, { @@ -2064,10 +2066,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 21728, - 26688 + 54704, + 34688 ], - "id": "420c890d-ad31-4500-b0ff-c0999df7795d", + "id": "85440a47-e842-43d6-9015-76690b89712f", "name": "Crear staging Q1" }, { @@ -2077,10 +2079,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 21968, - 26688 + 54944, + 34688 ], - "id": "9691c16a-26ae-4ef3-9231-507ab11c5231", + "id": "2fe967fa-afc8-48df-87b1-d73ba67e825d", "name": "Reunir staging Q1" }, { @@ -2090,10 +2092,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 23408, - 26688 + 56384, + 34688 ], - "id": "17fa00ac-b777-44b6-a84a-1c3d9649d998", + "id": "693f40c8-f19f-4ab6-a526-f0ad006e7402", "name": "Parsear Q1 Excel" }, { @@ -2103,10 +2105,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 23888, - 26832 + 56864, + 34832 ], - "id": "647bdb88-d7ed-46df-a1dc-7b80d00817f1", + "id": "e3a1ef29-d141-4b50-b35d-2ba175a41d4e", "name": "Preparar Q2" }, { @@ -2122,10 +2124,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 24368, - 26688 + 57344, + 34688 ], - "id": "e4610d47-633c-4527-b6e8-8db8f69dd84a", + "id": "70365021-6de6-43f7-b3c7-0a41fc275d5d", "name": "Crear staging Q2" }, { @@ -2135,10 +2137,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 24608, - 26688 + 57584, + 34688 ], - "id": "6d6ca2c5-c6bd-4d4a-838a-6c9ee056ae7e", + "id": "b182e631-7af4-4666-b74a-609f29c78e69", "name": "Reunir staging Q2" }, { @@ -2148,10 +2150,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 26048, - 26688 + 59024, + 34688 ], - "id": "b5fa883c-f78d-4e32-87d0-53ff44b914a2", + "id": "c5ce7b48-654e-4749-94f9-06311f088443", "name": "Parsear Q2 Excel" }, { @@ -2161,10 +2163,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 26288, - 26832 + 59264, + 34832 ], - "id": "fe97274d-5538-4382-be55-100f474ba51f", + "id": "0a9b3b13-da91-49d4-a018-3f72cc0209a1", "name": "Preparar CSS" }, { @@ -2180,10 +2182,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 26528, - 26832 + 59504, + 34832 ], - "id": "93d515a6-580d-4c36-b4bb-875309694d42", + "id": "3daecbfd-ae7c-4cbb-ad6b-564c1523972e", "name": "Crear staging CSS" }, { @@ -2193,10 +2195,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 26768, - 26832 + 59744, + 34832 ], - "id": "ad126f54-1ed2-433b-8695-0cd83f9e26ec", + "id": "0456e69e-4dbf-4568-8ab9-1f1efcc4d730", "name": "Reunir staging CSS" }, { @@ -2206,40 +2208,42 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 28208, - 26832 + 61184, + 34832 ], - "id": "b7c40654-7c89-4c6e-82bc-8bac76dd1221", + "id": "c487313c-347e-4b81-ac82-f9f2f13bc945", "name": "Parsear CSS" }, { "parameters": { "operation": "pdf", - "options": {} + "options": { + "joinPages": true + } }, "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 28688, - 26832 + 61664, + 34832 ], - "id": "7b3089a6-fbbf-403f-b4fb-c02321004c8c", + "id": "a08a8ee5-7dd5-46a0-bf76-288870e633ae", "name": "Extraer texto PDFs" }, { "parameters": { - "jsCode": "const extracted=$input.all();\nconst metaItems=$items('Normalizar PDFs descargados');\nconst css=($items('Parsear CSS')[0]||{}).json||{};\nconst init=(($items('Inicializar ejecución')[0]||{}).json)||{};\nconst cfg=init.config||{};\nconst rates=cfg.defaultRates||{ssRegular:0.0975,se:0.0125,ssDtm:0.0725};\nconst targetMonth=Number(css.period?.month||0),targetYear=Number(css.period?.year||0);\n\nfunction clean(v){return String(v??'').replace(/\\s+/g,' ').trim();}\nfunction norm(v){return clean(v).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').replace(/[¿?]/g,'').toUpperCase().replace(/[^A-Z0-9 ]/g,' ').replace(/\\s+/g,' ').trim();}\nfunction round(n){return Math.round((Number(n)||0)*100)/100;}\nfunction parseDMY(s){const m=String(s||'').match(/(\\d{1,2})\\/(\\d{1,2})\\/(20\\d{2})/);if(!m)return null;return new Date(Number(m[3]),Number(m[2])-1,Number(m[1]));}\nfunction dateKey(d){return d?`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`:'';}\nfunction fmtDate(d){return d?`${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}/${d.getFullYear()}`:'';}\nfunction inTarget(d){return !!(d&&d.getMonth()+1===targetMonth&&d.getFullYear()===targetYear);}\nfunction overlapTarget(a,b){if(!a||!b)return null;const x=[a,b].sort((p,q)=>p-q),first=new Date(targetYear,targetMonth-1,1),last=new Date(targetYear,targetMonth,0,23,59,59);return x[0]<=last&&x[1]>=first;}\nfunction regexAll(text,re){const flags=(re.flags||'').includes('g')?re.flags:(re.flags||'')+'g',rg=new RegExp(re.source,flags),out=[];let m;while((m=rg.exec(String(text||'')))!==null){out.push(m);if(m[0]==='')rg.lastIndex++;}return out;}\nfunction markers(text,re){return regexAll(text,re).map(m=>({index:m.index,end:m.index+m[0].length,match:m}));}\n\n// PDF.js a veces separa el decimal en dos líneas (66.\\n54 o 66 \\n.54).\n// Solo se reparan fragmentos que contienen un punto decimal explícito para no unir cifras distintas.\nfunction healNumericFragments(src){\n let s=String(src||'').replace(/\\r/g,'');\n for(let pass=0;pass<5;pass++){\n s=s.replace(/(\\d[\\d,]*)\\.\\s+(\\d{1,2})(?=(?:\\s|$|[^\\d]))/g,'$1.$2');\n s=s.replace(/(\\d[\\d,]*)\\s+\\.\\s*(\\d{1,2})(?=(?:\\s|$|[^\\d]))/g,'$1.$2');\n s=s.replace(/(\\d{1,3}),\\s+(\\d{3})(?=(?:\\.\\d{1,2})?(?:\\s|$|[^\\d]))/g,'$1,$2');\n }\n return s;\n}\nfunction moneyNum(v){\n if(v===null||v===undefined)return null;\n let s=String(v).replace(/B\\/\\.?|\\$/gi,'').replace(/,/g,'').trim();\n if(/^\\(.*\\)$/.test(s))s='-'+s.slice(1,-1);\n if(!/^-?\\d+(?:\\.\\d+)?$/.test(s))return null;\n const n=Number(s);return Number.isFinite(n)?round(n):null;\n}\nfunction pureMoney(line){\n const s=String(line||'').trim();\n if(!/^[\\s$()\\-]*[\\d,]+(?:\\.\\d+)?\\s*$/.test(s))return null;\n return moneyNum(s);\n}\nfunction inlineNumbers(s){\n const src=String(s||''),out=[];\n const re=/[-(]?\\$?\\d[\\d,]*(?:\\.\\d+)?\\)?/g;let m;\n while((m=re.exec(src))!==null){const v=moneyNum(m[0]);if(v!==null)out.push({value:v,index:m.index,end:re.lastIndex,raw:m[0]});if(m[0]==='')re.lastIndex++;}\n return out;\n}\nfunction splitLines(block){return String(block||'').replace(/\\r/g,'').split('\\n').map(x=>x.trim());}\n\n// Lee el importe que pertenece a una etiqueta concreta. Prioridad:\n// 1) cifra en la MISMA línea de la etiqueta; 2) línea monetaria inmediatamente anterior;\n// 3) línea monetaria inmediatamente posterior. Nunca busca importes arbitrarios a distancia.\nfunction valueAtLabel(block,labelRe,opts={}){\n const {prefer='before',steps=3}=opts,ls=splitLines(block),hits=[];\n for(let i=0;i{const a=inlineNumbers(str);if(!a.length)return null;return fromEnd?a[a.length-1].value:a[0].value;};\n if(prefer==='after'){\n let v=first(after,false);if(v!==null)return {found:true,value:v,source:'INLINE_AFTER'};\n v=first(before,true);if(v!==null)return {found:true,value:v,source:'INLINE_BEFORE'};\n }else{\n let v=first(before,true);if(v!==null)return {found:true,value:v,source:'INLINE_BEFORE'};\n v=first(after,false);if(v!==null)return {found:true,value:v,source:'INLINE_AFTER'};\n }\n }\n const before=[],after=[];\n for(let d=1;d<=steps;d++){if(i-d>=0)before.push(i-d);if(i+d0?ms[j-1].end:0,ms[j].index);}\nfunction segPost(text,ms,j){return text.slice(ms[j].end,j+1({txt:x[1],d:parseDMY(x[1])})).filter(x=>x.d);\n if(dates.length<2)return null;\n const pair=dates.slice(-2).map(x=>x.d).sort((a,b)=>a-b),startDate=pair[0],endDate=pair[1];\n return {startDate,endDate,start:dateKey(startDate),end:dateKey(endDate),display:`${fmtDate(startDate)} - ${fmtDate(endDate)}`};\n}\nfunction close(a,b,tol){return Number.isFinite(a)&&Number.isFinite(b)&&Math.abs(a-b)<=tol;}\nfunction recordFingerprint(r){return [r.category,clean(r.code),norm(r.name),clean(r.date),clean(r.transaction),round(r.ss),round(r.se),round(r.gross),round(r.regularBase),round(r.dtmBase)].join('|');}\nfunction samePerson(a,b){const ac=clean(a.code).replace(/^0+(?=\\d)/,''),bc=clean(b.code).replace(/^0+(?=\\d)/,'');if(ac&&bc&&ac===bc)return true;return !!(norm(a.name)&&norm(a.name)===norm(b.name));}\n\nconst records=[],validations=[],supportDocs=[],files=[];\nfunction validation(status,category,fileName,detail){validations.push({status,severity:(status==='OK'||status==='IGNORADO'||status==='EXCLUIDO')?'INFO':'WARNING',category,fileName,detail});}\nfunction addRecord(r){records.push({...r,ss:round(r.ss||0),se:round(r.se||0),gross:round(r.gross||0),regularBase:round(r.regularBase||0),dtmBase:round(r.dtmBase||0),parserMode:'LINE_ANCHOR_V7'});}\n\nfor(let i=0;i({code:clean(x.match[1]).replace(/^0+(?=\\d)/,''),name:clean(x.match[2]).replace(/\\s+(?:A[nñ]o|C[eé]dula:).*$/i,'')}));\n const seen=new Set(),employees=[];for(const e of raw){const k=`${e.code}|${norm(e.name)}`;if(!seen.has(k)){seen.add(k);employees.push(e);}}\n supportDocs.push({fileName,employees});validation('IGNORADO',category,fileName,`Documento de cálculo/soporte (${employees.length} empleado(s)); no se suma para evitar duplicidad.`);continue;\n }\n const ms=markers(text,/Empleado\\s*#\\s*(\\d+)\\s*:\\s*([^\\n\\r]+?)(?=\\s+Clave\\s*ISR|$)/i);\n if(!ms.length){validation('REVISAR',category,fileName,'No se detectaron empleados en la Planilla de Liquidación resumida.');continue;}\n let inc=0,out=0,review=0;\n for(let j=0;j!x.found))reason='No se pudieron leer todos los campos impresos de la liquidación dentro del bloque del empleado.';\n let regularBase=0;\n if(!reason){\n regularBase=round(sv.value-dtm.value);\n const compSV=round(sal.value+vac.value+dtm.value),compGross=round(sv.value+pai.value),expSS=round(regularBase*Number(rates.ssRegular||0.0975)+dtm.value*Number(rates.ssDtm||0.0725)),expSE=round(regularBase*Number(rates.se||0.0125));\n if(regularBase<0||!close(sv.value,compSV,.08)||!close(gross.value,compGross,.08)||!close(ss.value,expSS,.08)||!close(se.value,expSE,.08))reason=`Control contable no cuadra: Sal/Vac/13 ${sv.value} vs ${compSV}; bruto ${gross.value} vs ${compGross}; S.S. ${ss.value} (~${expSS}); S.E. ${se.value} (~${expSE}).`;\n }\n if(reason){review++;addRecord({category:'Liquidación',fileName,code,name,ss:ss.found?ss.value:0,se:se.found?se.value:0,gross:gross.found?gross.value:0,regularBase,dtmBase:dtm.found?dtm.value:0,salaryVac13:sv.found?sv.value:0,salaryReg:sal.found?sal.value:0,vacationBase:vac.found?vac.value:0,date,transaction,planillaNumber,generatedAt,included:false,reason});continue;}\n const included=inTarget(parseDMY(date));if(included)inc++;else out++;\n addRecord({category:'Liquidación',fileName,code,name,ss:ss.value,se:se.value,gross:gross.value,regularBase,dtmBase:dtm.value,salaryVac13:sv.value,salaryReg:sal.value,vacationBase:vac.value,date,transaction,planillaNumber,generatedAt,included,reason:included?'':'Fecha de liquidación fuera del mes de la Planilla CSS',ssMethod:'SOURCE_LINE',seMethod:'SOURCE_LINE'});\n }\n validation(review?'REVISAR':out?'EXCLUIDO':'OK',category,fileName,`${ms.length} liquidaciones: ${inc} incluidas, ${out} fuera de período y ${review} no legibles. Lectura por línea/etiqueta + identidades contables del mismo empleado.`);\n }else validation('REVISAR',category||'desconocida',fileName,'Categoría complementaria no reconocida; archivo excluido del cálculo.');\n}\n\n// Duplicados: nunca se suman dos veces.\nconst seenExtra=new Map();\nfor(const r of records){if(r.category!=='Extraordinaria'||!r.included)continue;const k=r.transaction?`T:${r.transaction}`:`F:${r.code||norm(r.name)}|${r.periodStart}|${r.periodEnd}|${r.ss}|${r.se}|${r.gross}`;if(!seenExtra.has(k)){seenExtra.set(k,r);continue;}const p=seenExtra.get(k);if(recordFingerprint(p)===recordFingerprint(r)){r.included=false;r.reason=`Duplicado exacto; ya incluido desde ${p.fileName}`;}else{r.included=false;r.reason=`Transacción ${r.transaction} repetida con datos distintos; revisar.`;validation('REVISAR','extraordinaria',r.fileName,r.reason);}}\nconst seenVac=new Map(),vacGroups=new Map();\nfor(const r of records){if(r.category!=='Vacaciones'||!r.included)continue;const person=r.code||norm(r.name),ek=`${person}|${r.periodStart}|${r.periodEnd}|${r.ss}|${r.se}|${r.gross}`;if(seenVac.has(ek)){r.included=false;r.reason=`Duplicado exacto; ya incluido desde ${seenVac.get(ek).fileName}`;continue;}seenVac.set(ek,r);const gk=`${person}|${r.periodStart}|${r.periodEnd}`;if(!vacGroups.has(gk))vacGroups.set(gk,[]);vacGroups.get(gk).push(r);}\nfor(const arr of vacGroups.values()){const active=arr.filter(x=>x.included);if(active.length<=1)continue;active.sort((a,b)=>(Number(b.planillaNumber)||0)-(Number(a.planillaNumber)||0)||String(b.generatedAt).localeCompare(String(a.generatedAt)));for(const r of active.slice(1)){r.included=false;r.reason=`Sustituida por planilla de vacaciones más reciente (${active[0].planillaNumber||active[0].fileName}).`;}}\nconst seenLiq=new Map();\nfor(const r of records){if(r.category!=='Liquidación'||!r.included)continue;const k=r.transaction?`T:${r.transaction}`:`F:${r.code||norm(r.name)}|${r.date}|${r.ss}|${r.se}|${r.gross}`;if(!seenLiq.has(k)){seenLiq.set(k,r);continue;}const p=seenLiq.get(k);if(recordFingerprint(p)===recordFingerprint(r)){r.included=false;r.reason=`Duplicado exacto; ya incluido desde ${p.fileName}`;}else{r.included=false;r.reason=`Transacción ${r.transaction} repetida con datos distintos; revisar.`;validation('REVISAR','liquidaciones',r.fileName,r.reason);}}\n\n// Documentos de cálculo: son soporte y nunca alteran importes ni provocan doble suma.\nconst liqAll=records.filter(r=>r.category==='Liquidación');\nfor(const doc of supportDocs){const missing=(doc.employees||[]).filter(e=>!liqAll.some(r=>samePerson(e,r)));if(missing.length)validation('REVISAR','liquidaciones',doc.fileName,`Documento de cálculo contiene persona(s) sin planilla resumida: ${missing.map(x=>`${x.name}${x.code?` (#${x.code})`:''}`).join(', ')}.`);}\n\nconst includedRecords=records.filter(r=>r.included),catCount=cat=>includedRecords.filter(r=>r.category===cat).length;\nconst RESULT={records,validations,supportDocs,includedRecords,files:[...new Set(files)],counts:{extraordinarias:catCount('Extraordinaria'),vacaciones:catCount('Vacaciones'),liquidaciones:catCount('Liquidación')},totals:{extraSS:round(includedRecords.filter(r=>r.category==='Extraordinaria').reduce((s,r)=>s+r.ss,0)),vacSS:round(includedRecords.filter(r=>r.category==='Vacaciones').reduce((s,r)=>s+r.ss,0)),liqSS:round(includedRecords.filter(r=>r.category==='Liquidación').reduce((s,r)=>s+r.ss,0)),totalSS:round(includedRecords.reduce((s,r)=>s+r.ss,0)),totalSE:round(includedRecords.reduce((s,r)=>s+r.se,0))},parserVersion:'LINE_ANCHOR_V7_SIN_IA',quality:{status:validations.some(v=>v.status==='REVISAR')?'REVISAR':'VALIDADO',rule:'Los complementarios se leen por líneas ancladas a etiquetas dentro del bloque exacto de cada empleado. No se alinean listas globales, no se buscan importes por proximidad amplia y no se reconstruyen descuentos cuando el PDF trae el valor impreso.'}};\nreturn [{json:RESULT}];\n" + "jsCode": "function parseSupplementals(extracted, metaItems, css, init={}) {\nconst cfg=init.config||{};\nconst rates=cfg.defaultRates||{ssRegular:0.0975,se:0.0125,ssDtm:0.0725};\nconst targetMonth=Number(css.period?.month||0),targetYear=Number(css.period?.year||0);\nfunction clean(v){return String(v??'').replace(/\\s+/g,' ').trim();}\nfunction norm(v){return clean(v).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').replace(/[¿?]/g,'').toUpperCase().replace(/[^A-Z0-9 ]/g,' ').replace(/\\s+/g,' ').trim();}\nfunction round(n){return Math.round((Number(n)||0)*100)/100;}\nfunction parseDMY(s){const m=String(s||'').match(/(\\d{1,2})\\/(\\d{1,2})\\/(20\\d{2})/);if(!m)return null;return new Date(Number(m[3]),Number(m[2])-1,Number(m[1]));}\nfunction dateKey(d){return d?`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`:'';}\nfunction fmtDate(d){return d?`${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}/${d.getFullYear()}`:'';}\nfunction inTarget(d){return !!(d&&d.getMonth()+1===targetMonth&&d.getFullYear()===targetYear);}\nfunction overlapTarget(a,b){if(!a||!b)return null;const x=[a,b].sort((p,q)=>p-q),first=new Date(targetYear,targetMonth-1,1),last=new Date(targetYear,targetMonth,0,23,59,59);return x[0]<=last&&x[1]>=first;}\nfunction regexAll(text,re){let flags=re.flags||'';if(!flags.includes('g'))flags+='g';const rg=new RegExp(re.source,flags),out=[];let m;while((m=rg.exec(String(text||'')))!==null){out.push(m);if(m[0]==='')rg.lastIndex++;}return out;}\nfunction markers(text,re){return regexAll(text,re).map(m=>({index:m.index,end:m.index+m[0].length,match:m}));}\nfunction segPre(text,ms,j){return text.slice(j>0?ms[j-1].end:0,ms[j].index);}\nfunction segPost(text,ms,j){return text.slice(ms[j].end,j+1({raw:m[0],index:m.index,val:moneyNum(m[0])})).filter(x=>x.val!==null);}\nfunction moneyNearLabel(block,labelRe){\n const lines=String(block||'').split('\\n'), candidates=[];\n for(let i=0;i=lines.length)break;const p=lines[j].trim();if(!p)continue;if(/^\\(?-?\\d[\\d,]*(?:\\.\\d+)?\\)?$/.test(p)){found={line:i,val:moneyNum(p),method:`NEXT_${d}`};break;}else break;}}\n if(found)candidates.push(found);\n }\n return candidates.length?candidates[candidates.length-1]:{line:-1,val:null,method:''};\n}\nfunction textNearLabel(block,labelRe,valueRe){\n const lines=String(block||'').split('\\n'),out=[];for(let i=0;ip-q);return {a:x[0],b:x[1],start:dateKey(x[0]),end:dateKey(x[1]),display:`${fmtDate(x[0])} - ${fmtDate(x[1])}`};}\nfunction periodLibre(post){\n const lines=String(post||'').split('\\n');for(let i=0;ix[0]);if(same.length>=2)return mkPeriod(same[0],same[1]);const prev=[...lines.slice(Math.max(0,i-4),i+1).join(' ').matchAll(/\\d{1,2}\\/\\d{1,2}\\/20\\d{2}/g)].map(x=>x[0]);if(prev.length>=2)return mkPeriod(prev[prev.length-2],prev[prev.length-1]);const next=[...lines.slice(i,Math.min(lines.length,i+5)).join(' ').matchAll(/\\d{1,2}\\/\\d{1,2}\\/20\\d{2}/g)].map(x=>x[0]);if(next.length>=2)return mkPeriod(next[0],next[1]);}return null;\n}\nfunction mkPeriod(a,b){const d1=parseDMY(a),d2=parseDMY(b);if(!d1||!d2)return null;const x=[d1,d2].sort((p,q)=>p-q);return {startDate:x[0],endDate:x[1],start:dateKey(x[0]),end:dateKey(x[1]),display:`${fmtDate(x[0])} - ${fmtDate(x[1])}`};}\nfunction recordFingerprint(r){return [r.category,clean(r.code),norm(r.name),clean(r.date),clean(r.transaction),round(r.ss),round(r.se),round(r.gross),round(r.regularBase),round(r.dtmBase)].join('|');}\nfunction samePerson(a,b){const ac=clean(a.code).replace(/^0+(?=\\d)/,''),bc=clean(b.code).replace(/^0+(?=\\d)/,'');if(ac&&bc&&ac===bc)return true;return !!(norm(a.name)&&norm(a.name)===norm(b.name));}\nfunction vacFields(block){return {vac:moneyNearLabel(block,/Vacaciones(?!\\s+Correspondientes)/i),gross:moneyNearLabel(block,/TOTAL\\s+BRUTO\\s*:?/i),ss:moneyNearLabel(block,/Seg\\.\\s*Soc\\.?/i),se:moneyNearLabel(block,/Seg\\.\\s*Edu\\.?/i)};}\nfunction vacCandidateOK(f){if([f.vac.val,f.gross.val,f.ss.val,f.se.val].some(v=>v===null))return false;const g=f.gross.val;return close(f.vac.val,g,.08)&&close(f.ss.val,round(g*Number(rates.ssRegular||.0975)),.16)&&close(f.se.val,round(g*Number(rates.se||.0125)),.13);}\nfunction extraFields(block){return {gross:moneyNearLabel(block,/\\bTotales\\b/i),ss:moneyNearLabel(block,/Seg\\.\\s*Soc\\.?/i),se:moneyNearLabel(block,/Seg\\.\\s*Edu\\.?/i)};}\nfunction extraCandidateOK(f){if([f.gross.val,f.ss.val,f.se.val].some(v=>v===null))return false;const g=f.gross.val;return close(f.ss.val,round(g*Number(rates.ssRegular||.0975)),.16)&&close(f.se.val,round(g*Number(rates.se||.0125)),.13);}\nfunction liqFields(block){return {date:dateNearLabel(block,/FECHA\\s+DE\\s+LIQUIDACI[ÓO]N\\s*:?/i),transaction:txnNearLabel(block),ss:moneyNearLabel(block,/Seg\\.\\s*Soc\\.?/i),se:moneyNearLabel(block,/Seg\\.\\s*Edu\\.?/i),sv:moneyNearLabel(block,/Salario,?\\s*Vac,?\\s*13\\s*Mes\\s*:?/i),sal:moneyNearLabel(block,/Salario\\s+Reg\\.?\\s*:?/i),vac:moneyNearLabel(block,/^\\s*Vacaciones\\s*:?/i),dtm:moneyNearLabel(block,/^\\s*XIII\\s+Mes\\s*:?/i),pai:moneyNearLabel(block,/Preav,?\\s*P\\s*Ant,?\\s*Indem\\s*:?/i),gross:moneyNearLabel(block,/Total\\s+Bruto\\s*:?/i)};}\nfunction liqCandidateOK(f){if(!f.date||!f.transaction||[f.ss.val,f.se.val,f.sv.val,f.sal.val,f.vac.val,f.dtm.val,f.pai.val,f.gross.val].some(v=>v===null))return false;const rb=round(f.sal.val+f.vac.val),compSV=round(rb+f.dtm.val),compGross=round(f.sv.val+f.pai.val),es=round(rb*Number(rates.ssRegular||.0975)+f.dtm.val*Number(rates.ssDtm||.0725)),ee=round(rb*Number(rates.se||.0125));return close(f.sv.val,compSV,.14)&&close(f.gross.val,compGross,.14)&&close(f.ss.val,es,.17)&&close(f.se.val,ee,.13);}\nconst records=[],validations=[],supportDocs=[],files=[];\nfunction validation(status,category,fileName,detail){validations.push({status,severity:(status==='OK'||status==='IGNORADO'||status==='EXCLUIDO')?'INFO':'WARNING',category,fileName,detail});}\nfunction addRecord(r){records.push({...r,ss:round(r.ss||0),se:round(r.se||0),gross:round(r.gross||0),regularBase:round(r.regularBase||0),dtmBase:round(r.dtmBase||0),parserMode:'STRUCTURE_AWARE_V10'});}\nfor(let i=0;i\\n'):String(raw||''));if(!text.trim()){validation('REVISAR',category,fileName,'PDF sin texto extraíble; archivo excluido.');continue;}\n if(category==='vacaciones'){\n const ms=markers(text,/Empleado\\s*#\\s*(\\d+)\\s*:\\s*(.+?)(?=\\s+(?:Clave\\s+ISR|Vacaciones\\s+Correspondientes|Periodo\\s+Libre)|[\\n\\r]|$)/i);if(!ms.length){validation('REVISAR',category,fileName,'No se detectaron empleados.');continue;}\n const firstPre=vacFields(segPre(text,ms,0)),orientation=vacCandidateOK(firstPre)?'PRE':'POST',planillaNumber=globalPlanilla(text),generatedAt=globalGenerated(text);let inc=0,out=0,review=0;\n for(let j=0;j({code:clean(x.match[1]).replace(/^0+(?=\\d)/,''),name:clean(x.match[2]).replace(/\\s+(?:A[nñ]o|C[eé]dula:).*$/i,'')}));const seen=new Set(),employees=[];for(const e of rawm){const k=e.code+'|'+norm(e.name);if(!seen.has(k)){seen.add(k);employees.push(e);}}supportDocs.push({fileName,employees});validation('IGNORADO',category,fileName,`Documento de cálculo/soporte (${employees.length} empleado(s)); no se suma.`);continue;}\n const ms=markers(text,/Empleado\\s*#\\s*(\\d+)\\s*:\\s*(.+?)(?=\\s+(?:FECHA\\s+DE\\s+LIQUIDACI[ÓO]N|Clave\\s+ISR|N[uú]mero\\s+de\\s+Transacci[oó]n)|[\\n\\r]|$)/i);if(!ms.length){validation('REVISAR',category,fileName,'No se detectaron empleados resumidos.');continue;}const firstPre=liqFields(segPre(text,ms,0)),orientation=liqCandidateOK(firstPre)?'PRE':'POST',planillaNumber=globalPlanilla(text),generatedAt=globalGenerated(text);let inc=0,out=0,review=0;\n for(let j=0;j(Number(b.planillaNumber)||0)-(Number(a.planillaNumber)||0)||String(b.generatedAt).localeCompare(String(a.generatedAt)));for(const r of arr.slice(1)){r.included=false;r.reason=`Sustituida por planilla de vacaciones más reciente (${arr[0].planillaNumber||arr[0].fileName}).`;}}\nconst liqAll=records.filter(r=>r.category==='Liquidación');for(const doc of supportDocs){const missing=(doc.employees||[]).filter(e=>!liqAll.some(r=>samePerson(e,r)));if(missing.length)validation('REVISAR','liquidaciones',doc.fileName,`Documento de cálculo contiene persona(s) sin planilla resumida: ${missing.map(x=>`${x.name}${x.code?` (#${x.code})`:''}`).join(', ')}.`);}\nconst includedRecords=records.filter(r=>r.included),catCount=cat=>includedRecords.filter(r=>r.category===cat).length;return {records,validations,supportDocs,includedRecords,files:[...new Set(files)],counts:{extraordinarias:catCount('Extraordinaria'),vacaciones:catCount('Vacaciones'),liquidaciones:catCount('Liquidación')},totals:{extraSS:round(includedRecords.filter(r=>r.category==='Extraordinaria').reduce((s,r)=>s+r.ss,0)),vacSS:round(includedRecords.filter(r=>r.category==='Vacaciones').reduce((s,r)=>s+r.ss,0)),liqSS:round(includedRecords.filter(r=>r.category==='Liquidación').reduce((s,r)=>s+r.ss,0)),totalSS:round(includedRecords.reduce((s,r)=>s+r.ss,0)),totalSE:round(includedRecords.reduce((s,r)=>s+r.se,0))},parserVersion:'STRUCTURE_AWARE_V10_SIN_IA',quality:{status:validations.some(v=>v.status==='REVISAR')?'REVISAR':'VALIDADO',rule:'La orientación PRE/POST se determina con el primer empleado del archivo mediante controles matemáticos. La misma orientación se aplica a todo el archivo. Vacaciones lee Periodo Libre siempre del bloque posterior. No hay alineación global por listas.'}};\n}\n\nconst __result = parseSupplementals($input.all(), $items('Normalizar PDFs descargados'), (($items('Parsear CSS')[0]||{}).json||{}), (($items('Inicializar ejecución')[0]||{}).json||{}));\nreturn [{json:__result}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 28928, - 26832 + 61904, + 34832 ], - "id": "ed614c7e-da77-4670-86ac-ba83d855ba21", + "id": "14f0d70e-f329-49c3-b500-4fdad7c2112f", "name": "Parsear PDFs", "notesInFlow": true, - "notes": "Parser V7 SIN IA: lectura determinista por líneas ancladas a etiquetas dentro del bloque exacto de cada empleado; sin alineación global ni búsqueda por proximidad amplia." + "notes": "Parser complementarios V8 SIN IA: extracción directa por campos del mismo empleado, detección PRE/POST por archivo, validación contable/tasas, soporte para PDF.js y decimales fragmentados. No alinea listas globales ni toma montos de empleados vecinos." }, { "parameters": { @@ -2248,10 +2252,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 29168, - 26832 + 62144, + 34832 ], - "id": "cae896d0-72d3-424a-ae45-3c3483b9fbd3", + "id": "6e10d4ed-e0ff-4f3c-bbd4-503735047765", "name": "Construir reporte producción" }, { @@ -2268,10 +2272,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 29408, - 26832 + 62384, + 34832 ], - "id": "6fa8dc8f-bd83-445f-97d7-e1acdedddc97", + "id": "bac4af9b-bb77-4456-9e17-be8fa3af8587", "name": "Crear Google Sheet Reporte", "credentials": { "googleSheetsOAuth2Api": { @@ -2294,10 +2298,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 29648, - 26832 + 62624, + 34832 ], - "id": "fe78de18-a2d4-4866-8e99-4a87819129fc", + "id": "acc027fa-0b8e-42ae-81d3-e8fb393bf755", "name": "Escribir reporte", "credentials": { "googleSheetsOAuth2Api": { @@ -2313,10 +2317,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 29888, - 26832 + 62864, + 34832 ], - "id": "6e359784-7724-4dcc-82f3-e8ec5867288b", + "id": "023c71e6-974a-4ac4-9c19-cdeb97719dd6", "name": "Preparar formato reporte" }, { @@ -2333,10 +2337,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 30128, - 26832 + 63104, + 34832 ], - "id": "d4092b24-112e-4cbc-89a8-757458c3baec", + "id": "176ef0a3-091f-493a-ac08-53addee2e6b1", "name": "Formatear reporte", "credentials": { "googleSheetsOAuth2Api": { @@ -2352,10 +2356,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 30368, - 26832 + 63344, + 34832 ], - "id": "c569cb6f-97bc-42ef-bfc0-0636ef6f98b1", + "id": "f12bf3d7-f5c9-4b12-93bc-a0da50c9c629", "name": "Preparar permisos" }, { @@ -2372,10 +2376,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 30608, - 26832 + 63584, + 34832 ], - "id": "164892df-85f8-4265-bb47-a21c7700f6f4", + "id": "63765f02-d4c8-4911-bf9d-7f768ed382ff", "name": "Compartir reporte como editor", "credentials": { "googleDriveOAuth2Api": { @@ -2392,10 +2396,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 30848, - 26832 + 63824, + 34832 ], - "id": "bdee4a88-ed77-41b1-a4ea-c0321f5587d2", + "id": "40a9e530-9b60-4450-bfa3-0d7d5e2e95d3", "name": "Preparar correo HTML GLM" }, { @@ -2410,10 +2414,10 @@ "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ - 31088, - 26832 + 64064, + 34832 ], - "id": "1cf118be-34f7-46ff-9a83-414bdb9bdf5a", + "id": "33c125b7-9b00-4e68-b197-2cfcd8651f55", "name": "Enviar correo resumen", "webhookId": "ae5f87e7-df2b-4349-8eb5-aac208accb96", "executeOnce": true, @@ -2431,10 +2435,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 31808, - 26832 + 64784, + 34832 ], - "id": "b99cc3cd-a57e-401b-aab3-b671895bdc05", + "id": "034abc56-cf5b-4a09-a6c5-c110c46683eb", "name": "Preparar confirmación" }, { @@ -2446,10 +2450,10 @@ "type": "n8n-nodes-base.form", "typeVersion": 2.5, "position": [ - 32048, - 26832 + 65024, + 34832 ], - "id": "716a9401-cf32-4eac-b329-8d318f0c686b", + "id": "b949ed7f-046d-4d13-beaf-d064a8b6e5bf", "name": "Confirmación y regreso al formulario", "webhookId": "d4a102a8-3d72-4c5b-83b2-0e0d04c71099", "notesInFlow": true, @@ -2482,10 +2486,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 21488, - 26912 + 54464, + 34912 ], - "id": "5abe74ca-8b12-4ee2-8289-ff31fa149407", + "id": "1c33ef12-3450-4ad1-b9f9-f742d02bd5c6", "name": "¿Q1 es PDF?" }, { @@ -2496,10 +2500,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 21728, - 27056 + 54704, + 35056 ], - "id": "34048913-682f-42a2-bc3f-8b02746dc92b", + "id": "c291f41f-cb07-4314-9e34-cb20c98f63e5", "name": "Extraer texto Q1 PDF" }, { @@ -2509,10 +2513,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 21984, - 27024 + 54960, + 35024 ], - "id": "c0af9861-2562-41d1-a15b-00b2e42b7bf2", + "id": "e256e1d6-2774-462c-9d55-5a031485d6cb", "name": "Parsear Q1 PDF" }, { @@ -2522,10 +2526,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 23648, - 26832 + 56624, + 34832 ], - "id": "321d3b2f-837e-48b3-af92-8662b625adc7", + "id": "3d6d60e8-487a-41cc-b256-46d813aace5c", "name": "Resultado Q1" }, { @@ -2555,10 +2559,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 24128, - 26912 + 57104, + 34912 ], - "id": "ed7c9435-b43d-4e4c-a702-23127d54fe1f", + "id": "b4e4e628-7ddb-475a-8983-892aac060cf7", "name": "¿Q2 es PDF?" }, { @@ -2569,10 +2573,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 24368, - 27056 + 57344, + 35056 ], - "id": "e7dfdc83-f288-48e3-82ef-72bc1ca65ca4", + "id": "3223cc16-c235-4286-ab4b-6caa1bc402b7", "name": "Extraer texto Q2 PDF" }, { @@ -2582,10 +2586,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 24608, - 27056 + 57584, + 35056 ], - "id": "c8d62ff5-5098-4509-b79d-0b2a670cbbec", + "id": "dd41d8e5-d079-4422-a22c-ca2010fa31c2", "name": "Parsear Q2 PDF" }, { @@ -2595,10 +2599,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 25568, - 26832 + 58544, + 34832 ], - "id": "012df9a7-6095-4322-8f93-8b4d24986e54", + "id": "22ae7500-d025-4854-aeab-7b6827c4fb14", "name": "Resultado Q2" }, { @@ -2608,10 +2612,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 31328, - 26832 + 64304, + 34832 ], - "id": "e83d7ddc-c884-4034-acc3-f6331f491608", + "id": "a358393b-3223-450e-a6db-4a146838f266", "name": "Preparar limpieza temporales" }, { @@ -2625,10 +2629,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 31568, - 26832 + 64544, + 34832 ], - "id": "d7d69f76-df90-4f5c-bad4-536410e6fc9d", + "id": "c20af9b0-54e4-44c9-be47-5c277d575807", "name": "Eliminar staging temporal", "alwaysOutputData": true, "notesInFlow": true, @@ -2655,10 +2659,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 21248, - 27216 + 54224, + 35216 ], - "id": "b16c3231-7cda-4395-b4a7-6b17cc5be53d", + "id": "09fa4c3c-7a56-46ae-a0ef-2bd5030a1417", "name": "Crear carpeta temporal PDFs", "retryOnFail": true, "maxTries": 3, @@ -2702,10 +2706,10 @@ "type": "n8n-nodes-base.form", "typeVersion": 2.5, "position": [ - 21488, - 27216 + 54464, + 35216 ], - "id": "b784edea-6563-4f63-b8cb-900f2850753e", + "id": "8f7476cf-6957-4714-9cd3-9f71feb1e687", "name": "Definir Extraordinarias", "webhookId": "2d4fba32-6a24-46e8-a9c7-a38ca80b106a" }, @@ -2736,10 +2740,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 21728, - 27216 + 54704, + 35216 ], - "id": "534cbd73-479a-4797-848c-a012e29bbc58", + "id": "953ada8a-80d8-40ee-a130-c040a9376a09", "name": "¿Hay extraordinarias?" }, { @@ -2778,10 +2782,10 @@ "type": "n8n-nodes-base.form", "typeVersion": 2.5, "position": [ - 21984, - 27024 + 54960, + 35024 ], - "id": "c0214ec8-c0ef-4e9d-bf81-b1cc55eaffef", + "id": "4bf3e422-a9fe-4f98-9122-c3c4edff7f0f", "name": "Cargar Extraordinarias", "webhookId": "237dd241-cbba-4455-beec-b4b63e45827d" }, @@ -2792,10 +2796,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 22224, - 27024 + 55200, + 35024 ], - "id": "693a4ce9-c131-4bb1-9809-c455482b0b3a", + "id": "d7cbe170-a566-42bf-9653-d5282de9a8c7", "name": "Preparar lote Extraordinarias", "notesInFlow": true, "notes": "Valida PDF y prepara cada archivo del lote para guardarlo temporalmente." @@ -2819,10 +2823,10 @@ "type": "n8n-nodes-base.googleDrive", "typeVersion": 3, "position": [ - 22464, - 27024 + 55440, + 35024 ], - "id": "bc86f7bb-749b-4c13-9a20-3411dcb14f94", + "id": "88420c7c-55f7-46e5-8a22-fe6f5fb91182", "name": "Guardar lote Extraordinarias", "retryOnFail": true, "maxTries": 3, @@ -2841,10 +2845,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 22704, - 27024 + 55680, + 35024 ], - "id": "4c14fe7c-7ee1-4ba6-b214-1f7f4b450c06", + "id": "11655d6b-b8c4-4931-bfe5-7cc6e21d65be", "name": "Cerrar lote Extraordinarias" }, { @@ -2854,7 +2858,7 @@ "caseSensitive": true, "leftValue": "", "typeValidation": "strict", - "version": 2 + "version": 3 }, "conditions": [ { @@ -2874,10 +2878,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 22944, - 27024 + 55920, + 35024 ], - "id": "801c384f-0904-474c-a036-f90f13861d57", + "id": "b7f00446-e4a5-4314-a0dc-f27916f57120", "name": "¿Agregar más Extraordinarias?" }, { @@ -2916,10 +2920,10 @@ "type": "n8n-nodes-base.form", "typeVersion": 2.5, "position": [ - 23184, - 27184 + 56160, + 35184 ], - "id": "cdcf55f9-3935-4775-bbd1-0f7af2053895", + "id": "cacdbf95-a924-469f-8b6b-5ec5ed9043c4", "name": "Cargar Vacaciones", "webhookId": "4405d87c-973b-4828-8f0b-e75ec83473f3" }, @@ -2930,10 +2934,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 23424, - 27184 + 56400, + 35184 ], - "id": "db348186-5aa6-48ca-b987-b608371a6135", + "id": "615b13f0-62b6-41d9-b73c-df8156f4fbd4", "name": "Preparar lote Vacaciones" }, { @@ -2955,10 +2959,10 @@ "type": "n8n-nodes-base.googleDrive", "typeVersion": 3, "position": [ - 23664, - 27184 + 56640, + 35184 ], - "id": "765675c4-1b52-47cd-8548-c7ee8e672795", + "id": "8ef5b6f6-e0c0-46e6-9063-ac981022c548", "name": "Guardar lote Vacaciones", "retryOnFail": true, "maxTries": 3, @@ -2977,10 +2981,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 23888, - 27216 + 56864, + 35216 ], - "id": "a132a6cb-3992-489b-992d-391f1b25d939", + "id": "cd731fba-163e-4102-867f-93fdd05161c9", "name": "Cerrar lote Vacaciones" }, { @@ -3010,10 +3014,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 24128, - 27216 + 57104, + 35216 ], - "id": "42d058c1-e95d-4f50-97eb-92c8b676f519", + "id": "fb6e392f-5d4a-4474-baf9-bb4079018fdc", "name": "¿Agregar más Vacaciones?" }, { @@ -3052,10 +3056,10 @@ "type": "n8n-nodes-base.form", "typeVersion": 2.5, "position": [ - 24368, - 27216 + 57344, + 35216 ], - "id": "b1b680ab-5f7e-46eb-ac89-217fc6e1c5bb", + "id": "4a7d5715-a5fe-4a86-84d4-78ad63edb8ea", "name": "Cargar Liquidaciones", "webhookId": "f04eb408-97b4-4c5c-9c90-53e58021ea20" }, @@ -3066,10 +3070,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 24608, - 27216 + 57584, + 35216 ], - "id": "73add3fa-2c1f-4b49-90a2-aeb8946e6407", + "id": "1aacdced-cc5a-459e-9314-cbe0c48535ec", "name": "Preparar lote Liquidaciones" }, { @@ -3091,10 +3095,10 @@ "type": "n8n-nodes-base.googleDrive", "typeVersion": 3, "position": [ - 24848, - 27216 + 57824, + 35216 ], - "id": "65feccb0-75ee-4dac-a387-d104b3eb6b77", + "id": "a68ad67c-236c-4417-a527-f570dcf6803a", "name": "Guardar lote Liquidaciones", "retryOnFail": true, "maxTries": 3, @@ -3113,10 +3117,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 25088, - 27216 + 58064, + 35216 ], - "id": "4cbe541e-cdcc-4f61-a8b3-3ed8f4fa5915", + "id": "6ef08c3a-b33f-4cd7-af93-584be7575f7f", "name": "Cerrar lote Liquidaciones" }, { @@ -3146,10 +3150,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 25328, - 27216 + 58304, + 35216 ], - "id": "eb344e25-564f-47fe-9c2f-78196123900c", + "id": "435f9ae2-7ccb-479f-8078-6f895bd4726a", "name": "¿Agregar más Liquidaciones?" }, { @@ -3162,10 +3166,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 28448, - 27056 + 61424, + 35056 ], - "id": "e3645976-82e4-4464-ac32-8a1be494f3df", + "id": "1b450788-edf6-472c-8497-338312040c0d", "name": "Listar PDFs temporales", "retryOnFail": true, "maxTries": 3, @@ -3184,10 +3188,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 28688, - 27056 + 61664, + 35056 ], - "id": "dc199c71-3269-4587-92fa-404ad7815521", + "id": "b525df3e-0968-489a-80d7-436a9697f00a", "name": "Preparar descarga PDFs", "notesInFlow": true, "notes": "Reconstruye las tres categorías desde la carpeta temporal y valida mínimos antes de descargar." @@ -3208,10 +3212,10 @@ "type": "n8n-nodes-base.googleDrive", "typeVersion": 3, "position": [ - 28928, - 27056 + 61904, + 35056 ], - "id": "f0ce2d8b-da54-4aa9-9c1a-560b90f5e2c1", + "id": "b5e9a2fb-a85c-45fc-b819-431840e1e007", "name": "Descargar PDFs temporales", "retryOnFail": true, "maxTries": 3, @@ -3230,10 +3234,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 29168, - 27056 + 62144, + 35056 ], - "id": "b169ad1c-478f-4fc9-b80d-2d4d020c86b8", + "id": "833ab32f-3fad-4271-8a6d-81923f4eda84", "name": "Normalizar PDFs descargados" } ], @@ -3920,15 +3924,15 @@ } }, "authors": "Isaac Aracena", - "name": "Version 6b4a28c8", + "name": "Version d6cac1fb", "description": "", "autosaved": true, "workflowPublishHistory": [ { - "createdAt": "2026-08-21T21:02:10.870Z", - "id": 5116, + "createdAt": "2026-08-26T01:49:22.089Z", + "id": 5223, "workflowId": "qxcXg0Z1YASkWgxL", - "versionId": "6b4a28c8-b9fc-42c1-a440-6bd1aae0ab20", + "versionId": "d6cac1fb-fb4b-4bef-a2b3-146b21e84ac4", "event": "activated", "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" }