AutomationFlowsWeb Scraping › Resumen Ventas

Resumen Ventas

Resumen Ventas. Uses httpRequest, emailSend. Webhook trigger; 8 nodes.

Webhook trigger★★★★☆ complexity8 nodesHTTP RequestEmail Send
Web Scraping Trigger: Webhook Nodes: 8 Complexity: ★★★★☆ Added:

This workflow follows the Emailsend → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "resumen-7dias-email",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "5eeadaa1-ce54-4025-9850-e2aed9b242d0",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1,
      "position": [
        -240,
        -160
      ]
    },
    {
      "parameters": {
        "functionCode": "// Normaliza entrada y periodo\nconst b = $json.body || $json || {};\nconst toEmail = (b.toEmail || 'rubencereceda23@gmail.com') + '';\nconst subject = (b.subject || 'Resumen ventas \u00faltimos 7 d\u00edas') + '';\n\nfunction iso(d){ return new Date(d).toISOString(); }\nconst now = new Date();\nconst defaultFrom = new Date(Date.now() - 7*24*60*60*1000);\nlet from = b.from ? new Date(b.from) : defaultFrom;\nlet to = b.to ? new Date(b.to) : now;\nif (isNaN(from)) from = defaultFrom;\nif (isNaN(to)) to = now;\n\nreturn [{ json: { toEmail, subject, periodo: { from: iso(from), to: iso(to) } } }];"
      },
      "id": "8d370ec6-cdad-430f-99ff-2922f7689104",
      "name": "Preparar par\u00e1metros",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -48,
        -160
      ]
    },
    {
      "parameters": {
        "url": "https://api-vapers.onrender.com/api/ventas",
        "options": {}
      },
      "id": "ed67668c-3041-4601-981e-2fd37fecbb16",
      "name": "Obtener ventas (todas)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        160,
        -256
      ]
    },
    {
      "parameters": {
        "mode": "passThrough"
      },
      "id": "7357c5d6-dcfe-416e-9196-590aa888148d",
      "name": "Merge ventas + params",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        368,
        -208
      ]
    },
    {
      "parameters": {
        "functionCode": "// Filtrar por periodo recibido y calcular m\u00e9tricas (determinista)\nconst params = $json || {};\nconst periodo = params.periodo || {};\nconst desde = new Date(periodo.from || new Date(Date.now() - 7*24*60*60*1000));\nconst hasta = new Date(periodo.to || new Date());\n\nconst todas = Array.isArray(params.data) ? params.data : (Array.isArray(params.ventas) ? params.ventas : (Array.isArray($json) ? $json : []));\n\nconst ventas = todas.filter(v => {\n  if (!v.fecha) return false;\n  const d = new Date(v.fecha);\n  return d && !isNaN(d) && d >= desde && d <= hasta;\n});\n\nlet ingresoTotal = 0; let numVentas = ventas.length; let unidades = 0;\nconst porProducto = new Map();\nconst porCliente = new Map();\n\nfor (const v of ventas) {\n  const cantidad = Number(v.cantidad ?? 1);\n  const total = v.total != null ? Number(v.total) : Number(cantidad * Number(v.precio_unitario ?? 0));\n  ingresoTotal += isNaN(total) ? 0 : total;\n  unidades += isNaN(cantidad) ? 0 : cantidad;\n\n  // nombre desde API de ventas (join ya hecho en backend)\n  const nombreProducto = v.producto?.nombre || v.producto_nombre || v.nombre || v.sku || (v.id_vaper ? `Producto ${v.id_vaper}` : 'Desconocido');\n  const p = porProducto.get(nombreProducto) || { unidades: 0, ingresos: 0 };\n  p.unidades += isNaN(cantidad) ? 0 : cantidad;\n  p.ingresos += isNaN(total) ? 0 : total;\n  porProducto.set(nombreProducto, p);\n\n  const cKey = (v.cliente ?? v.usuario ?? 'Desconocido') + '';\n  porCliente.set(cKey, (porCliente.get(cKey) || 0) + (isNaN(total) ? 0 : total));\n}\n\nconst ticketMedio = numVentas > 0 ? ingresoTotal / numVentas : 0;\nconst topProductos = [...porProducto.entries()].map(([producto, s]) => ({ producto, ...s })).sort((a,b)=> b.ingresos - a.ingresos).slice(0,5);\nconst topClientePair = [...porCliente.entries()].sort((a,b)=> b[1] - a[1])[0] || [null, 0];\n\nreturn [{ json: {\n  toEmail: params.toEmail,\n  subject: params.subject,\n  periodo: { from: desde.toISOString(), to: hasta.toISOString() },\n  resumen: { ingresoTotal, numVentas, unidades, ticketMedio },\n  topProductos,\n  topCliente: { nombre: topClientePair[0], gasto: topClientePair[1] },\n  ventas\n} }];"
      },
      "id": "9fb5a071-8905-4cd2-aa14-13f54d98fd2d",
      "name": "Filtrar + Agregar (por periodo)",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        560,
        -208
      ]
    },
    {
      "parameters": {
        "functionCode": "// Construir HTML con CSS inline para email\nconst d = $json;\nconst r = d.resumen || {};\nconst tp = d.topProductos || [];\nconst tc = d.topCliente || {};\n\nconst nf = new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 });\nconst money = (n)=> nf.format(Number(n||0));\nconst esc = (s)=> String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');\n\nconst periodoDesde = new Date(d.periodo?.from || '').toLocaleString('es-ES', { timeZone: 'Europe/Madrid' });\nconst periodoHasta = new Date(d.periodo?.to || '').toLocaleString('es-ES', { timeZone: 'Europe/Madrid' });\n\nconst rows = tp.length ? tp.map((p,i)=> `\n  <tr>\n    <td style=\"border:1px solid #e5e7eb; padding:10px; text-align:center;\">${i+1}</td>\n    <td style=\"border:1px solid #e5e7eb; padding:10px;\">${esc(p.producto)}</td>\n    <td style=\"border:1px solid #e5e7eb; padding:10px; text-align:right;\">${p.unidades ?? 0}</td>\n    <td style=\"border:1px solid #e5e7eb; padding:10px; text-align:right;\">${money(p.ingresos)}</td>\n  </tr>`).join('\\n') : `\n  <tr>\n    <td colspan=\"4\" style=\"border:1px solid #e5e7eb; padding:10px; text-align:center; color:#6b7280;\">Sin datos en el periodo</td>\n  </tr>`;\n\nconst html = `\n<div style=\"font-family: Arial, sans-serif; color: #111827; padding: 24px; background:#f9fafb;\">\n  <div style=\"max-width:680px; margin:0 auto; background:#ffffff; border:1px solid #e5e7eb; border-radius:12px; overflow:hidden;\">\n    <div style=\"background:#111827; color:#ffffff; padding:18px 24px;\">\n      <h2 style=\"margin:0; font-size:20px;\">Resumen de ventas</h2>\n      <div style=\"font-size:13px; opacity:.85; margin-top:4px;\">Desde ${esc(periodoDesde)} hasta ${esc(periodoHasta)}</div>\n    </div>\n\n    <div style=\"padding:24px;\">\n      <div style=\"display:flex; gap:16px; flex-wrap:wrap;\">\n        <div style=\"flex:1 1 220px; border:1px solid #e5e7eb; border-radius:10px; padding:14px;\">\n          <div style=\"font-size:12px; color:#6b7280;\">Ingresos totales</div>\n          <div style=\"font-size:20px; font-weight:700;\">${money(r.ingresoTotal)}</div>\n        </div>\n        <div style=\"flex:1 1 220px; border:1px solid #e5e7eb; border-radius:10px; padding:14px;\">\n          <div style=\"font-size:12px; color:#6b7280;\">N\u00ba de ventas</div>\n          <div style=\"font-size:20px; font-weight:700;\">${r.numVentas ?? 0}</div>\n        </div>\n        <div style=\"flex:1 1 220px; border:1px solid #e5e7eb; border-radius:10px; padding:14px;\">\n          <div style=\"font-size:12px; color:#6b7280;\">Unidades vendidas</div>\n          <div style=\"font-size:20px; font-weight:700;\">${r.unidades ?? 0}</div>\n        </div>\n        <div style=\"flex:1 1 220px; border:1px solid #e5e7eb; border-radius:10px; padding:14px;\">\n          <div style=\"font-size:12px; color:#6b7280;\">Ticket medio</div>\n          <div style=\"font-size:20px; font-weight:700;\">${money(r.ticketMedio)}</div>\n        </div>\n      </div>\n\n      <h3 style=\"margin:24px 0 8px 0; font-size:16px;\">Top productos</h3>\n      <table style=\"border-collapse: collapse; width: 100%; font-size:14px;\">\n        <thead>\n          <tr style=\"background:#f3f4f6;\">\n            <th style=\"border:1px solid #e5e7eb; padding:10px; text-align:center; width:50px;\">#</th>\n            <th style=\"border:1px solid #e5e7eb; padding:10px; text-align:left;\">Producto</th>\n            <th style=\"border:1px solid #e5e7eb; padding:10px; text-align:right; width:120px;\">Unidades</th>\n            <th style=\"border:1px solid #e5e7eb; padding:10px; text-align:right; width:140px;\">Ingresos</th>\n          </tr>\n        </thead>\n        <tbody>\n          ${rows}\n        </tbody>\n      </table>\n\n      <h3 style=\"margin:24px 0 8px 0; font-size:16px;\">Top cliente</h3>\n      <div style=\"border:1px solid #e5e7eb; border-radius:10px; padding:14px; font-size:14px;\">\n        <div><strong>Nombre:</strong> ${esc(tc.nombre)}</div>\n        <div><strong>Gasto:</strong> ${money(tc.gasto)}</div>\n      </div>\n\n      <div style=\"color:#6b7280; font-size:12px; margin-top:24px;\">Este informe ha sido generado autom\u00e1ticamente por n8n.</div>\n    </div>\n  </div>\n</div>`;\n\nreturn [{ json: { html, toEmail: d.toEmail, subject: d.subject, periodo: d.periodo, resumen: d.resumen, topProductos: d.topProductos, topCliente: d.topCliente } }];"
      },
      "id": "7bee6946-de09-4375-9fc3-3c47f14ebf52",
      "name": "Construir HTML (CSS inline)",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        768,
        -208
      ]
    },
    {
      "parameters": {
        "fromEmail": "Ventas <rubencereceda23@gmail.com>",
        "toEmail": "=rubencereceda23@gmail.com",
        "subject": "=Ventas Semanales",
        "emailFormat": "html",
        "html": "={{ $json.html }}",
        "options": {}
      },
      "id": "dd199ffc-f3bb-4c41-bd1f-14f57d266209",
      "name": "Enviar Email (SMTP)",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2,
      "position": [
        960,
        -208
      ],
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "359585d7-7c36-4bb8-a433-9a11137522e5",
      "name": "Responder",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        1168,
        -208
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Preparar par\u00e1metros",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Preparar par\u00e1metros": {
      "main": [
        [
          {
            "node": "Obtener ventas (todas)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge ventas + params",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Obtener ventas (todas)": {
      "main": [
        [
          {
            "node": "Merge ventas + params",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge ventas + params": {
      "main": [
        [
          {
            "node": "Filtrar + Agregar (por periodo)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filtrar + Agregar (por periodo)": {
      "main": [
        [
          {
            "node": "Construir HTML (CSS inline)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Construir HTML (CSS inline)": {
      "main": [
        [
          {
            "node": "Enviar Email (SMTP)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enviar Email (SMTP)": {
      "main": [
        [
          {
            "node": "Responder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "meta": {
    "templateCredsSetupCompleted": true
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Resumen Ventas. Uses httpRequest, emailSend. Webhook trigger; 8 nodes.

Source: https://github.com/Rruubeenn23/Vapers/blob/53d90f273b2d0172d72c0351e38e6199d0a65d27/n8n/resumen_ventas.json — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

세미나 데모 용 워크플로우. Uses httpRequest, emailSend. Webhook trigger; 17 nodes.

HTTP Request, Email Send
Web Scraping

worklow_doc. Uses httpRequest, readBinaryFile, n8n-nodes-docxtemplater, emailSend. Webhook trigger; 15 nodes.

HTTP Request, Read Binary File, N8N Nodes Docxtemplater +1
Web Scraping

WF2 - Upload Manual | JurisAI. Uses httpRequest, emailSend. Webhook trigger; 15 nodes.

HTTP Request, Email Send
Web Scraping

Deliver personalized files instantly after PayPal transactions using n8n – without writing a single backend line.

HTTP Request, Email Send
Web Scraping

This workflow automates real-time student tracking using iOS Shortcuts and geolocation data, notifying both teachers and parents based on geofenced logic.

HTTP Request, Email Send