""" Construye el workflow V4 a partir de V3: - Quita el campo 'cliente' del webhook: ahora se EXTRAE del PDF. - Reemplaza el nodo "Validar cliente" por "Extraer cliente del PDF" que toma nombre_receptor de Mergear chunks. - Anade despues de "Append a consolidado": - Leer Sheet consolidado entero (HTTP GET a Sheets API) - Preparar JSON para Python - Llenar plantilla con Python via stdin/base64 - Leer XLSX resultante como binario - Responder al webhook con el XLSX como descarga """ import json import copy SRC = "C:/Users/prevo/Videos/Estados de cuenta/ESTADOS_DE_CUENTA_V3.json" DST = "C:/Users/prevo/Videos/Estados de cuenta/ESTADOS_DE_CUENTA_V4.json" PARENT_FOLDER_ID = "1HJBC0f6JW_oPgdvI2HcNNg20XhDQ78bz" with open(SRC, "r", encoding="utf-8") as f: wf = json.load(f) # === FASE 1: Reemplazar 'Validar cliente' por 'Extraer cliente del PDF' === # El nuevo nodo se EJECUTA DESPUES de Mergear chunks (no antes de Guardar PDF). # Asi el cliente se obtiene del encabezado.nombre_receptor. new_nodes = [] removed_names = {"Validar cliente"} for n in wf["nodes"]: if n["name"] in removed_names: continue new_nodes.append(n) wf["nodes"] = new_nodes # Reconectar Webhook -> Guardar PDF Temp directamente wf["connections"]["Webhook"] = {"main": [[{"node": "Guardar PDF Temp", "type": "main", "index": 0}]]} if "Validar cliente" in wf["connections"]: del wf["connections"]["Validar cliente"] # Insertar nuevo nodo "Extraer cliente del PDF" entre Mergear chunks y Buscar carpeta cliente EXTRAER_CLIENTE = { "parameters": { "jsCode": ( "// Extrae el nombre del cliente desde el encabezado del PDF y lo normaliza\n" "const data = $input.first().json;\n" "const enc = data.encabezado || {};\n" "// Probar varios campos del encabezado en orden de preferencia\n" "let raw = enc.nombre_receptor || enc.nombre_titular || enc.titular || enc.nombre || '';\n" "raw = String(raw || '').trim();\n" "// Si Gemini metio multiples nombres en uno (ej: 'Y/O'), tomar el primero\n" "raw = raw.split(/\\s+Y\\/?O\\s+/i)[0].trim();\n" "if (!raw) {\n" " // Fallback: usar banco + fecha si no hay nombre\n" " raw = `SIN_NOMBRE_${data.banco || 'X'}_${$now.format('yyyyMMdd')}`;\n" "}\n" "// Normalizar: mayusculas, sin acentos, espacios a _, sin chars especiales\n" "let cliente = raw.normalize('NFD').replace(/[\\u0300-\\u036f]/g, '');\n" "cliente = cliente.toUpperCase().replace(/\\s+/g, '_').replace(/[^A-Z0-9_-]/g, '');\n" "// Truncar a 60 chars para nombre de carpeta sano\n" "cliente = cliente.substring(0, 60);\n" "if (!cliente) cliente = 'CLIENTE_SIN_NOMBRE';\n" "return [{ json: {\n" " cliente,\n" " cliente_raw: raw,\n" " banco: data.banco,\n" " encabezado: enc,\n" " transacciones: data.transacciones || [],\n" " _meta: data._meta\n" "} }];" ) }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [-1104, -544], "id": "a1b2c3d4-4001-4001-4001-000000004001", "name": "Extraer cliente del PDF" } wf["nodes"].append(EXTRAER_CLIENTE) # Reconectar: Mergear chunks -> Extraer cliente del PDF -> Buscar carpeta cliente wf["connections"]["Mergear chunks"] = {"main": [[{"node": "Extraer cliente del PDF", "type": "main", "index": 0}]]} wf["connections"]["Extraer cliente del PDF"] = {"main": [[{"node": "Buscar carpeta cliente", "type": "main", "index": 0}]]} # Actualizar referencias a $('Validar cliente').item.json.cliente -> $('Extraer cliente del PDF') def replace_in_node(n): s = json.dumps(n, ensure_ascii=False) s = s.replace("$('Validar cliente').item.json.cliente", "$('Extraer cliente del PDF').item.json.cliente") return json.loads(s) wf["nodes"] = [replace_in_node(n) for n in wf["nodes"]] # === FASE 2: Anadir nodos finales para llenar plantilla y devolver XLSX === NUEVOS = [ # 1. Leer Sheet consolidado (todas las filas) { "parameters": { "method": "GET", "url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $('Set sheet_id').item.json.sheet_consolidado_id }}/values/A2:L?majorDimension=ROWS", "authentication": "predefinedCredentialType", "nodeCredentialType": "googleSheetsOAuth2Api", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, "position": [2256, -544], "id": "a1b2c3d4-4010-4010-4010-000000004010", "name": "Leer datos consolidado" }, # 2. Preparar JSON para Python (con cliente + todas las transacciones) { "parameters": { "jsCode": ( "// Construye el JSON que se pasara al script Python por stdin (base64)\n" "const cliente = $('Extraer cliente del PDF').item.json.cliente;\n" "const cliente_raw = $('Extraer cliente del PDF').item.json.cliente_raw;\n" "const filas = $input.first().json.values || [];\n" "// Filas vienen como arrays paralelos a las columnas: Fecha, Concepto, Tipo, Banco, Categoria, Deposito, Retiro, Monto, Mes, Anno, MesAnno, Trimestre\n" "const transacciones = filas.map(r => ({\n" " Fecha: r[0] || '',\n" " Concepto: r[1] || '',\n" " Tipo: r[2] || '',\n" " Banco: r[3] || '',\n" " Categoria: r[4] || '',\n" " Deposito: parseFloat(r[5]) || 0,\n" " Retiro: parseFloat(r[6]) || 0,\n" " Monto: parseFloat(r[7]) || 0\n" "}));\n" "const payload = { cliente, cliente_raw, transacciones };\n" "const json_str = JSON.stringify(payload);\n" "const json_b64 = Buffer.from(json_str, 'utf-8').toString('base64');\n" "const exec_id = $execution.id;\n" "return [{ json: {\n" " cliente, cliente_raw,\n" " json_b64,\n" " total_transacciones: transacciones.length,\n" " output_path: `/tmp/expediente_${exec_id}.xlsx`\n" "} }];" ) }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [2480, -544], "id": "a1b2c3d4-4011-4011-4011-000000004011", "name": "Preparar payload Python" }, # 3. Ejecutar Python con el JSON via stdin { "parameters": { "command": "=echo '{{ $json.json_b64 }}' | base64 -d | python3 /scripts/fill_template.py '{{ $json.output_path }}' /scripts/EXPEDIENTE_FINANCIERO_FINAL.xlsx" }, "type": "n8n-nodes-base.executeCommand", "typeVersion": 1, "position": [2704, -544], "id": "a1b2c3d4-4012-4012-4012-000000004012", "name": "Llenar plantilla" }, # 4. Leer XLSX como binario { "parameters": { "filePath": "={{ $('Preparar payload Python').item.json.output_path }}" }, "type": "n8n-nodes-base.readBinaryFile", "typeVersion": 1, "position": [2928, -544], "id": "a1b2c3d4-4013-4013-4013-000000004013", "name": "Leer XLSX resultante" }, # 5. (Opcional) subir el XLSX llenado a la carpeta del cliente en Drive { "parameters": { "operation": "upload", "name": "=EXPEDIENTE_{{ $('Extraer cliente del PDF').item.json.cliente }}_{{ $now.format('yyyyMMdd_HHmmss') }}.xlsx", "driveId": {"__rl": True, "mode": "list", "value": "My Drive"}, "folderId": {"__rl": True, "mode": "id", "value": "={{ $('Set sheet_id').item.json.carpeta_id }}"}, "options": {} }, "type": "n8n-nodes-base.googleDrive", "typeVersion": 3, "position": [3152, -704], "id": "a1b2c3d4-4014-4014-4014-000000004014", "name": "Guardar XLSX en Drive" }, # 6. Responder al webhook con el binario (descarga) { "parameters": { "respondWith": "binary", "responseBinaryPropertyName": "data", "options": { "responseHeaders": { "entries": [ { "name": "Content-Disposition", "value": "=attachment; filename=\"EXPEDIENTE_{{ $('Extraer cliente del PDF').item.json.cliente }}.xlsx\"" }, { "name": "Content-Type", "value": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, { "name": "Access-Control-Expose-Headers", "value": "Content-Disposition" } ] } } }, "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.5, "position": [3152, -544], "id": "a1b2c3d4-4015-4015-4015-000000004015", "name": "Responder con XLSX" }, ] wf["nodes"].extend(NUEVOS) # === FASE 3: Reconectar el final del flujo === # Antes: Append a consolidado -> Responder OK # Ahora: Append a consolidado -> Leer datos consolidado -> Preparar payload Python # -> Llenar plantilla -> Leer XLSX resultante # -> [paralelo] Guardar XLSX en Drive + Responder con XLSX # Eliminar el "Responder OK" antiguo (lo reemplaza "Responder con XLSX") wf["nodes"] = [n for n in wf["nodes"] if n["name"] != "Responder OK"] if "Responder OK" in wf["connections"]: del wf["connections"]["Responder OK"] # Reconectar Append a consolidado -> Leer datos consolidado wf["connections"]["Append a consolidado"] = {"main": [[{"node": "Leer datos consolidado", "type": "main", "index": 0}]]} # Cadena nueva del final wf["connections"]["Leer datos consolidado"] = {"main": [[{"node": "Preparar payload Python", "type": "main", "index": 0}]]} wf["connections"]["Preparar payload Python"] = {"main": [[{"node": "Llenar plantilla", "type": "main", "index": 0}]]} wf["connections"]["Llenar plantilla"] = {"main": [[{"node": "Leer XLSX resultante", "type": "main", "index": 0}]]} wf["connections"]["Leer XLSX resultante"] = {"main": [[ {"node": "Responder con XLSX", "type": "main", "index": 0}, {"node": "Guardar XLSX en Drive", "type": "main", "index": 0} ]]} # Renombrar workflow wf["name"] = "ESTADOS DE CUENTA V4" wf.pop("versionId", None) wf.pop("id", None) wf.pop("meta", None) wf.pop("active", None) # Guardar with open(DST, "w", encoding="utf-8") as f: json.dump(wf, f, ensure_ascii=False, indent=2) # Verificacion print(f"V4 generado: {len(wf['nodes'])} nodos, {len(wf['connections'])} conexiones") node_names = {n["name"] for n in wf["nodes"]} dangling = [] for src, conf in wf["connections"].items(): if src not in node_names: dangling.append(f"Source {src} no existe") for outs in conf.get("main", []): for c in outs: if c["node"] not in node_names: dangling.append(f"{src} -> {c['node']} no existe") print("Dangling:", dangling if dangling else "ninguna") print("\nNodos finales:") for n in wf["nodes"]: print(f" - {n['name']}")