diff --git a/workflows/Track Expenses from Receipt Photos with AI, Google Sheets & Slack Reports-10970/track_expenses_from_receipt_photos_with_ai_google_sheets_slack_reports.json b/workflows/Track Expenses from Receipt Photos with AI, Google Sheets & Slack Reports-10970/track_expenses_from_receipt_photos_with_ai_google_sheets_slack_reports.json new file mode 100644 index 000000000..f3f551b0f --- /dev/null +++ b/workflows/Track Expenses from Receipt Photos with AI, Google Sheets & Slack Reports-10970/track_expenses_from_receipt_photos_with_ai_google_sheets_slack_reports.json @@ -0,0 +1 @@ +{"id":"tXIAVEIkPlrlzwO9","meta":{"instanceId":"d66f4a8292e2201b622a3d783e2857e4d90c675c5e7bbd26eacc6af060964522"},"name":"Photo Receipt AI for Budget & Slack Alerts","tags":[],"nodes":[{"id":"d9e934b1-3ddb-4312-8556-03e0bc75f001","name":"Receipt Photo Upload","type":"n8n-nodes-base.webhook","position":[0,256],"webhookId":"8d328615-3aeb-42ec-990a-1263815468b2","parameters":{"path":"receipt Text","options":{},"httpMethod":"POST"},"typeVersion":2.1},{"id":"be58ef74-b716-4f12-875d-aa09572f9b3c","name":"Add to Budget Sheet","type":"n8n-nodes-base.googleSheets","position":[800,256],"parameters":{"columns":{"value":{"Date":"={{ $json[\"Date\"] }}","Items":"={{ $json[\"Items\"] }}","Store":"={{ $json[\"Store\"] }}","Amount":"={{ $json[\"Amount\"] }}"},"schema":[{"id":"Date","type":"string","display":true,"removed":false,"required":false,"displayName":"Date","defaultMatch":false,"canBeUsedToMatch":true},{"id":"Store","type":"string","display":true,"removed":false,"required":false,"displayName":"Store","defaultMatch":false,"canBeUsedToMatch":true},{"id":"Items","type":"string","display":true,"removed":false,"required":false,"displayName":"Items","defaultMatch":false,"canBeUsedToMatch":true},{"id":"Amount","type":"string","display":true,"removed":false,"required":false,"displayName":"Amount","defaultMatch":false,"canBeUsedToMatch":true}],"mappingMode":"defineBelow","matchingColumns":["Data"],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{},"operation":"append","sheetName":{"__rl":true,"mode":"list","value":"gid=0","cachedResultUrl":"https://docs.google.com/spreadsheets/d/1hzLc4ioe2H24wTG3TSQNMinFPhoi6LdlIJkyCb0yxNk/edit#gid=0","cachedResultName":"Budget"},"documentId":{"__rl":true,"mode":"list","value":"1hzLc4ioe2H24wTG3TSQNMinFPhoi6LdlIJkyCb0yxNk","cachedResultUrl":"https://docs.google.com/spreadsheets/d/1hzLc4ioe2H24wTG3TSQNMinFPhoi6LdlIJkyCb0yxNk/edit?usp=drivesdk","cachedResultName":"Budget"}},"typeVersion":4.7},{"id":"51956fcd-1422-4e1c-88fb-ea7a5f921fb2","name":"Code in JavaScript2","type":"n8n-nodes-base.code","position":[1024,256],"parameters":{"jsCode":"// Get all rows from the previous Google Sheets (Read) node\nconst items = $items();\n\n// If the sheet is empty, do nothing\nif (items.length === 0) {\n return [{\n json: {\n message: 'No household budget data yet.'\n }\n }];\n}\n\n// Function to convert date strings \"YYYY/MM/DD\" or \"YYYY-MM-DD\" to Date objects\nfunction parseDate(str) {\n if (!str) return null;\n const parts = str.split(/[\\/\\-]/).map(Number);\n if (parts.length < 3) return null;\n const [y, m, d] = parts;\n return new Date(y, m - 1, d);\n}\n\n// Assume the last row is the newly added record and aggregate by its month\nconst lastRow = items[items.length - 1].json;\nconst lastDateStr = lastRow['Date'];\nconst targetDate = parseDate(lastDateStr) || new Date();\n\n// Target Year-Month (e.g., \"2025-04\")\nconst targetYm = `${targetDate.getFullYear()}-${('0' + (targetDate.getMonth() + 1)).slice(-2)}`;\n\n// Calculate total for the current month\nlet total = 0;\nfor (const { json: row } of items) {\n const ds = row['Date'];\n const d = parseDate(ds);\n if (!d) continue;\n\n const ym = `${d.getFullYear()}-${('0' + (d.getMonth() + 1)).slice(-2)}`;\n if (ym === targetYm) {\n const price = Number(row['Amount'] || 0);\n if (!isNaN(price)) {\n total += price;\n }\n }\n}\n\n// ★ Monthly budget (change this to your desired amount)\nconst budget = 30000;\n\nconst remaining = budget - total;\n\n// Assemble message for Slack\nconst lines = [\n `📊 Household Budget for ${targetYm}`,\n `Budget: ${budget.toLocaleString()} JPY`,\n `Total Spent: ${total.toLocaleString()} JPY`,\n `Remaining Budget: ${remaining.toLocaleString()} JPY`,\n];\n\nif (remaining < 0) {\n lines.push(`⚠️ You have exceeded your budget by ${Math.abs(remaining).toLocaleString()} JPY.`);\n}\n\nreturn [{\n json: {\n month: targetYm,\n budget: budget,\n totalSpent: total,\n remainingBudget: remaining,\n message: lines.join('\\n')\n }\n}];\n"},"typeVersion":2},{"id":"8a6fa2ef-887e-4695-94d3-7275b5dd2410","name":"Send a message","type":"n8n-nodes-base.slack","position":[1600,256],"webhookId":"a82d6c2e-b15c-4795-b7ec-75b5296a487b","parameters":{"text":"={{ $json.output }}","select":"channel","channelId":{"__rl":true,"mode":"list","value":"C09SMMZGXMM","cachedResultName":""},"otherOptions":{},"authentication":"oAuth2"},"typeVersion":2.3},{"id":"843ef5f2-cba2-4629-bde0-f19355739d93","name":"OpenRouter Chat Model1","type":"@n8n/n8n-nodes-langchain.lmChatOpenRouter","position":[992,-64],"parameters":{"options":{}},"typeVersion":1},{"id":"c0ea41d0-15e8-461f-8162-f6762444f75b","name":"Code in JavaScript","type":"n8n-nodes-base.code","position":[576,256],"parameters":{"jsCode":"// Assuming AI Agent output is in $json.output\nlet raw = $json.output;\n\n// Convert to JSON if it's a string\nif (typeof raw === 'string') {\n raw = raw.trim();\n\n // Remove ```json ~ ``` if present\n raw = raw.replace(/^```(?:json)?/i, '').replace(/```$/i, '').trim();\n\n try {\n raw = JSON.parse(raw);\n } catch (error) {\n throw new Error('Could not parse AI Agent output as JSON: ' + error.message + '\\nraw=' + raw);\n }\n}\n\n// Ensure it's an array, even if only one item\nconst data = Array.isArray(raw) ? raw : [raw];\n\n// Format into n8n's items array structure\nconst items = data.map(entry => {\n return {\n json: {\n Date: entry['Date'],\n Store: entry['Store'],\n Items: entry['Items'],\n Amount: entry['Amount']\n }\n };\n});\n\nreturn items;\n"},"typeVersion":2},{"id":"fe796bfc-a632-4d8c-9ba3-2c76b768655d","name":"OpenRouter Chat Model2","type":"@n8n/n8n-nodes-langchain.lmChatOpenRouter","position":[296,480],"parameters":{"options":{}},"typeVersion":1},{"id":"a204a1cb-3cb6-4ea0-8e79-cbd6b301cd30","name":"Parse Receipt","type":"@n8n/n8n-nodes-langchain.agent","position":[224,256],"parameters":{"text":"={{ $json.body[''].text }}","options":{"systemMessage":"You are an expert in receipt analysis. From the following text, accurately extract the date, store name, purchased items, and amount, and output them in the following format:\n\n[\n {\n \"Date\": \"YYYY/MM/DD\",\n \"Store\": \"Store Name\",\n \"Items\": \"Item Name\",\n \"Amount\": Amount\n }\n]\n\nIf there are multiple items, return multiple objects. Do not include any extra comments or sentences, return only JSON. Group items with the same date into a single entry."},"promptType":"define"},"typeVersion":3},{"id":"8515213f-2fe5-4334-bc8c-7f6b6739c85d","name":"Report Budget","type":"@n8n/n8n-nodes-langchain.agent","position":[1248,256],"parameters":{"text":"={{ $json[\"message\"] }}","options":{"systemMessage":"You are a specialist in drafting polished content for Slack. Summarize the total amount spent and the remaining budget. Example:\nCurrent spending: 1000 JPY\nRemaining budget this month: 10000 JPY"},"promptType":"define"},"typeVersion":3},{"id":"78cafaa5-7ef7-4441-affb-ca5494ce10eb","name":"Daily Report Trigger","type":"n8n-nodes-base.cron","position":[240,-288],"parameters":{},"typeVersion":1},{"id":"4baa9d88-2664-4dbf-b59c-d673c743564b","name":"Get Budget Sheet (Daily)","type":"n8n-nodes-base.googleSheets","position":[464,-288],"parameters":{"options":{},"sheetName":{"__rl":true,"mode":"list","value":"gid=0","cachedResultUrl":"https://docs.google.com/spreadsheets/d/1hzLc4ioe2H24wTG3TSQNMinFPhoi6LdlIJkyCb0yxNk/edit#gid=0","cachedResultName":"Budget"},"documentId":{"__rl":true,"mode":"list","value":"1hzLc4ioe2H24wTG3TSQNMinFPhoi6LdlIJkyCb0yxNk","cachedResultUrl":"https://docs.google.com/spreadsheets/d/1hzLc4ioe2H24wTG3TSQNMinFPhoi6LdlIJkyCb0yxNk/edit#gid=0","cachedResultName":"Budget"}},"typeVersion":4.7},{"id":"a1e12212-03c5-4f11-bf73-4191b7a52c41","name":"Monthly Analysis","type":"n8n-nodes-base.code","position":[688,-288],"parameters":{"jsCode":"// All rows from the sheet\nconst items = $items();\nif (items.length === 0) {\n return [{\n json: {\n message: 'No household budget data yet.'\n }\n }];\n}\n\n// Date parsing function\nfunction parseDate(str) {\n if (!str) return null;\n const parts = str.split(/[\\/\\-]/).map(Number);\n if (parts.length < 3) return null;\n const [y, m, d] = parts;\n return new Date(y, m - 1, d);\n}\n\n// The month of the latest row's date will be the target month\nconst lastRow = items[items.length - 1].json;\nconst lastDate = parseDate(lastRow['Date']) || new Date();\nconst targetYm = `${lastDate.getFullYear()}-${('0' + (lastDate.getMonth() + 1)).slice(-2)}`;\n\n// For aggregation\nlet total = 0;\nconst byStore = {}; // Per store\nconst byItem = {}; // Per item\nconst byDate = {}; // Per date\n\nfor (const { json: row } of items) {\n const d = parseDate(row['Date']);\n if (!d) continue;\n\n const ym = `${d.getFullYear()}-${('0' + (d.getMonth() + 1)).slice(-2)}`;\n if (ym !== targetYm) continue; // Skip if not current month\n\n const price = Number(row['Amount'] || 0);\n if (isNaN(price)) continue;\n\n const dateKey = row['Date'];\n const store = row['Store'] || 'Unknown Store';\n const item = row['Items'] || 'Unknown Item';\n\n total += price;\n\n byDate[dateKey] = (byDate[dateKey] || 0) + price;\n byStore[store] = (byStore[store] || 0) + price;\n byItem[item] = (byItem[item] || 0) + price;\n}\n\n// Daily average\nconst days = Object.keys(byDate).length || 1;\nconst avgPerDay = Math.round(total / days);\n\n// Top N ranking function\nfunction topN(map, n) {\n return Object.entries(map)\n .sort((a, b) => b[1] - a[1])\n .slice(0, n)\n .map(([key, value]) => ({\n name: key,\n amount: value\n }));\n}\n\nconst topStores = topN(byStore, 3);\nconst topItems = topN(byItem, 3);\nconst topDays = topN(byDate, 3);\n\nreturn [{\n json: {\n month: targetYm,\n monthlyTotal: total,\n dailyAverage: avgPerDay,\n top3Stores: topStores,\n top3Items: topItems,\n top3SpendingDays: topDays\n }\n}];"},"typeVersion":2},{"id":"ca04ca63-5377-4f27-bc2f-87cfecee94d1","name":"Monthly Report","type":"@n8n/n8n-nodes-langchain.agent","position":[912,-288],"parameters":{"text":"={{`Create a report from this month's (${ $json[\"month\"] }) household budget data.\n\n- Total monthly expenditure: ${$json[\"monthlyTotal\"]} JPY\n- Daily average: ${$json[\"dailyAverage\"]} JPY\n- Top 3 stores by spending: ${JSON.stringify($json[\"top3Stores\"])}\n- Top 3 items by spending: ${JSON.stringify($json[\"top3Items\"])}\n- Top 3 days with most spending: ${JSON.stringify($json[\"top3SpendingDays\"])}\n\nSummarize this month's trends and provide simple advice in English, using bullet points and short comments. Keep it to a readable length and use a polite, informal tone.`}}","options":{"systemMessage":"You are an assistant that analyzes household budget data and writes clear English reports. Based on the given numbers, describe the trends and offer simple advice. Keep it to a readable length and use a polite, informal tone."},"promptType":"define"},"typeVersion":3},{"id":"6d340421-5082-4dc2-98e8-8aadb85acbc9","name":"Send monthly report","type":"n8n-nodes-base.slack","position":[1264,-288],"webhookId":"a82d6c2e-b15c-4795-b7ec-75b5296a487b","parameters":{"text":"={{ $json.output }}","select":"channel","channelId":{"__rl":true,"mode":"list","value":"C09SMMZGXMM","cachedResultName":""},"otherOptions":{},"authentication":"oAuth2"},"typeVersion":2.3},{"id":"1e37b78a-f1ce-46e6-94c2-3efd52f140b1","name":"OpenRouter Chat Model","type":"@n8n/n8n-nodes-langchain.lmChatOpenRouter","position":[1320,480],"parameters":{"options":{}},"typeVersion":1},{"id":"fc9c9473-198e-4d57-9968-f6f69030a385","name":"Sticky Note","type":"n8n-nodes-base.stickyNote","position":[-656,-272],"parameters":{"width":496,"height":592,"content":"## What it does\nThis workflow automates your household budget tracking in several steps:\n1. **Receipt Photo Upload**: You upload a photo of your receipt to a webhook.\n2. **Parse Receipt**: An AI agent extracts the date, store name, purchased items, and total amount from the receipt text.\n3. **Add to Budget Sheet**: The extracted data is then appended to your designated Google Sheet.\n4. **Daily Budget Report**: After each receipt entry, the workflow calculates your current month's spending, remaining budget, and sends a summary to Slack.\n5. **Monthly Budget Report**: Once a day (triggered by a cron job), it reads all budget data for the current month from Google Sheets, performs an analysis (total spending, daily average, top stores, items, and spending days), and sends a comprehensive report to Slack.\n### Requirements\n- **n8n Account**: Self-hosted or Cloud.\n- **Google Sheets**: A Google Sheet set up with columns for \"Date\", \"Store\", \"Items\", and \"Amount\".\n- **Slack Workspace**: A Slack channel where you want to receive budget updates.\n- **OpenRouter Account**: An API key for OpenRouter to use their chat models."},"typeVersion":1},{"id":"017435d3-43cb-4322-9697-024de9511297","name":"Sticky Note1","type":"n8n-nodes-base.stickyNote","position":[48,64],"parameters":{"color":7,"width":1600,"height":464,"content":"### Steps\n1. **Google Sheets Setup**: Create/use a Google Sheet, share with n8n credentials, and select it in the workflow nodes.\n2. **OpenRouter Credentials**: Get an API key from OpenRouter and configure n8n credentials for all chat model nodes.\n3. **Slack Credentials**: Connect your Slack workspace in n8n and select your desired channels for messages.\n4. **Webhook URL**: Copy the 'Receipt Photo Upload' webhook URL after activation.\n5. **Monthly Budget Adjustment**: Edit `const budget = 30000;` in 'Code in JavaScript2' to set your budget."},"typeVersion":1},{"id":"53e317a2-b71a-466d-a94c-d389dccf9b83","name":"Sticky Note3","type":"n8n-nodes-base.stickyNote","position":[288,-496],"parameters":{"color":7,"width":1024,"height":480,"content":"## How to customize the workflow\n\n- **Daily Report Trigger**: Adjust the cron settings for frequency.\n- **AI Model**: Change AI models in 'OpenRouter Chat Model' nodes to other LLMs.\n- **Output Formatting**: Customize Slack message content and tone in 'Report Budget' and 'Monthly Report' nodes.\n- **Additional Integrations**: Expand with more nodes for other accounting software, notifications, or advanced analysis."},"typeVersion":1}],"active":false,"pinData":{},"settings":{"executionOrder":"v1"},"versionId":"4e51c46b-3d65-4634-b5a6-ccd423cc3c4c","connections":{"Parse Receipt":{"main":[[{"node":"Code in JavaScript","type":"main","index":0}]]},"Report Budget":{"main":[[{"node":"Send a message","type":"main","index":0}]]},"Monthly Report":{"main":[[{"node":"Send monthly report","type":"main","index":0}]]},"Monthly Analysis":{"main":[[{"node":"Monthly Report","type":"main","index":0}]]},"Code in JavaScript":{"main":[[{"node":"Add to Budget Sheet","type":"main","index":0}]]},"Add to Budget Sheet":{"main":[[{"node":"Code in JavaScript2","type":"main","index":0}]]},"Code in JavaScript2":{"main":[[{"node":"Report Budget","type":"main","index":0}]]},"Daily Report Trigger":{"main":[[{"node":"Get Budget Sheet (Daily)","type":"main","index":0}]]},"Receipt Photo Upload":{"main":[[{"node":"Parse Receipt","type":"main","index":0}]]},"OpenRouter Chat Model":{"ai_languageModel":[[{"node":"Report Budget","type":"ai_languageModel","index":0}]]},"OpenRouter Chat Model1":{"ai_languageModel":[[{"node":"Monthly Report","type":"ai_languageModel","index":0}]]},"OpenRouter Chat Model2":{"ai_languageModel":[[{"node":"Parse Receipt","type":"ai_languageModel","index":0}]]},"Get Budget Sheet (Daily)":{"main":[[{"node":"Monthly Analysis","type":"main","index":0}]]}}} \ No newline at end of file