diff --git a/Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/README.md b/Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/README.md new file mode 100644 index 0000000..18f9dde --- /dev/null +++ b/Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/README.md @@ -0,0 +1,74 @@ +***Tags**: Supply Chain, Inventory Management, ABC Analysis, Pareto Principle, Demand Variability, Automation, Google Sheets* + +### Context + +Hi! Iโ€™m Samir โ€” a Supply Chain Engineer and Data Scientist based in Paris, and founder of [LogiGreen Consulting](https://logi-green.com). + +I help companies **optimise inventory and logistics operations** by combining **data analytics** and **workflow automation**. + +This workflow is part of our **inventory optimisation toolkit**, allowing businesses to perform **ABC classification and Pareto analysis** directly from their transactional sales data. + +> Automate inventory segmentation with n8n! + +๐Ÿ“ฌ For business inquiries, feel free to connect with me on [LinkedIn](https://www.linkedin.com/in/samir-saci) + +### Who is this template for? + +This workflow is designed for **supply chain analysts**, **demand planners**, or **inventory managers** who want to: + +- Identify their top-performing items (Pareto 80/20 principle) +- Classify products into **ABC categories** based on sales contribution +- Evaluate demand variability (XYZ classification support) + +Imagine you have a **Google Sheet** where daily sales transactions are stored: + +![Example Sheet](https://www.samirsaci.com/content/images/size/w1000/2025/09/image.png) + +The workflow aggregates sales by item, calculates cumulative contribution, and assigns **A, B, or C** classes. It also computes **mean, standard deviation, and coefficient of variation (CV)** to highlight demand volatility. + +![Workflow](https://www.samirsaci.com/content/images/2025/09/image-1.png) + +### How does it work? + +This workflow automates the process of **ABC & Pareto analysis from raw sales data**: + +- ๐Ÿ“Š Google Sheets input provides daily transactional sales +- ๐Ÿงฎ Aggregation & code nodes compute sales, turnover, and cumulative shares +- ๐Ÿง  ABC class mapping assigns items into A/B/C buckets +- ๐Ÿ“ˆ Demand variability metrics (XYZ) are calculated +- ๐Ÿ“‘ Results are appended into dedicated Google Sheets tabs for reporting + +[![Tutorial](https://www.samirsaci.com/content/images/2025/09/temp.png)](https://www.youtube.com/watch?v=YbAA-cq9X_E) + +๐ŸŽฅ [Watch My Tutorial](https://www.youtube.com/watch?v=YbAA-cq9X_E) + +### Steps: + +1. ๐Ÿ“ Load daily sales records from Google Sheets +2. ๐Ÿ”Ž Filter out items with zero sales +3. ๐Ÿ“Š Aggregate sales by **store**, **item**, and **day** +4. ๐Ÿ“ˆ Perform Pareto analysis to calculate cumulative turnover share +5. ๐Ÿงฎ Compute demand variability (mean, stdev, CV) +6. ๐Ÿง  Assign ABC classes based on cumulative share thresholds +7. ๐Ÿ“ฅ Append results into **ABC XYZ** and **Pareto** output sheets + +![workflow](https://www.samirsaci.com/content/images/size/w1000/2025/09/image-2.png) + +### What do I need to get started? + +Youโ€™ll need: + +- A **Google Sheet** with sales transactions (date, item, quantity, turnover) that is available here: [Test Sheet](https://docs.google.com/spreadsheets/d/1nsS_pj3o8yMEIQ59QBU80sx20HtkkTgCKJB32sOge1o/edit?usp=sharing) +- A **Google Sheets account** connected in n8n +- Basic knowledge of inventory analysis (ABC/XYZ) + +### Next Steps + +๐Ÿ—’๏ธ Use the sticky notes in the n8n canvas to: + +- Add your Google Sheets credentials +- Replace the Sheet ID with your own sales dataset +- Run the workflow and check the output tabs: **ABC XYZ**, **Pareto**, and **Store Sales** + +*This template was built using n8n v1.107.3* +*Submitted: September 15, 2025* diff --git a/Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/workflow.json b/Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/workflow.json new file mode 100644 index 0000000..da66108 --- /dev/null +++ b/Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/workflow.json @@ -0,0 +1,882 @@ +{ + "meta": { + "instanceId": "", + "templateCredsSetupCompleted": true + }, + "nodes": [ + { + "id": "dd09796e-b4c4-43df-8401-bba895d330b3", + "name": "When clicking โ€˜Execute workflowโ€™", + "type": "n8n-nodes-base.manualTrigger", + "position": [ + -1056, + -400 + ], + "parameters": {}, + "typeVersion": 1 + }, + { + "id": "5f502423-b87c-4322-aac6-10d07e685087", + "name": "Get row(s) in sheet", + "type": "n8n-nodes-base.googleSheets", + "position": [ + -832, + -400 + ], + "parameters": { + "options": {}, + "sheetName": { + "__rl": true, + "mode": "list", + "value": "gid=0", + "cachedResultUrl": "", + "cachedResultName": "Input Data" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "", + "cachedResultUrl": "", + "cachedResultName": "Data Analytics" + } + }, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "", + "name": "" + } + }, + "notesInFlow": true, + "typeVersion": 4.7 + }, + { + "id": "15e8b11a-83d9-41cb-84e4-dbfe0ddf970f", + "name": "Filter Out Zero Sales", + "type": "n8n-nodes-base.filter", + "position": [ + -608, + -400 + ], + "parameters": { + "options": {}, + "conditions": { + "options": { + "version": 2, + "leftValue": "", + "caseSensitive": true, + "typeValidation": "strict" + }, + "combinator": "and", + "conditions": [ + { + "id": "39ce1523-ed7e-404a-82db-099200bcb82c", + "operator": { + "type": "number", + "operation": "notEquals" + }, + "leftValue": "={{ $json.QTY }}", + "rightValue": "={{ 0 }}" + } + ] + } + }, + "typeVersion": 2.2 + }, + { + "id": "2cd9b9a0-dc4f-4d99-b4c4-43f6d165cad8", + "name": "Transpose", + "type": "n8n-nodes-base.code", + "position": [ + -160, + -592 + ], + "parameters": { + "jsCode": "const input = $input.all().map(i => i.json);\n\n// Collect store IDs\nconst stores = input.map(row => row.STORE);\n// Collect items (from keys)\nconst items = Object.keys(input[0]).filter(k => k !== \"STORE\");\n\n// Transpose\nconst result = items.map(ITEM => {\n let row = { ITEM };\n for (const store of stores) {\n const storeRow = input.find(r => r.STORE === store);\n row[store] = storeRow[ITEM];\n }\n return { json: row };\n});\n\nreturn result;\n" + }, + "typeVersion": 2 + }, + { + "id": "7bedeb08-f47e-4809-9936-7cdc8a9236d1", + "name": "Daily Sales per Store", + "type": "n8n-nodes-base.code", + "position": [ + -384, + -304 + ], + "parameters": { + "jsCode": "const agg = {};\nfor (const {json} of items) {\n const k = `${json.STORE}|${json.DAY}`;\n if (!agg[k]) agg[k] = { STORE: json.STORE, DAY: json.DAY, QTY: 0, TO: 0 };\n agg[k].QTY += Number(json.QTY||0);\n agg[k].TO += Number(json.TO||0);\n}\nreturn Object.values(agg).map(r => ({ json: r }));" + }, + "typeVersion": 2 + }, + { + "id": "b0134581-ef21-4485-bd75-8a5d7f865ad4", + "name": "If", + "type": "n8n-nodes-base.if", + "position": [ + -160, + -304 + ], + "parameters": { + "options": {}, + "conditions": { + "options": { + "version": 2, + "leftValue": "", + "caseSensitive": true, + "typeValidation": "strict" + }, + "combinator": "and", + "conditions": [ + { + "id": "59bb6e5b-0765-4812-a842-19a3bc7a0357", + "operator": { + "name": "filter.operator.equals", + "type": "string", + "operation": "equals" + }, + "leftValue": "={{ $json.STORE }}", + "rightValue": "STORE-1" + } + ] + } + }, + "typeVersion": 2.2 + }, + { + "id": "95a2efc7-9c7b-4263-aab4-c00bbe902714", + "name": "Sales Store 1", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 64, + -400 + ], + "parameters": { + "columns": { + "value": { + "TO": "={{ $json.TO }}", + "DAY": "={{ $json.DAY }}", + "QTY": "={{ $json.QTY }}" + }, + "schema": [ + { + "id": "DAY", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "DAY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "QTY", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "QTY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "TO", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "TO", + "defaultMatch": false, + "canBeUsedToMatch": true + } + ], + "mappingMode": "defineBelow", + "matchingColumns": [], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {}, + "operation": "append", + "sheetName": { + "__rl": true, + "mode": "list", + "value": 421687347, + "cachedResultUrl": "", + "cachedResultName": "Daily Sales Store 1" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "", + "cachedResultUrl": "", + "cachedResultName": "Data Analytics" + } + }, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "", + "name": "" + } + }, + "notesInFlow": true, + "typeVersion": 4.7 + }, + { + "id": "fdffd98a-36ac-425b-861c-b3f738828fe2", + "name": "Sales Store 2", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 64, + -208 + ], + "parameters": { + "columns": { + "value": { + "TO": "={{ $json.TO }}", + "DAY": "={{ $json.DAY }}", + "QTY": "={{ $json.QTY }}" + }, + "schema": [ + { + "id": "DAY", + "type": "string", + "display": true, + "required": false, + "displayName": "DAY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "QTY", + "type": "string", + "display": true, + "required": false, + "displayName": "QTY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "TO", + "type": "string", + "display": true, + "required": false, + "displayName": "TO", + "defaultMatch": false, + "canBeUsedToMatch": true + } + ], + "mappingMode": "defineBelow", + "matchingColumns": [], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {}, + "operation": "append", + "sheetName": { + "__rl": true, + "mode": "list", + "value": 749936247, + "cachedResultUrl": "", + "cachedResultName": "Daily Sales Store 2" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "", + "cachedResultUrl": "", + "cachedResultName": "Data Analytics" + } + }, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "", + "name": "" + } + }, + "notesInFlow": true, + "typeVersion": 4.7 + }, + { + "id": "8a3afe9e-d873-4a02-9fb1-45fc9c380601", + "name": "Pareto Analysis", + "type": "n8n-nodes-base.code", + "position": [ + -160, + -784 + ], + "parameters": { + "jsCode": "// Normalize + sort by turnover (TO) desc\nconst rows = items\n .map(i => ({\n ...i.json,\n TO: Number(i.json.TO || 0), // turnover = QTY x Unit Price\n QTY: Number(i.json.QTY || 0),\n }))\n .sort((a, b) => b.TO - a.TO);\n\nconst n = rows.length;\nconst totalTO = rows.reduce((s, r) => s + r.TO, 0) || 1;\n\nlet cumTO = 0;\nrows.forEach((r, idx) => {\n cumTO += r.TO;\n r.cum_turnover = cumTO; // absolute cumulative TO\n r.cum_share = +(cumTO / totalTO).toFixed(6); // 0..1\n r.sku_rank = idx + 1;\n r.cum_skus = +((idx + 1) / n).toFixed(6); // 0..1\n r.cum_skus_pct = +(r.cum_skus * 100).toFixed(2); // 0..100 for charts\n});\n\n// Return table\nreturn rows.map(r => ({ json: r }));\n\n" + }, + "typeVersion": 2 + }, + { + "id": "e8e1409d-b571-43b8-ae9e-bb8da228cefd", + "name": "TO, QTY GroupBy ITEM", + "type": "n8n-nodes-base.code", + "position": [ + -384, + -784 + ], + "parameters": { + "jsCode": "const agg = {};\nfor (const {json} of items) {\n const ITEM = String(json.ITEM);\n const TO = Number(json.TO);\n const QTY = Number(json.QTY);\n if (!agg[ITEM]) agg[ITEM] = { ITEM, TO: 0, QTY: 0 };\n agg[ITEM].TO += TO;\n agg[ITEM].QTY += QTY;\n}\nconst rows = Object.values(agg).sort((a,b)=> b.TO - a.TO);\nreturn rows.map(r => ({ json: r }));" + }, + "typeVersion": 2 + }, + { + "id": "9e02d5cb-701d-4a40-b2bf-c2fae43af8ef", + "name": "TO GroupBy (STORE, ITEM)", + "type": "n8n-nodes-base.code", + "position": [ + -384, + -592 + ], + "parameters": { + "jsCode": "// Build dictionaries of unique stores & skus\nconst stores = new Set();\nconst skus = new Set();\n// Append dictionnaries with values\nfor (const {json} of items) { stores.add(json.STORE); skus.add(json.ITEM); }\n// List of Unique SKU\nconst skuList = Array.from(skus).sort();\n\n// Accumulate sums\nconst mat = {}; // store_id -> sku -> sum\nfor (const {json} of items) {\n const s = json.STORE;\n const k = json.ITEM;\n const v = Number(json.TO) || 0;\n if (!mat[s]) mat[s] = {};\n mat[s][k] = (mat[s][k] || 0) + v;\n}\n\n// Output wide rows with 0 fill\nconst rows = [];\nfor (const s of Array.from(stores).sort()) {\n const row = { STORE: s };\n for (const k of skuList) row[k] = mat[s][k] || 0;\n rows.push(row);\n}\nreturn rows.map(r => ({ json: r }));\n" + }, + "typeVersion": 2 + }, + { + "id": "efe6f052-ff5d-4755-ae07-4a7bc480ddbb", + "name": "Multi-Store Sales", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 64, + -592 + ], + "parameters": { + "columns": { + "value": { + "TO": "={{ $json.TO }}", + "DAY": "={{ $json.DAY }}", + "QTY": "={{ $json.QTY }}" + }, + "schema": [ + { + "id": "DAY", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "DAY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "QTY", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "QTY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "TO", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "TO", + "defaultMatch": false, + "canBeUsedToMatch": true + } + ], + "mappingMode": "defineBelow", + "matchingColumns": [], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {}, + "operation": "append", + "sheetName": { + "__rl": true, + "mode": "list", + "value": 421687347, + "cachedResultUrl": "", + "cachedResultName": "Daily Sales Store 1" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "", + "cachedResultUrl": "", + "cachedResultName": "Data Analytics" + } + }, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "", + "name": "" + } + }, + "notesInFlow": true, + "typeVersion": 4.7 + }, + { + "id": "5bd88834-9c20-4d9e-bd31-146dc31e2c18", + "name": "Demand Variability x Sales %", + "type": "n8n-nodes-base.code", + "position": [ + -384, + -16 + ], + "parameters": { + "jsCode": "// INPUT items: rows with fields ITEM, DAY, QTY (others optional)\n\nfunction mean(a){ return a.reduce((s,x)=>s + x, 0) / (a.length || 1); }\nfunction stdev_samp(a){\n if (a.length <= 1) return 0;\n const m = mean(a);\n const v = a.reduce((s,x)=> s + (x - m) ** 2, 0) / (a.length - 1);\n return Math.sqrt(v);\n}\n\n// 1) build daily series per ITEM (sum across stores)\nconst series = {}; // ITEM -> { day -> qty_sum }\nlet totalQty = 0;\n\nfor (const { json } of items) {\n const item = String(json.ITEM);\n const day = String(json.DAY);\n const qty = Number(json.QTY || 0);\n\n if (!series[item]) series[item] = {};\n series[item][day] = (series[item][day] || 0) + qty;\n totalQty += qty;\n}\n\n// 2) compute metrics per ITEM\nconst out = [];\nfor (const [item, dayMap] of Object.entries(series)) {\n const daily = Object.values(dayMap); // array of daily qty\n const qty_total = daily.reduce((s,x)=>s+x, 0);\n const m = mean(daily);\n const sd = stdev_samp(daily);\n const cv = m ? sd / m : null;\n const share_qty_pct = totalQty ? (qty_total / totalQty) * 100 : 0;\n\n out.push({\n ITEM: item,\n qty_total,\n share_qty_pct: Number(share_qty_pct.toFixed(2)),\n mean_qty: Number(m.toFixed(3)),\n std_qty: Number(sd.toFixed(3)),\n cv_qty: cv == null ? null : Number(cv.toFixed(3)),\n });\n}\n\n// Optional: sort by share desc\nout.sort((a,b) => b.share_qty_pct - a.share_qty_pct);\n\n// Return as n8n items\nreturn out.map(r => ({ json: r }));\n" + }, + "typeVersion": 2 + }, + { + "id": "805e6a80-bb24-446e-ab9c-a0e0c68f555a", + "name": "ABC XYZ Analysis", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 64, + -16 + ], + "parameters": { + "columns": { + "value": { + "ABC": "={{ $json.ABC }}", + "ITEM": "={{ $json.ITEM }}", + "cv_qty": "={{ $json.cv_qty }}", + "std_qty": "={{ $json.std_qty }}", + "mean_qty": "={{ $json.mean_qty }}", + "cum_share": "={{ $json.cum_share }}", + "qty_total": "={{ $json.qty_total }}", + "share_qty_pct": "={{ $json.share_qty_pct }}" + }, + "schema": [ + { + "id": "ITEM", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "ITEM", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "qty_total", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "qty_total", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "share_qty_pct", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "share_qty_pct", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "cv_qty", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "cv_qty", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "ABC", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "ABC", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "mean_qty", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "mean_qty", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "std_qty", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "std_qty", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "cum_share", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "cum_share", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "limit_cov", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "limit_cov", + "defaultMatch": false, + "canBeUsedToMatch": true + } + ], + "mappingMode": "defineBelow", + "matchingColumns": [], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": { + "useAppend": false + }, + "operation": "append", + "sheetName": { + "__rl": true, + "mode": "list", + "value": 1281688243, + "cachedResultUrl": "", + "cachedResultName": "ABC XYZ" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "", + "cachedResultUrl": "", + "cachedResultName": "Data Analytics" + } + }, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "", + "name": "" + } + }, + "notesInFlow": true, + "typeVersion": 4.7 + }, + { + "id": "fb1b23cb-cad9-4a75-bb41-5d87209b51c5", + "name": "Update Pareto Sheet", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 64, + -784 + ], + "parameters": { + "columns": { + "value": { + "TO": "={{ $json.TO }}", + "QTY": "={{ $json.QTY }}", + "ITEM": "={{ $json.ITEM }}", + "cum_skus": "={{ $json.cum_skus }}", + "sku_rank": "={{ $json.sku_rank }}", + "cum_share": "={{ $json.cum_share }}", + "cum_skus_pct": "={{ $json.cum_skus_pct }}", + "cum_turnover": "={{ $json.cum_turnover }}" + }, + "schema": [ + { + "id": "ITEM", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "ITEM", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "TO", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "TO", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "QTY", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "QTY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "cum_turnover", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "cum_turnover", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "cum_share", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "cum_share", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "sku_rank", + "type": "string", + "display": true, + "removed": false, + "required": false, +