
Track Expenses from Receipt Photos with Telegram & Google Sheets using OCR.space
説明
Categories
🤖 AI & Machine Learning
Nodes Used
n8n-nodes-base.ifn8n-nodes-base.ifn8n-nodes-base.ifn8n-nodes-base.functionn8n-nodes-base.httpRequestn8n-nodes-base.telegramBotn8n-nodes-base.telegramBotn8n-nodes-base.telegramBotn8n-nodes-base.telegramBotn8n-nodes-base.telegramBot
Price無料
Views0
最終更新11/28/2025
workflow.json
{
"nodes": [
{
"name": "Telegram Bot (Webhook)",
"type": "n8n-nodes-base.telegramBot",
"position": [
250,
200
],
"parameters": {
"webhookUrl": "=https://api.telegram.org/bot{{$env.YOUR_TELEGRAM_BOT_TOKEN}}/getUpdates",
"pollInterval": 5,
"allowedUpdates": [
"message"
],
"onlyNewUpdates": true
},
"typeVersion": 1
},
{
"name": "Check 'Income'",
"type": "n8n-nodes-base.if",
"position": [
450,
100
],
"parameters": {
"conditions": [
{
"value1": "={{$json.message.text}}",
"value2": "income",
"operation": "contains"
}
]
},
"typeVersion": 1
},
{
"name": "Check 'Expense' (Text)",
"type": "n8n-nodes-base.if",
"position": [
450,
300
],
"parameters": {
"conditions": [
{
"value1": "={{$json.message.text}}",
"value2": "expense",
"operation": "contains"
}
]
},
"typeVersion": 1
},
{
"name": "Add Income to Google Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
700,
100
],
"parameters": {
"operation": "appendRow",
"sheetName": "Income",
"columnValues": {
"mapping": {
"Date": "={{$today}}",
"Type": "Income",
"Amount": "={{$json.message.text.split(' ')[2]}}",
"Source": "Manual",
"Description": "={{$json.message.text.split(' ')[1]}}"
}
},
"spreadsheetId": "={{$env.YOUR_GOOGLE_SHEET_ID}}",
"authentication": "oAuth2",
"valueInputOption": "USER_ENTERED"
},
"typeVersion": 1
},
{
"name": "Add Expense to Google Sheet (Text)",
"type": "n8n-nodes-base.googleSheets",
"position": [
700,
300
],
"parameters": {
"operation": "appendRow",
"sheetName": "Expenses",
"columnValues": {
"mapping": {
"Date": "={{$today}}",
"Type": "Expense",
"Amount": "={{$json.message.text.split(' ')[2]}}",
"Source": "Manual",
"Category": "={{$json.message.text.split(' ')[3]}}",
"Description": "={{$json.message.text.split(' ')[1]}}"
}
},
"spreadsheetId": "={{$env.YOUR_GOOGLE_SHEET_ID}}",
"authentication": "oAuth2",
"valueInputOption": "USER_ENTERED"
},
"typeVersion": 1
},
{
"name": "Send Income Confirmation",
"type": "n8n-nodes-base.telegramBot",
"position": [
950,
100
],
"parameters": {
"text": "Income \"{{$json.message.text.split(' ')[1]}}\" of ${{parseFloat($json.message.text.split(' ')[2]).toLocaleString('en-US')}} successfully recorded!",
"chatId": "={{$json.message.chat.id}}",
"parseMode": "HTML"
},
"typeVersion": 1
},
{
"name": "Send Expense Confirmation (Text)",
"type": "n8n-nodes-base.telegramBot",
"position": [
950,
300
],
"parameters": {
"text": "Expense \"{{$json.message.text.split(' ')[1]}}\" of ${{parseFloat($json.message.text.split(' ')[2]).toLocaleString('en-US')}} for category \"{{$json.message.text.split(' ')[3]}}\" successfully recorded!",
"chatId": "={{$json.message.chat.id}}",
"parseMode": "HTML"
},
"typeVersion": 1
},
{
"name": "Send Error Message (Text)",
"type": "n8n-nodes-base.telegramBot",
"position": [
700,
700
],
"parameters": {
"text": "Sorry, message format not recognized. Use format:\n*income [description] [amount]*\nor\n*expense [description] [amount] [category]*\nAlternatively, send a photo of your receipt for automatic tracking!",
"chatId": "={{$json.message.chat.id}}",
"parseMode": "HTML"
},
"typeVersion": 1
},
{
"name": "Check 'Photo'",
"type": "n8n-nodes-base.if",
"position": [
450,
500
],
"parameters": {
"conditions": [
{
"value1": "={{$json.message.photo}}",
"value2": "true",
"operation": "isNotEmpty"
}
]
},
"typeVersion": 1
},
{
"name": "Get Telegram Photo",
"type": "n8n-nodes-base.telegramBot",
"position": [
700,
500
],
"parameters": {
"chatId": "={{$json.message.chat.id}}",
"fileId": "={{$json.message.photo[{$json.message.photo.length - 1}].file_id}}",
"operation": "getFile"
},
"typeVersion": 1
},
{
"name": "OCR.space Request",
"type": "n8n-nodes-base.httpRequest",
"position": [
950,
500
],
"parameters": {
"url": "https://api.ocr.space/parse/image",
"method": "POST",
"options": {
"query": [
{
"name": "apikey",
"value": "={{$env.YOUR_OCR_SPACE_API_KEY}}"
},
{
"name": "language",
"value": "eng"
},
{
"name": "isOverlayRequired",
"value": "true"
}
],
"bodyParameters": [
{
"name": "file",
"value": "={{$binary.data}}"
}
],
"bodyContentType": "formData"
}
},
"typeVersion": 1
},
{
"name": "Parse OCR Data",
"type": "n8n-nodes-base.function",
"position": [
1200,
500
],
"parameters": {
"function": "const ocrData = $json.ParsedResults[0].ParsedText;\n\n// Basic regex to find common patterns for amount ($) and dates (MM/DD/YYYY, YYYY-MM-DD, DD/MM/YYYY)\nconst amountRegex = /(?:[USD|SGD|MYR|\\$])?\\s*(\\d{1,3}(?:[.,]\\d{3})*(?:[.,]\\d{2})?)/i;\nconst dateRegex = /(\\d{1,2}[-/.]\\d{1,2}[-/.]\\d{2,4})/;\n\nlet description = 'OCR Expense';\nlet amount = 0;\nlet date = new Date().toISOString().slice(0, 10); // Default to today\nlet category = 'Uncategorized';\n\nconst amountMatch = ocrData.match(amountRegex);\nif (amountMatch && amountMatch[1]) {\n amount = parseFloat(amountMatch[1].replace(/[,.]/g, m => m === ',' ? '.' : ',')).toFixed(2); // Handle both comma and dot decimals\n}\n\nconst dateMatch = ocrData.match(dateRegex);\nif (dateMatch && dateMatch[1]) {\n // Attempt to parse date, try common formats\n const dateStr = dateMatch[1];\n let parsedDate = null;\n\n // Try YYYY-MM-DD\n if (dateStr.match(/^\\d{4}[-/.]\\d{1,2}[-/.]\\d{1,2}$/)) {\n parsedDate = new Date(dateStr);\n }\n // Try MM/DD/YYYY or DD/MM/YYYY (be careful with ambiguity)\n else if (dateStr.match(/^\\d{1,2}[-/.]\\d{1,2}[-/.]\\d{2,4}$/)) {\n // Assuming MM/DD/YYYY for simplicity, adjust if DD/MM/YYYY is more common for you\n const parts = dateStr.split(/[-/.]/);\n if (parts.length === 3) {\n parsedDate = new Date(`${parts[2]}-${parts[0]}-${parts[1]}`); // YYYY-MM-DD format for constructor\n }\n }\n\n if (parsedDate && !isNaN(parsedDate.getTime())) {\n date = parsedDate.toISOString().slice(0, 10);\n }\n}\n\n// Simple keyword-based categorization (you'd make this more robust)\nif (ocrData.toLowerCase().includes('food') || ocrData.toLowerCase().includes('restaurant') || ocrData.toLowerCase().includes('cafe')) {\n category = 'Food & Drinks';\n} else if (ocrData.toLowerCase().includes('transport') || ocrData.toLowerCase().includes('uber') || ocrData.toLowerCase().includes('taxi')) {\n category = 'Transportation';\n} else if (ocrData.toLowerCase().includes('shopping') || ocrData.toLowerCase().includes('store')) {\n category = 'Shopping';\n} else if (ocrData.toLowerCase().includes('utility') || ocrData.toLowerCase().includes('electric') || ocrData.toLowerCase().includes('water')) {\n category = 'Utilities';\n}\n\n// Try to get a simple description from the text (e.g., first few words of the text)\nconst lines = ocrData.split('\\n').filter(line => line.trim() !== '');\nif (lines.length > 0) {\n description = lines[0].substring(0, Math.min(lines[0].length, 50)); // Take first 50 chars of the first non-empty line\n}\n\nreturn [\n {\n json: {\n description: description,\n amount: amount,\n date: date,\n category: category\n }\n }\n];"
},
"typeVersion": 1
},
{
"name": "Add Expense to Google Sheet (OCR)",
"type": "n8n-nodes-base.googleSheets",
"position": [
1450,
500
],
"parameters": {
"operation": "appendRow",
"sheetName": "Expenses",
"columnValues": {
"mapping": {
"Date": "={{$json.date}}",
"Type": "Expense",
"Amount": "={{$json.amount}}",
"Source": "OCR",
"Category": "={{$json.category}}",
"Description": "={{$json.description}}"
}
},
"spreadsheetId": "={{$env.YOUR_GOOGLE_SHEET_ID}}",
"authentication": "oAuth2",
"valueInputOption": "USER_ENTERED"
},
"typeVersion": 1
},
{
"name": "Send Expense Confirmation (OCR)",
"type": "n8n-nodes-base.telegramBot",
"position": [
1700,
500
],
"parameters": {
"text": "Expense from receipt successfully recorded:\nDescription: *{{$json.description}}*\nAmount: *${{parseFloat($json.amount).toLocaleString('en-US')}}*\nCategory: *{{$json.category}}*",
"chatId": "={{$json.message.chat.id}}",
"parseMode": "HTML"
},
"typeVersion": 1
}
],
"connections": {
"Check 'Photo'": {
"output": [
{
"node": "Get Telegram Photo",
"type": "main",
"index": 0
},
{
"node": "Send Error Message (Text)",
"type": "main",
"index": 1
}
]
},
"Check 'Income'": {
"output": [
{
"node": "Add Income to Google Sheet",
"type": "main",
"index": 0
},
{
"node": "Send Error Message (Text)",
"type": "main",
"index": 1
}
]
},
"Parse OCR Data": {
"output": [
{
"node": "Add Expense to Google Sheet (OCR)",
"type": "main",
"index": 0
}
]
},
"OCR.space Request": {
"output": [
{
"node": "Parse OCR Data",
"type": "main",
"index": 0
}
]
},
"Get Telegram Photo": {
"output": [
{
"node": "OCR.space Request",
"type": "main",
"index": 0
}
]
},
"Check 'Expense' (Text)": {
"output": [
{
"node": "Add Expense to Google Sheet (Text)",
"type": "main",
"index": 0
},
{
"node": "Send Error Message (Text)",
"type": "main",
"index": 1
}
]
},
"Telegram Bot (Webhook)": {
"output": [
{
"node": "Check 'Income'",
"type": "main",
"index": 0
},
{
"node": "Check 'Expense' (Text)",
"type": "main",
"index": 0
},
{
"node": "Check 'Photo'",
"type": "main",
"index": 0
}
]
},
"Add Income to Google Sheet": {
"output": [
{
"node": "Send Income Confirmation",
"type": "main",
"index": 0
}
]
},
"Add Expense to Google Sheet (OCR)": {
"output": [
{
"node": "Send Expense Confirmation (OCR)",
"type": "main",
"index": 0
}
]
},
"Add Expense to Google Sheet (Text)": {
"output": [
{
"node": "Send Expense Confirmation (Text)",
"type": "main",
"index": 0
}
]
}
}
}