AutomationFlowsFinance › Invoice to Ledger

Invoice to Ledger

Invoice to Ledger. Uses telegramTrigger, telegram, googleSheets, httpRequest. Event-driven trigger; 21 nodes.

Event trigger★★★★☆ complexity21 nodesTelegram TriggerTelegramGoogle SheetsHTTP Request
Finance Trigger: Event Nodes: 21 Complexity: ★★★★☆ Added:

This workflow follows the Google Sheets → 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
{
  "name": "Invoice to Ledger",
  "nodes": [
    {
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      },
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.3,
      "position": [
        -1504,
        -464
      ],
      "name": "Telegram Trigger",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "file",
        "fileId": "={{ $json.message.photo[$json.message.photo.length - 1].file_id }}",
        "additionalFields": {}
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -1376,
        400
      ],
      "name": "Download photo",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "YOUR_SPREADSHEET_ID",
          "mode": "list",
          "cachedResultName": "Ledger_Template",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $json.nama_pihak }}",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "TANGGAL": "={{ $json.TANGGAL }}",
            "NO SURAT": "={{ $json['NO SURAT'] }}",
            "NAMA BARANG": "={{ $json['NAMA BARANG'] }}",
            "JUMLAH": "={{ $json.JUMLAH }}",
            "SATUAN": "={{ $json.SATUAN }}",
            "HARGA / SATUAN": "={{ $json['HARGA / SATUAN'] }}",
            "KREDIT": "={{ $json.KREDIT }}",
            "DEBET": "={{ $json.DEBET }}",
            "TOTAL": "={{ $json.TOTAL }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "TANGGAL",
              "displayName": "TANGGAL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "NO SURAT",
              "displayName": "NO SURAT",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "NAMA BARANG",
              "displayName": "NAMA BARANG",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "JUMLAH",
              "displayName": "JUMLAH",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "SATUAN",
              "displayName": "SATUAN",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "HARGA / SATUAN",
              "displayName": "HARGA / SATUAN",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "KREDIT",
              "displayName": "KREDIT",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "DEBET",
              "displayName": "DEBET",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "TOTAL",
              "displayName": "TOTAL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "nama_pihak",
              "displayName": "nama_pihak",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -320,
        320
      ],
      "name": "Append to party tab",
      "retryOnFail": true,
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const buffer = await this.helpers.getBinaryDataBuffer($itemIndex, 'data');\n\nreturn {\n  json: {\n    image_base64: buffer.toString('base64'),\n    mime_type: $input.item.binary.data.mimeType\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1232,
        -464
      ],
      "name": "Encode image to base64"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"claude-opus-4-8\",\n  \"max_tokens\": 2000,\n  \"system\": \"Kamu adalah asisten admin yang membaca foto nota untuk perusahaan percetakan NORTHGATE PRINT WORKS (kode internal: NPW), berlokasi di Tangerang. Pemilik: the owner.\\n\\nBaca foto nota yang dilampirkan, lalu ekstrak informasi berikut dalam format JSON:\\n\\n{\\n  \\\"jenis\\\": \\\"HUTANG\\\" atau \\\"PIUTANG\\\",\\n  \\\"nama_pihak\\\": \\\"nama pihak lawan transaksi, atau null\\\",\\n  \\\"tanggal\\\": \\\"YYYY-MM-DD\\\",\\n  \\\"no_surat\\\": \\\"nomor nota persis seperti tertulis\\\",\\n  \\\"items\\\": [\\n    {\\n      \\\"nama_barang\\\": \\\"...\\\",\\n      \\\"jumlah\\\": angka,\\n      \\\"satuan\\\": \\\"lbr/pcs/set/kg/dll\\\",\\n      \\\"harga_satuan\\\": angka atau null,\\n      \\\"subtotal\\\": angka\\n    }\\n  ],\\n  \\\"total_nota\\\": angka,\\n  \\\"perlu_foto_ulang\\\": true atau false,\\n  \\\"alasan_kurang_jelas\\\": \\\"sebutkan field mana yang tidak terbaca, kosongkan jika lengkap\\\"\\n}\\n\\n=== ATURAN JENIS (HUTANG vs PIUTANG) ===\\nLihat segmen kedua nomor nota (dipisah tanda /).\\n- Mengandung \\\"NPW\\\" (termasuk varian \\\"NPW-P\\\") -> PIUTANG. Kami yang menerbitkan nota ke customer.\\n- Selain itu -> HUTANG. Pihak lain yang menerbitkan nota kepada kami.\\nContoh: 060032/NPW-P/VI/2026 -> PIUTANG. 060001/PPS/VI/2026 -> HUTANG.\\n\\n=== ATURAN nama_pihak ===\\nIni nama LAWAN TRANSAKSI, bukan nama kami sendiri.\\n- PIUTANG: ambil nama customer. Biasanya di kotak kanan atas nota. Bisa berupa nama perusahaan maupun nama orang.\\n- HUTANG: ambil nama penerbit nota, biasanya dari field \\\"Dari:\\\" atau kop surat penerbit.\\n\\nPENTING:\\n- NORTHGATE PRINT WORKS / NORTHGATE / NPW adalah NAMA KAMI SENDIRI. Jangan pernah dipakai sebagai nama_pihak.\\n- Pada nota HUTANG, nama kami sering muncul di kotak kanan atas sebagai penerima. Abaikan, itu bukan nama_pihak.\\n- Kalau nama lawan transaksi TIDAK TERLIHAT jelas di foto, isi null. JANGAN menebak dari nama lain yang kebetulan ada di kertas.\\n- nama_pihak bernilai null TIDAK memicu perlu_foto_ulang, karena sistem punya cadangan dari kode nota.\\n\\n=== ATURAN no_surat ===\\nFormat: 6DIGIT/KODE/ROMAWI/TAHUN. Contoh: 060043/NPW-P/VI/2026.\\n- Segmen pertama SELALU 6 digit angka, tidak pernah ada huruf.\\n- 2 digit PERTAMA dari segmen pertama adalah nomor bulan, dan HARUS sama dengan angka romawi di segmen ketiga. Contoh: romawi VI berarti 2 digit awal adalah 06. Romawi V berarti 05.\\n- Pakai aturan ini untuk mengoreksi huruf yang salah terbaca. Kalau romawi VI tapi digit awal terbaca 0S atau 05, yang benar adalah 06.\\n- Sebelum menjawab, cek ulang: apakah 2 digit awal sudah cocok dengan angka romawi? Kalau belum, perbaiki.\\n\\n=== ATURAN tanggal ===\\n- Ambil dari field tanggal di kiri atas nota, format asli DD/MM/YYYY.\\n- Keluarkan sebagai YYYY-MM-DD.\\n- JANGAN mengambil bulan dari nomor nota. Angka romawi di nomor nota sering tidak sinkron dengan tanggal sebenarnya.\\n\\n=== ATURAN ANGKA (PENTING) ===\\nNota memakai format angka Indonesia: TITIK = pemisah ribuan, KOMA = desimal.\\n- \\\"30.200\\\" berarti tiga puluh ribu dua ratus, BUKAN 30,2.\\n- \\\"5.250\\\" berarti lima ribu dua ratus lima puluh, BUKAN 5,25.\\n- \\\"145,08\\\" berarti seratus empat puluh lima koma nol delapan.\\nKeluarkan semua angka sebagai JSON polos tanpa pemisah ribuan: 30200, 5250, 145.08.\\n\\nVERIFIKASI WAJIB sebelum menjawab:\\n- Untuk SETIAP item, hitung jumlah x harga_satuan. Hasilnya HARUS sama dengan subtotal.\\n- Kalau tidak sama, berarti ada angka yang salah dibaca. Baca ulang kolom Banyaknya dan Harga untuk item itu, lalu perbaiki.\\n- Contoh: kalau subtotal 250.000 dan harga 50, maka jumlah pasti 5.000 (bukan 3.000).\\n- Jumlah semua subtotal harus sama dengan total_nota.\\n\\n=== ATURAN items ===\\n- Satu baris di tabel nota = satu objek di array items.\\n- nama_barang: salin persis, PERTAHANKAN tanda * atau x pada ukuran. Contoh \\\"31*19,5 cm\\\" jangan jadi \\\"3119,5 cm\\\". Pertahankan juga rasio seperti \\\"1:1\\\" atau \\\"1:4\\\".\\n- harga_satuan: kalau kolom harga tertulis \\\"minimum\\\" atau kosong, isi null (BUKAN 0). Ini normal dan TIDAK memicu perlu_foto_ulang selama subtotal terbaca.\\n- subtotal: ambil dari kolom Jumlah di nota, jangan dihitung sendiri.\\n- Kalau ada keterangan kuantitas seperti \\\"ket : 10 pak x 3.000 pcs + 200 pcs\\\", pakai itu untuk memastikan angka di kolom Banyaknya sudah benar.\\n\\n=== KONDISI FOTO ===\\n- Foto mungkin terbalik 180 derajat atau miring. Tetap baca dengan benar sesuai orientasi tulisan.\\n- Nota sering ditumpuk. Teks samar yang tembus dari lembar di bawahnya HARUS DIABAIKAN, jangan dimasukkan sebagai item.\\n\\n=== KAPAN perlu_foto_ulang = true ===\\nSet true HANYA jika salah satu dari ini tidak terbaca:\\n- tanggal\\n- no_surat\\n- total_nota\\n- minimal 1 item dengan nama_barang, jumlah, dan subtotal terisi\\n\\nJangan set true hanya karena nama_pihak null atau harga_satuan null.\\n\\nJangan mengarang angka. Kalau ragu antara dua angka yang mirip pada field wajib, isi null dan set perlu_foto_ulang true dengan alasan yang spesifik menyebut field dan item mana.\\n\\nPENTING: Balas HANYA dengan JSON di atas, tanpa teks pembuka, penutup, atau markdown code fence.\",\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": [\n        {\n          \"type\": \"image\",\n          \"source\": {\n            \"type\": \"base64\",\n            \"media_type\": \"{{ $json.mime_type }}\",\n            \"data\": \"{{ $json.image_base64 }}\"\n          }\n        },\n        {\n          \"type\": \"text\",\n          \"text\": \"Baca foto nota ini dan ekstrak datanya sesuai format JSON yang sudah ditentukan di system prompt.\"\n        }\n      ]\n    }\n  ]\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -1040,
        400
      ],
      "name": "Claude Vision - extract invoice",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// ====== KONFIGURASI ======\nconst SELF = ['NORTHGATE PRINT WORKS', 'NORTHGATE', 'NPW'];\n\nconst KODE_PIHAK = {\n  // 'PPS': 'HARAPAN GEMILANG',\n};\n\nfunction norm(s) {\n  return String(s || '')\n    .toUpperCase()\n    .replace(/[:\\\\\\/\\?\\*\\[\\]]/g, ' ')\n    .replace(/\\b(PT|CV|UD|TB)\\b\\.?/g, '')\n    .replace(/\\s+/g, ' ')\n    .trim()\n    .slice(0, 90);\n}\n\n// ====== PARSE ======\nlet teks = $input.item.json.content[0].text;\nteks = teks.replace(/```json/g, '').replace(/```/g, '').trim();\nconst d = JSON.parse(teks);\n\n// ====== JENIS & NAMA PIHAK ======\nconst noSurat = String(d.no_surat || '').toUpperCase();\nconst seg = noSurat.split('/');\nconst kode = (seg[1] || '').trim();\nconst jenis = kode.includes('NPW') ? 'PIUTANG' : 'HUTANG';\n\nlet pihak = norm(d.nama_pihak);\nlet pakaiKode = false;\n\nif (!pihak || SELF.includes(pihak)) {\n  pihak = KODE_PIHAK[kode] || kode;\n  pakaiKode = !KODE_PIHAK[kode] && !!kode;\n}\n\n// ====== PERINGATAN ======\nconst peringatan = [];\n\n// 1. cek aritmatika tiap item\n(d.items || []).forEach((it, i) => {\n  if (it.harga_satuan === null || it.harga_satuan === undefined) return;\n  const j = Number(it.jumlah);\n  const h = Number(it.harga_satuan);\n  const s = Number(it.subtotal);\n  if (!isFinite(j) || !isFinite(h) || !isFinite(s)) return;\n\n  const hitung = j * h;\n  const toleransi = Math.max(5, s * 0.01);\n  if (Math.abs(hitung - s) > toleransi) {\n    peringatan.push('Item ' + (i + 1) + ': ' + j + ' x ' + h + ' = ' + hitung\n      + ', tapi nota tulis ' + s + ' (jumlah/harga kemungkinan salah baca)');\n  }\n});\n\n// 2. cek bulan di nomor nota vs angka romawi\nconst ROMAWI = { I:1, II:2, III:3, IV:4, V:5, VI:6, VII:7, VIII:8, IX:9, X:10, XI:11, XII:12 };\nconst bulanRomawi = ROMAWI[(seg[2] || '').trim()];\nconst bulanDepan = parseInt(String(seg[0] || '').slice(0, 2), 10);\nif (bulanRomawi && !isNaN(bulanDepan) && bulanRomawi !== bulanDepan) {\n  peringatan.push('No nota: 2 digit awal (' + String(seg[0]).slice(0, 2)\n    + ') tidak cocok dengan bulan ' + seg[2] + ' (' + bulanRomawi + ')');\n}\n\n// 3. cek jumlah subtotal vs total nota\nconst jmlSub = (d.items || []).reduce((a, it) => a + Number(it.subtotal || 0), 0);\nif (d.total_nota != null && jmlSub !== Number(d.total_nota)) {\n  peringatan.push('Jumlah subtotal ' + jmlSub + ' beda dgn total nota ' + d.total_nota);\n}\n\n// ====== KAPAN MINTA FOTO ULANG ======\nlet perluUlang = d.perlu_foto_ulang === true;\nlet alasan = d.alasan_kurang_jelas || 'Foto kurang jelas';\n\nif (!perluUlang && !pihak) {\n  perluUlang = true;\n  alasan = 'Nama pihak dan kode nota tidak terbaca';\n}\nif (!perluUlang && (!Array.isArray(d.items) || d.items.length === 0)) {\n  perluUlang = true;\n  alasan = 'Item nota tidak terbaca';\n}\n\n// ====== TEMPEL METADATA KE TIAP ITEM ======\nconst items = (d.items || []).map(it => ({\n  nama_barang: it.nama_barang || '',\n  jumlah: it.jumlah ?? null,\n  satuan: it.satuan || '',\n  harga_satuan: it.harga_satuan ?? null,\n  subtotal: it.subtotal ?? null,\n  tanggal: d.tanggal || '',\n  no_surat: d.no_surat || '',\n  nama_pihak: pihak,\n  jenis: jenis,\n}));\n\nreturn {\n  json: {\n    nama_pihak: pihak,\n    jenis: jenis,\n    kode: kode,\n    pakai_kode: pakaiKode,\n    no_surat: d.no_surat || '',\n    tanggal: d.tanggal || '',\n    total_nota: d.total_nota ?? null,\n    perlu_foto_ulang: perluUlang,\n    alasan: alasan,\n    peringatan: peringatan,\n    items: items,\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -896,
        -448
      ],
      "name": "Parse, classify & validate"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "c73d2cb5-d731-4f46-89c5-99ca299c97b4",
              "leftValue": "={{ $json.perlu_foto_ulang }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -832,
        384
      ],
      "name": "Photo readable?"
    },
    {
      "parameters": {
        "chatId": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
        "text": "={{\n(() => {\n  const d = $json;\n  const bersih = (t) => String(t ?? '').replace(/\\*/g, 'x').replace(/[_`\\[\\]]/g, ' ');\n\n  return 'Foto kurang jelas, tolong foto ulang ya.\\n\\n'\n    + 'Alasan: ' + bersih(d.alasan || 'tidak disebutkan') + '\\n\\n'\n    + (d.no_surat ? 'No. Nota terbaca: ' + bersih(d.no_surat) + '\\n\\n' : '')\n    + 'Tips: nota jangan ketutup bayangan, posisi tegak (jangan kebalik), seluruh tabel masuk frame.';\n})()\n}}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -432,
        -656
      ],
      "name": "Reply: retake photo",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "fieldToSplitOut": "items",
        "include": "allOtherFields",
        "options": {}
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        80,
        -208
      ],
      "name": "Split line items"
    },
    {
      "parameters": {
        "amount": 1
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        256,
        -208
      ],
      "name": "Throttle writes"
    },
    {
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        16,
        320
      ],
      "name": "Aggregate line items"
    },
    {
      "parameters": {
        "chatId": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
        "text": "={{\n(() => {\n  const d = $('Parse, classify & validate').first().json;\n  const bersih = (t) => String(t ?? '').replace(/\\*/g, 'x').replace(/[_`\\[\\]]/g, ' ');\n  const rp = (n) => (n === null || n === undefined || n === '') ? '-' : 'Rp' + Number(n).toLocaleString('id-ID');\n  const angka = (n) => Number(n || 0).toLocaleString('id-ID');\n\n  const items = d.items || [];\n  const peringatan = d.peringatan || [];\n\n  const baris = items.map((it, i) =>\n    (i + 1) + '. ' + bersih(it.nama_barang)\n    + ' - ' + angka(it.jumlah) + ' ' + bersih(it.satuan)\n    + ' @ ' + rp(it.harga_satuan)\n    + ' = ' + rp(it.subtotal)\n  ).join('\\n');\n\n  const jml = items.reduce((a, it) => a + Number(it.subtotal || 0), 0);\n  const cocok = d.total_nota != null && jml === Number(d.total_nota);\n\n  const blokWarn = peringatan.length\n    ? '\\n\\nPERLU DICEK:\\n' + peringatan.map(p => '- ' + bersih(p)).join('\\n')\n    : '';\n\n  return 'Tercatat ke Sheets\\n\\n'\n    + 'Jenis: ' + d.jenis + '\\n'\n    + 'Pihak: ' + bersih(d.nama_pihak) + (d.pakai_kode ? '  (kode - nama belum diset)' : '') + '\\n'\n    + 'No. Nota: ' + bersih(d.no_surat) + '\\n'\n    + 'Tanggal: ' + d.tanggal + '\\n\\n'\n    + 'Item:\\n' + baris + '\\n\\n'\n    + 'Total: ' + rp(jml)\n    + (cocok ? '  (cocok dgn nota)' : '  (BEDA dgn nota: ' + rp(d.total_nota) + ')')\n    + blokWarn + '\\n\\n'\n    + 'Cek excel kalo perlu koreksi ya.';\n})()\n}}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        256,
        320
      ],
      "name": "Reply: confirmation",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "create",
        "documentId": {
          "__rl": true,
          "value": "=YOUR_SPREADSHEET_ID",
          "mode": "id"
        },
        "title": "={{ $json.nama_pihak }}",
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -448,
        -224
      ],
      "name": "Create party tab",
      "alwaysOutputData": true,
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "b2b9b80b-7722-4ccd-82b9-f33a991086fc",
              "name": "TANGGAL",
              "value": "={{ $json.tanggal }}",
              "type": "string"
            },
            {
              "id": "8d0234b2-ea07-4dfa-9b0a-82992265d2a5",
              "name": "NO SURAT",
              "value": "={{ $json.no_surat }}",
              "type": "string"
            },
            {
              "id": "24ab5273-d480-4d50-b545-024f00d3d98f",
              "name": "NAMA BARANG",
              "value": "={{ $json.items.nama_barang }}",
              "type": "string"
            },
            {
              "id": "7f0d32bd-ef82-4c24-9eab-a092667f2176",
              "name": "JUMLAH",
              "value": "={{ $json.items.jumlah }}",
              "type": "string"
            },
            {
              "id": "1f203fd4-4953-42f6-9018-fd7e08b8e803",
              "name": "SATUAN",
              "value": "={{ $json.items.satuan }}",
              "type": "string"
            },
            {
              "id": "dabfeaaf-fefb-4fcb-911b-1c16454d55d9",
              "name": "HARGA / SATUAN",
              "value": "={{ $json.items.harga_satuan }}",
              "type": "string"
            },
            {
              "id": "e5e561da-602d-4c41-85f2-0a967793b878",
              "name": "KREDIT",
              "value": "={{ $json.items.subtotal }}",
              "type": "string"
            },
            {
              "id": "05548dbd-ccb6-4d33-8c8a-8210d860374b",
              "name": "DEBET",
              "value": "",
              "type": "string"
            },
            {
              "id": "942507b6-1671-48a8-a0d6-ed8151b73e0d",
              "name": "TOTAL",
              "value": "={{ $json.items.subtotal }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "include": "selected",
        "includeFields": "nama_pihak, jenis",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        464,
        -208
      ],
      "name": "Map to ledger columns"
    },
    {
      "parameters": {
        "jsCode": "return $('Parse, classify & validate').all();"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -112,
        -208
      ],
      "name": "Restore invoice data"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/YOUR_SPREADSHEET_ID/values/{{ encodeURIComponent($('Parse, classify & validate').item.json.nama_pihak) }}!A1:I1?valueInputOption=RAW",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"values\": [\n    [\"TANGGAL\", \"NO SURAT\", \"NAMA BARANG\", \"JUMLAH\", \"SATUAN\", \"HARGA / SATUAN\", \"KREDIT\", \"DEBET\", \"TOTAL\"]\n  ]\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -288,
        -448
      ],
      "name": "Write header (idempotent)",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "YOUR_SPREADSHEET_ID",
          "mode": "list",
          "cachedResultName": "Ledger_Template",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "_MASTER",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "TANGGAL": "={{ $('Map to ledger columns').item.json.TANGGAL }}",
            "JENIS": "={{ $('Map to ledger columns').item.json.jenis }}",
            "NAMA PIHAK": "={{ $('Map to ledger columns').item.json.nama_pihak }}",
            "NO SURAT": "={{ $('Map to ledger columns').item.json['NO SURAT'] }}",
            "NAMA BARANG": "={{ $('Map to ledger columns').item.json['NAMA BARANG'] }}",
            "JUMLAH": "={{ $('Map to ledger columns').item.json.JUMLAH }}",
            "SATUAN": "={{ $('Map to ledger columns').item.json.SATUAN }}",
            "HARGA / SATUAN": "={{ $('Map to ledger columns').item.json['HARGA / SATUAN'] }}",
            "KREDIT": "={{ $('Map to ledger columns').item.json.KREDIT }}",
            "DEBET": "={{ $('Map to ledger columns').item.json.DEBET }}",
            "TOTAL": "={{ $('Map to ledger columns').item.json.TOTAL }}",
            "WAKTU INPUT": "={{ $now.setZone('Asia/Jakarta').toFormat('yyyy-MM-dd HH:mm') }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "TANGGAL",
              "displayName": "TANGGAL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "JENIS",
              "displayName": "JENIS",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "NAMA PIHAK",
              "displayName": "NAMA PIHAK",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "NO SURAT",
              "displayName": "NO SURAT",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "NAMA BARANG",
              "displayName": "NAMA BARANG",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "JUMLAH",
              "displayName": "JUMLAH",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "SATUAN",
              "displayName": "SATUAN",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "HARGA / SATUAN",
              "displayName": "HARGA / SATUAN",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "KREDIT",
              "displayName": "KREDIT",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "DEBET",
              "displayName": "DEBET",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "TOTAL",
              "displayName": "TOTAL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "WAKTU INPUT",
              "displayName": "WAKTU INPUT",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -144,
        320
      ],
      "name": "Append to _MASTER",
      "retryOnFail": true,
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "YOUR_SPREADSHEET_ID",
          "mode": "list",
          "cachedResultName": "Ledger_Template",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "_MASTER",
          "mode": "name"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -800,
        800
      ],
      "name": "Read _MASTER ledger",
      "alwaysOutputData": true,
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const data = $('Parse, classify & validate').first().json;\nconst noSurat = String(data.no_surat || '').trim().toUpperCase();\n\n// kumpulin semua nomor nota yang udah ada di _MASTER\nconst sudahAda = new Set();\nfor (const item of $input.all()) {\n  const n = String(item.json['NO SURAT'] || '').trim().toUpperCase();\n  if (n) sudahAda.add(n);\n}\n\nconst duplikat = noSurat ? sudahAda.has(noSurat) : false;\n\nreturn [{\n  json: {\n    ...data,\n    duplikat: duplikat,\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -640,
        800
      ],
      "name": "Check invoice no."
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "1830d746-b83f-4fa2-ba48-0af41641af80",
              "leftValue": "={{ $json.duplikat }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -496,
        800
      ],
      "name": "Already recorded?"
    },
    {
      "parameters": {
        "chatId": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
        "text": "={{\n(() => {\n  const d = $('Check invoice no.').first().json;\n  const bersih = (t) => String(t ?? '').replace(/\\*/g, 'x').replace(/[_`\\[\\]]/g, ' ');\n  return bersih('Nota ini SUDAH PERNAH dicatat, jadi tidak ditulis ulang.\\n\\n'\n    + 'No. Nota: ' + d.no_surat + '\\n'\n    + 'Pihak: ' + d.nama_pihak + '\\n\\n'\n    + 'Kalau ini memang nota berbeda, cek nomornya di Sheets tab _MASTER.');\n})()\n}}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -256,
        576
      ],
      "name": "Reply: duplicate",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Telegram Trigger": {
      "main": [
        [
          {
            "node": "Download photo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download photo": {
      "main": [
        [
          {
            "node": "Encode image to base64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Encode image to base64": {
      "main": [
        [
          {
            "node": "Claude Vision - extract invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Vision - extract invoice": {
      "main": [
        [
          {
            "node": "Parse, classify & validate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse, classify & validate": {
      "main": [
        [
          {
            "node": "Photo readable?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Photo readable?": {
      "main": [
        [
          {
            "node": "Reply: retake photo",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Read _MASTER ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reply: retake photo": {
      "main": [
        []
      ]
    },
    "Split line items": {
      "main": [
        [
          {
            "node": "Throttle writes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Throttle writes": {
      "main": [
        [
          {
            "node": "Map to ledger columns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append to party tab": {
      "main": [
        [
          {
            "node": "Append to _MASTER",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate line items": {
      "main": [
        [
          {
            "node": "Reply: confirmation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create party tab": {
      "main": [
        [
          {
            "node": "Write header (idempotent)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Restore invoice data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map to ledger columns": {
      "main": [
        [
          {
            "node": "Append to party tab",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore invoice data": {
      "main": [
        [
          {
            "node": "Split line items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write header (idempotent)": {
      "main": [
        [
          {
            "node": "Restore invoice data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append to _MASTER": {
      "main": [
        [
          {
            "node": "Aggregate line items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read _MASTER ledger": {
      "main": [
        [
          {
            "node": "Check invoice no.",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check invoice no.": {
      "main": [
        [
          {
            "node": "Already recorded?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Already recorded?": {
      "main": [
        [
          {
            "node": "Reply: duplicate",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create party tab",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "tags": []
}

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

Invoice to Ledger. Uses telegramTrigger, telegram, googleSheets, httpRequest. Event-driven trigger; 21 nodes.

Source: https://github.com/konstaraprawiro/n8n-invoice-to-ledger/blob/main/workflow/workflow.json — original creator credit. Request a take-down →

More Finance workflows → · Browse all categories →

Related workflows

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

Finance

How It Works Trigger: Watches for new emails in Gmail with PDF/image attachments. OCR: Sends the attachment to OCR.space API (https://ocr.space/OCRAPI) to extract invoice text. Parsing: Extracts key f

Gmail Trigger, Google Sheets, Slack +3
Finance

Invoice PDF to Sheets. Uses googleDriveTrigger, googleDrive, httpRequest, googleSheets. Event-driven trigger; 10 nodes.

Google Drive Trigger, Google Drive, HTTP Request +1
Finance

This workflow is a sophisticated, end-to-end solution that automates the entire billing lifecycle, from invoice creation to intelligent payment reminders and status tracking. It's designed to give you

Google Sheets Trigger, Google Docs, OpenAI +4
Finance

This is the ultimate sales-to-cash automation. When a deal in Airtable is marked "Approved for Invoicing," this workflow intelligently syncs customer data across QuickBooks and Stripe (creating them i

Airtable Trigger, Stripe, QuickBooks +2
Finance

This workflow triggers on successful Stripe payment events, generates and finalizes invoices, then formats and sends invoice details via Gmail, stores invoice PDFs in Google Drive, and notifies an adm

Stripe, Gmail, HTTP Request +3