Upload files to "palletways"

This commit is contained in:
adminjeroen committed 2026-07-03 07:42:04 +02:00
1 parent c43cebef5a
commit 6c529e7120
1 file changed
+855
+855
View File
@@ -0,0 +1,855 @@
{
"name": "Palletways geboekte orders",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 20
}
]
}
},
"id": "ccc3cf02-f53d-4425-b6f9-586bcbdece52",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [
-704,
128
]
},
{
"parameters": {
"jsCode": "// Filter New Manifests - tolerant voor datumformaat \"YYYY-MM-DD HH:mm:ss\"\n\n// Haal de laatst gebruikte timestamp op\nconst lastRunTimestamp =\n $json.lastRunTimestamp ||\n $items(\"Parse Last Timestamp\")[0].json.lastRunTimestamp;\n\nconst items = [];\n\nfor (const item of $input.all()) {\n const manifest = item.json;\n const rawManifest = manifest.manifested;\n const rawLastRun = lastRunTimestamp;\n\n // Log de ruwe waarden\n console.log('--- Nieuw manifest check ---');\n console.log('manifested (raw):', rawManifest);\n console.log('lastRunTimestamp (raw):', rawLastRun);\n\n // Skip als een van beide leeg is\n if (!rawManifest || !rawLastRun) {\n console.log('⛔ Eén van de waarden ontbreekt, manifest overgeslagen');\n continue;\n }\n\n // Normaliseer: spatie → 'T'\n const manifestStr = rawManifest.replace(' ', 'T');\n const lastRunStr = rawLastRun.replace(' ', 'T');\n\n // Parse datums (voeg 'Z' toe als er geen tijdzone in zit)\n const manifestDate = new Date(\n /Z$|[+-]\\d{2}:?\\d{2}$/.test(manifestStr)\n ? manifestStr\n : manifestStr + 'Z'\n );\n\n const lastRunDate = new Date(\n /Z$|[+-]\\d{2}:?\\d{2}$/.test(lastRunStr)\n ? lastRunStr\n : lastRunStr + 'Z'\n );\n\n if (isNaN(manifestDate) || isNaN(lastRunDate)) {\n console.log('⛔ Ongeldige datum gevonden', {\n manifested: rawManifest,\n parsedManifest: manifestDate,\n lastRunTimestamp: rawLastRun,\n parsedLastRun: lastRunDate,\n });\n continue;\n }\n\n const isNew = manifestDate.getTime() > lastRunDate.getTime();\n console.log(\n `Vergelijking: ${manifestDate.toISOString()} > ${lastRunDate.toISOString()} = ${isNew}`\n );\n\n if (isNew) {\n items.push(item);\n }\n}\n\nconsole.log(`✅ Totaal nieuwe manifests: ${items.length}`);\n\nreturn items;\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
416
],
"id": "38c3d02d-0f70-479b-a91b-42cb3f4b3d39",
"name": "Filter New Manifests"
},
{
"parameters": {
"jsCode": "// Fixed timestamp parser - handles Redis data under \"propertyName\"\n\n// Helper function to format timestamps in UK timezone\nfunction formatUKTimestamp(date) {\n const options = {\n timeZone: 'Europe/London',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false\n };\n \n const formatter = new Intl.DateTimeFormat('en-CA', options);\n const parts = formatter.formatToParts(date);\n \n const year = parts.find(p => p.type === 'year').value;\n const month = parts.find(p => p.type === 'month').value;\n const day = parts.find(p => p.type === 'day').value;\n const hour = parts.find(p => p.type === 'hour').value;\n const minute = parts.find(p => p.type === 'minute').value;\n const second = parts.find(p => p.type === 'second').value;\n \n return {\n timestamp: `${year}-${month}-${day}T${hour}:${minute}:${second}`,\n date: `${year}-${month}-${day}`,\n time: `${hour}:${minute}:${second}`\n };\n}\n\n// Generate current UK timestamp (this will be saved for the NEXT run)\nconst now = new Date();\nconst currentUK = formatUKTimestamp(now);\n\n// Initialize variables for the PREVIOUS run timestamp\nlet lastRunTimestamp, lastRunDate, lastRunTime;\nlet source = 'unknown';\n\ntry {\n // Get the Redis response\n const input = $input.first()?.json;\n console.log('Full input received:', JSON.stringify(input, null, 2));\n \n // The Redis data is coming in under \"propertyName\" as a JSON string\n let parsedData = null;\n \n if (input?.propertyName) {\n // Parse the JSON string from propertyName\n parsedData = JSON.parse(input.propertyName);\n source = 'input.propertyName';\n console.log('Found and parsed Redis data from propertyName');\n } else if (input?.data) {\n // Fallback: check data property\n if (typeof input.data === 'string') {\n parsedData = JSON.parse(input.data);\n source = 'input.data-parsed';\n } else {\n parsedData = input.data;\n source = 'input.data-object';\n }\n console.log('Found Redis data in input.data');\n } else if (input?.lastRunTimestamp) {\n // Direct object\n parsedData = input;\n source = 'direct-input';\n console.log('Found Redis data directly in input');\n }\n \n console.log('Parsed Redis data:', JSON.stringify(parsedData, null, 2));\n \n if (!parsedData) {\n throw new Error('No Redis data found');\n }\n \n // Extract the previous timestamp - use exactly as stored in Redis\n if (parsedData.lastRunTimestamp) {\n lastRunTimestamp = parsedData.lastRunTimestamp; // Use exact value from Redis\n lastRunDate = parsedData.lastRunDate; // Use exact value from Redis\n lastRunTime = parsedData.lastRunTime; // Use exact value from Redis\n \n console.log(`✅ Successfully loaded PREVIOUS timestamp from Redis:`);\n console.log(` Timestamp: ${lastRunTimestamp}`);\n console.log(` Date: ${lastRunDate}`);\n console.log(` Time: ${lastRunTime}`);\n source = 'redis-success';\n \n } else {\n throw new Error('No lastRunTimestamp found in Redis data');\n }\n \n} catch (error) {\n // Fallback: Use 24 hours ago as the \"previous\" run time\n console.log(`❌ Error loading from Redis: ${error.message}`);\n console.log('Using 24-hour fallback for first run');\n \n const yesterday = new Date();\n yesterday.setHours(yesterday.getHours() - 24);\n const fallbackUK = formatUKTimestamp(yesterday);\n \n lastRunTimestamp = fallbackUK.timestamp;\n lastRunDate = fallbackUK.date;\n lastRunTime = fallbackUK.time;\n source = 'fallback-24h';\n \n console.log(`📅 Using fallback timestamp:`);\n console.log(` Timestamp: ${lastRunTimestamp}`);\n console.log(` Date: ${lastRunDate}`);\n console.log(` Time: ${lastRunTime}`);\n}\n\n// Log the final results\nconsole.log('\\n🔄 FINAL RESULTS:');\nconsole.log(`📍 PREVIOUS run (to use for queries): ${lastRunTimestamp}`);\nconsole.log(`🕐 CURRENT run (to save for next time): ${currentUK.timestamp}`);\nconsole.log(`📊 Data source: ${source}`);\n\n// Return both timestamps with clear separation\nreturn [{\n json: {\n // PREVIOUS run timestamps (use these for your data queries)\n lastRunTimestamp: lastRunTimestamp,\n lastRunDate: lastRunDate,\n lastRunTime: lastRunTime,\n \n // CURRENT run timestamps (save these to Redis for next run)\n currentTimestamp: currentUK.timestamp,\n currentDate: currentUK.date,\n currentTime: currentUK.time,\n \n // Metadata\n workflowId: $workflow.id,\n executionId: $execution.id,\n dataSource: source,\n processedAt: new Date().toISOString()\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-256,
128
],
"id": "b764e810-a177-4563-b8e5-d88ecdb6a66a",
"name": "Parse Last Timestamp"
},
{
"parameters": {
"operation": "get",
"key": "confirmed_lastrun_timestamp",
"options": {}
},
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
-480,
128
],
"id": "f979efde-6ec9-4d92-83c6-bd4428e26d6e",
"name": "Get Last Timestamp from Redis",
"credentials": {
"redis": {
"id": "uPJ4dFmf6tm25qHY",
"name": "Redis account 2"
}
},
"continueOnFail": true
},
{
"parameters": {
"operation": "set",
"key": "confirmed_lastrun_timestamp",
"value": "={{ $json.redisValue }}"
},
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
192,
0
],
"id": "feca99cb-8c2c-4c38-8e45-46dc37267894",
"name": "Save Timestamp to Redis",
"credentials": {
"redis": {
"id": "uPJ4dFmf6tm25qHY",
"name": "Redis account 2"
}
}
},
{
"parameters": {
"jsCode": "// Prepare the timestamp data structure for Redis storage\n// This saves the CURRENT execution time for the NEXT run to use\n\nconst input = $input.first().json;\n\n// Create the complete data structure\nconst timestampData = {\n lastRunTimestamp: input.currentTimestamp, // Current becomes \"last\" for next run\n lastRunDate: input.currentDate,\n lastRunTime: input.currentTime,\n workflowId: input.workflowId,\n executionId: input.executionId,\n savedAt: new Date().toISOString(),\n version: '3.0'\n};\n\nconsole.log('💾 Preparing to save CURRENT execution time for NEXT run:');\nconsole.log(` Timestamp: ${timestampData.lastRunTimestamp}`);\nconsole.log(` Date: ${timestampData.lastRunDate}`);\nconsole.log(` Time: ${timestampData.lastRunTime}`);\nconsole.log(` Workflow ID: ${timestampData.workflowId}`);\nconsole.log(` Execution ID: ${timestampData.executionId}`);\n\nreturn [{\n json: {\n success: true,\n timestampData: timestampData,\n redisValue: JSON.stringify(timestampData),\n summary: {\n whatWeUsedThisRun: {\n lastRunTimestamp: input.lastRunTimestamp,\n source: input.dataSource\n },\n whatWeSaveForNextRun: {\n lastRunTimestamp: timestampData.lastRunTimestamp,\n note: 'This current execution time becomes the lastRunTimestamp for next run'\n }\n }\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-32,
0
],
"id": "df86d744-cd39-4ce7-aeda-e743dc5b82e3",
"name": "Prepare Timestamp Data"
},
{
"parameters": {},
"id": "a5bc6a95-b035-4b21-9ab8-4eef02192b39",
"name": "When clicking ‘Execute workflow’",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-256,
480
]
},
{
"parameters": {
"jsCode": "const dataArray = $json.Response?.Detail?.Data || [];\nconst arr = Array.isArray(dataArray) ? dataArray : [dataArray];\nreturn arr.map(item => ({ json: item }));"
},
"id": "11ba156c-e249-44cb-8979-2b5c3d2ba661",
"name": "Splits items",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
416,
416
]
},
{
"parameters": {
"url": "https://api.palletways.com/consconfirmed?apikey=SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
},
"timeout": 30000
}
},
"id": "cf793d2d-475e-47fa-96cd-991f40072d4b",
"name": "Get Palletways Consignments1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
-32,
416
]
},
{
"parameters": {
"options": {
"explicitArray": false,
"mergeAttrs": true
}
},
"id": "f28e0fd7-69f4-4be5-bf74-49eb93cd290e",
"name": "Parse XML Response1",
"type": "n8n-nodes-base.xml",
"typeVersion": 1,
"position": [
192,
416
]
},
{
"parameters": {
"protocol": "sftp",
"path": "/dewit/data/edi_relaties.xlsx",
"options": {}
},
"id": "6946e2ed-1603-409c-94d4-902ef14890bd",
"name": "Download XLSX1",
"type": "n8n-nodes-base.ftp",
"typeVersion": 1,
"position": [
-32,
224
],
"credentials": {
"sftp": {
"id": "uKyzg5cSXQqXOuHI",
"name": "SFTP n8n"
}
}
},
{
"parameters": {
"operation": "xlsx",
"options": {}
},
"id": "5a92baf2-94f6-4568-a69e-f4f4a77a2656",
"name": "Extract from XLSX1",
"type": "n8n-nodes-base.extractFromFile",
"typeVersion": 1,
"position": [
192,
224
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"name": "Relationcode",
"value": "={{ $json.Relationcode }}",
"type": "string"
},
{
"name": "Direction",
"value": "={{ $json.Direction }}",
"type": "string"
}
]
},
"options": {}
},
"id": "16a10eb2-c5c1-4245-9ee5-c255ef8f3f46",
"name": "Edit Fields1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
640,
224
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "3998f988-a9e1-4eb8-8b2f-96b8cb95eb10",
"leftValue": "={{ $json.Direction }}",
"rightValue": 1,
"operator": {
"type": "number",
"operation": "equals"
}
},
{
"id": "86c6b058-d475-44b8-9407-6eab8130a9e1",
"leftValue": "={{ $json.Direction }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "or"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
416,
224
],
"id": "b057c5cc-f60a-42bb-80c3-cf8048032ad5",
"name": "If1"
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
864,
416
],
"id": "4dc54edb-2230-421c-90f9-f5dd968a253d",
"name": "Loop Over Items"
},
{
"parameters": {
"jsCode": "// Haal de lookup tabel uit de Edit Fields node\nconst lookup = $items(\"Edit Fields1\").map(\n item => item.json.Relationcode?.toString().trim()\n);\n\n// Pak de consignment code van dit item\nconst consignmentCode = $json.account_code?.toString().trim();\n\n// Zet een match-flag in het object\n$json.match = lookup.includes(consignmentCode);\n\nreturn [{ json: $json }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1088,
528
],
"id": "b26f1bef-5e73-4d47-8ce8-b355af5f1ff0",
"name": "filter"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "0b91ddb7-4c9a-42e2-84a8-5dba9a95cdcd",
"leftValue": "={{ $json.match }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.filter",
"typeVersion": 2.2,
"position": [
1088,
336
],
"id": "8d2f1bf2-94f6-419f-ba24-6ec2abe59145",
"name": "Filter"
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
1312,
336
],
"id": "f33f1260-28c0-4d63-b05f-d7fc1f3c52b2",
"name": "Loop Over Items1"
},
{
"parameters": {
"url": "=https://api.palletways.com/getconsignment/{{ $json.dc_syscon }}?apikey=SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D&outputformat=json",
"options": {}
},
"id": "3016eb9b-2baf-42a9-9a04-ec47b5b350e3",
"name": "Get Consignment Data",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [
1536,
192
]
},
{
"parameters": {
"functionCode": "// Extract Depot IDs - Altijd alle 3 types\nconst consignmentData = items[0].json;\n\n// Debug: log the structure we're working with\nconsole.log('Consignment data structure:', Object.keys(consignmentData));\n\n// Get depot numbers - check multiple possible paths\nconst detail = consignmentData.Response?.Detail?.Data || consignmentData.Detail?.Data;\nconst manifest = detail?.Manifest;\nconst consignment = manifest?.Consignment;\n\nconst depotNumber = manifest?.Depot?.Number;\nconst collectionDepot = manifest?.Depot?.Account?.Consignment?.CollectionDepot;\nconst deliveryDepot = manifest?.Depot?.Account?.Consignment?.DeliveryDepot;\n\nconsole.log('Found depots:', { depotNumber, collectionDepot, deliveryDepot });\n\n// ALTIJD alle 3 de types maken, ook als depot ID leeg is\nconst allDepots = [\n { depotId: depotNumber || 'EMPTY', type: 'depot' },\n { depotId: collectionDepot || 'EMPTY', type: 'collection' },\n { depotId: deliveryDepot || 'EMPTY', type: 'delivery' }\n];\n\n// Return alle 3, ongeacht of ze een geldig depot ID hebben\nreturn allDepots.map(depot => ({\n json: {\n depotId: depot.depotId,\n type: depot.type,\n originalData: consignmentData,\n isEmpty: depot.depotId === 'EMPTY'\n }\n}));"
},
"id": "4819cac6-6a76-4cd5-976c-9d0f6d993c04",
"name": "Extract Depot IDs",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
1760,
192
],
"alwaysOutputData": false
},
{
"parameters": {
"url": "=https://api.palletways.com/lookupDepotNo/{{ $json.depotId }}?apikey=SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D&outputformat=json",
"options": {}
},
"id": "8125d6f1-2951-422c-94ee-d75762a1cbe7",
"name": "Lookup Depot Info",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [
1984,
192
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{$json[\"consignmentData\"][\"Response\"][\"Status\"][\"Code\"]}}",
"value2": "OK"
}
]
}
},
"id": "e9622a3c-603e-43e4-8abb-a95be5b5b2f7",
"name": "Check API Response",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
2432,
192
]
},
{
"parameters": {
"functionCode": "// Correcte data structuur gebaseerd op de debug output\nconst consignmentData = items[0].json.Detail.Data;\nconst consignment = consignmentData.Manifest?.Depot?.Account?.Consignment;\nconst depot = consignmentData.Manifest?.Depot;\n\n// >>> NIEUW: customer_id uit input halen (Manifest.Depot.Account.Code)\nconst customerIdFromInput = consignmentData.Manifest?.Depot?.Account?.Code;\nif (!customerIdFromInput) {\n throw new Error('No Account.Code found in input (Manifest.Depot.Account.Code) for customer_id');\n}\n\n// Depot informatie uit de andere items (ook via .json)\nconst depotData = {\n depot: items[1]?.json?.Detail?.Data, // Index 1 bevat depot 454 info\n collection: items[2]?.json?.Detail?.Data, // Index 2 bevat depot 464 info (collection)\n delivery: items[3]?.json?.Detail?.Data // Index 3 bevat depot 182 info (delivery)\n};\n\nif (!consignment) {\n throw new Error('No consignment data found');\n}\n\n// Helper function to get current date/time\nfunction getCurrentDateTime() {\n const now = new Date();\n const date = now.toISOString().split('T')[0];\n const time = now.toTimeString().split(' ')[0].substring(0, 5);\n return { date, time };\n}\n\n// ------------------- Billing unit regels (mm + kg) -------------------\n// PalletDetail input is meestal mm (1200 = 120 cm). Output goodslines moeten cm zijn.\nconst BILLING_UNITS = [\n { code: 'MQP', maxWeight: 150, l: 1200, w: 1000, h: 600 },\n { code: 'QP', maxWeight: 250, l: 1200, w: 1000, h: 800 },\n { code: 'HP', maxWeight: 500, l: 1200, w: 1000, h: 1200 },\n { code: 'EPL', maxWeight: 750, l: 1200, w: 800, h: 2200 },\n { code: 'LP', maxWeight: 750, l: 1300, w: 1000, h: 2200 },\n { code: 'FP', maxWeight: 1200, l: 1200, w: 1000, h: 2200 },\n];\n\nconst UNIT_BY_CODE = Object.fromEntries(BILLING_UNITS.map(u => [u.code, u]));\nconst SMALLEST_BILLING_UNIT = 'MQP';\n\nfunction toNumber(v) {\n if (v === undefined || v === null || v === '') return 0;\n const n = Number(String(v).replace(',', '.'));\n return Number.isFinite(n) ? n : 0;\n}\nfunction toPositiveInt(v, fallback = 1) {\n const n = parseInt(String(v ?? '').replace(',', '.').trim(), 10);\n return Number.isFinite(n) && n > 0 ? n : fallback;\n}\n\nconst liftsAmount = toPositiveInt(consignment.Lifts, 1);\n\n// Als iemand per ongeluk cm aanlevert (120 i.p.v. 1200), normaliseren we naar mm.\n// Drempel 400 is veilig voor palletmaten.\nfunction normalizeDimToMm(v) {\n const n = toNumber(v);\n if (!n) return 0;\n return n < 400 ? n * 10 : n;\n}\n\nfunction mmToCmString(mm) {\n const n = normalizeDimToMm(mm);\n if (!n) return '';\n const cm = n / 10;\n return Number.isInteger(cm) ? String(cm) : String(cm).replace(/\\.0+$/, '');\n}\n\nfunction fitsFootprint(l, w, specL, specW) {\n // sta rotatie toe (L/W omdraaien)\n return (l <= specL && w <= specW) || (l <= specW && w <= specL);\n}\n\n// fallback classificatie (alleen als er palletdetails \"over\" zijn)\nfunction bestCandidate(cands) {\n let best = null;\n let bestScore = null;\n for (const spec of cands) {\n const area = spec.l * spec.w;\n const score = [area, spec.h, spec.maxWeight];\n if (!best || score[0] < bestScore[0]\n || (score[0] === bestScore[0] && score[1] < bestScore[1])\n || (score[0] === bestScore[0] && score[1] === bestScore[1] && score[2] < bestScore[2])\n ) {\n best = spec;\n bestScore = score;\n }\n }\n return best;\n}\n\nfunction classifyBillingUnit(length, width, height, weightKg) {\n const l = normalizeDimToMm(length);\n const w = normalizeDimToMm(width);\n const h = normalizeDimToMm(height);\n const kg = toNumber(weightKg);\n\n const strict = BILLING_UNITS.filter(spec =>\n fitsFootprint(l, w, spec.l, spec.w) && h <= spec.h && kg <= spec.maxWeight\n );\n if (strict.length) return { type: bestCandidate(strict).code, oversize: false };\n\n const dimsFit = BILLING_UNITS.filter(spec =>\n fitsFootprint(l, w, spec.l, spec.w) && h <= spec.h\n );\n if (dimsFit.length) return { type: bestCandidate(dimsFit).code, oversize: true };\n\n return { type: 'FP', oversize: true };\n}\n\n// ------------------- Match PalletDetail aan BillUnit -------------------\nfunction buildPalletPool(palletDetailsRaw, palletsRaw) {\n const arr = [];\n for (let i = 0; i < palletDetailsRaw.length; i++) {\n const pd = palletDetailsRaw[i] || {};\n arr.push({\n idx: i,\n raw: pd,\n l: normalizeDimToMm(pd.Length),\n w: normalizeDimToMm(pd.Width),\n h: normalizeDimToMm(pd.Height),\n kg: toNumber(pd.Weight),\n barcode: palletsRaw[i] || ''\n });\n }\n return arr;\n}\n\n// Score: lager = beter.\n// We willen eerst \"past in type\" (dims/hoogte/gewicht), daarna zo strak mogelijk.\nfunction scorePalletForType(pallet, billType) {\n const spec = UNIT_BY_CODE[billType];\n if (!spec) {\n // onbekend type -> pak eerste beschikbare (lage score)\n return 0;\n }\n\n const dimsOk = fitsFootprint(pallet.l,Line truncated
},
"id": "8bc1604a-7514-4f80-a416-b3a1161cb9d1",
"name": "Transform to Transpas Format",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
2656,
192
]
},
{
"parameters": {
"functionCode": "const data = items[0].json;\n\n// --- XML escaping (zonder dubbel-escapen van bestaande entities) ---\nconst AMP_FIX_RE = /&(?!amp;|lt;|gt;|quot;|apos;|#\\d+;|#x[0-9A-Fa-f]+;)/g;\n\nfunction xmlEsc(v) {\n return String(v ?? '')\n .replace(AMP_FIX_RE, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\n// (optioneel) maak bestandsnaam \"veilig\"\nfunction safeFilename(s) {\n return String(s ?? '')\n .replace(/[\\/\\\\?%*:|\"<>]/g, '-') // Windows/algemene verboden tekens\n .replace(/\\s+/g, '_')\n .slice(0, 120);\n}\n\n// Helper function to create XML element (XML-safe)\nfunction createElement(name, content, attributes = {}) {\n let attrStr = '';\n for (const [key, value] of Object.entries(attributes)) {\n if (value !== undefined && value !== null && value !== '') {\n // attributes altijd escapen (ook als het \"maar een nummer\" is)\n attrStr += ` ${key}=\"${xmlEsc(value)}\"`;\n }\n }\n\n if (content === undefined || content === null || content === '') {\n return `<${name}${attrStr}/>`;\n }\n\n if (typeof content === 'object') {\n let innerXml = '';\n for (const [key, value] of Object.entries(content)) {\n if (Array.isArray(value)) {\n value.forEach(item => {\n innerXml += createElement(key, item);\n });\n } else {\n innerXml += createElement(key, value);\n }\n }\n return `<${name}${attrStr}>${innerXml}</${name}>`;\n }\n\n // content escapen\n return `<${name}${attrStr}>${xmlEsc(content)}</${name}>`;\n}\n\n// Build XML\nlet xml = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n';\nxml += '<import>\\n';\nxml += ` ${createElement('ediprovider_id', data.ediprovider_id, { matchmode: '0' })}\\n`;\nxml += ` ${createElement('company_id', data.company_id, { matchmode: '0' })}\\n`;\nxml += ` <sourcefilename></sourcefilename>\\n`;\nxml += ' <transportbookings>\\n';\nxml += ' <transportbooking>\\n';\nxml += ' <autoacceptmethod>0</autoacceptmethod>\\n';\nxml += ` ${createElement('edireference', data.edireference)}\\n`;\nxml += ` ${createElement('customer_id', data.customer_id, { matchmode: '8' })}\\n`;\nxml += ` ${createElement('reference', data.reference)}\\n`;\nxml += ` ${createElement('department_id', '1', { matchmode: '0' })}\\n`;\nxml += ` ${createElement('currency_id', '1', { matchmode: '0' })}\\n`;\nxml += ' <shipments>\\n';\nxml += ' <shipment>\\n';\nxml += ` ${createElement('edireference', data.shipment.edireference)}\\n`;\nxml += ` ${createElement('reference', data.shipment.reference)}\\n`;\nxml += ` ${createElement('plangroup_id', '33' , { matchmode: '0' })}\\n`;\nxml += ` ${createElement('shipmentkind_id', data.shipment.shipmentkind_id, { matchmode: '0' })}\\n`;\n\n// Sender\nxml += ' <sender>\\n';\nxml += ` ${createElement('name', data.shipment.sender.name)}\\n`;\nxml += ` ${createElement('address1', data.shipment.sender.address1)}\\n`;\nxml += ` ${createElement('zipcode', data.shipment.sender.zipcode)}\\n`;\nxml += ` ${createElement('city_id', data.shipment.sender.city_id, { matchmode: '4' })}\\n`;\nxml += ` ${createElement('country_id', data.shipment.sender.country_id, { matchmode: '2' })}\\n`;\nxml += ` ${createElement('contact', data.shipment.sender.contact)}\\n`;\nxml += ` ${createElement('phone', data.shipment.sender.phone)}\\n`;\nxml += ' </sender>\\n';\n\n// Receiver\nxml += ' <receiver>\\n';\nxml += ` ${createElement('name', data.shipment.receiver.name)}\\n`;\nxml += ` ${createElement('address1', data.shipment.receiver.address1)}\\n`;\nxml += ` ${createElement('zipcode', data.shipment.receiver.zipcode)}\\n`;\nxml += ` ${createElement('city_id', data.shipment.receiver.city_id, { matchmode: '4' })}\\n`;\nxml += ` ${createElement('country_id', data.shipment.receiver.country_id, { matchmode: '2' })}\\n`;\nxml += ` ${createElement('contact', data.shipment.receiver.contact)}\\n`;\nxml += ` ${createElement('phone', data.shipment.receiver.phone)}\\n`;\nxml += ' </receiver>\\n';\n\n// Pickup address \nxml += ' <pickupaddress>\\n';\nxml += ' <address_id matchmode=\"5\"></address_id>\\n';\nxml += ` ${createElement('date', data.shipment.pickupaddress.date)}\\n`;\nxml += ` ${createElement('time', data.shipment.pickupaddress.time)}\\n`;\nxml += ` ${createElement('name', data.shipment.pickupaddress.name)}\\n`;\nxml += ` ${createElement('address1', data.shipment.pickupaddress.address1)}\\n`;\nxml += ` ${createElement('address2', data.shipment.pickupaddress.address2)}\\n`;\nxml += ` ${createElement('zipcode', data.shipment.pickupaddress.zipcode)}\\n`;\nxmLine truncated
},
"id": "3457998b-ada2-4671-9e45-32c8fb269903",
"name": "Generate XML",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
2880,
192
]
},
{
"parameters": {},
"id": "b16b247f-cc87-4b49-98b9-4e09741f7711",
"name": "Log Error",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
2656,
0
]
},
{
"parameters": {},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
2208,
192
],
"id": "d2c103eb-bb12-4eaf-87fc-f013f5051d1a",
"name": "Merge"
},
{
"parameters": {
"operation": "upload",
"path": "=/prod/ToTP/palletways/{{ $json.filename }}",
"binaryData": false,
"fileContent": "={{ $json.xml }}",
"options": {}
},
"id": "47f6f66a-bef5-4525-8f26-0a810cdd2ea4",
"name": "Upload to FTP1",
"type": "n8n-nodes-base.ftp",
"typeVersion": 1,
"position": [
3104,
336
],
"credentials": {
"ftp": {
"id": "oLAZ4OgmkOopMHAq",
"name": "FTP De Wit Transport"
}
}
},
{
"parameters": {
"functionCode": "// Correcte data structuur gebaseerd op de debug output\nconst consignmentData = items[0].json.Detail.Data;\nconst consignment = consignmentData.Manifest?.Depot?.Account?.Consignment;\nconst depot = consignmentData.Manifest?.Depot;\n\n// >>> NIEUW: customer_id uit input halen (Manifest.Depot.Account.Code)\nconst customerIdFromInput = consignmentData.Manifest?.Depot?.Account?.Code;\nif (!customerIdFromInput) {\n throw new Error('No Account.Code found in input (Manifest.Depot.Account.Code) for customer_id');\n}\n\n// Depot informatie uit de andere items (ook via .json)\nconst depotData = {\n depot: items[1]?.json?.Detail?.Data, // Index 1 bevat depot 454 info\n collection: items[2]?.json?.Detail?.Data, // Index 2 bevat depot 464 info (collection)\n delivery: items[3]?.json?.Detail?.Data // Index 3 bevat depot 182 info (delivery)\n};\n\nif (!consignment) {\n throw new Error('No consignment data found');\n}\n\n// Helper function to get current date/time\nfunction getCurrentDateTime() {\n const now = new Date();\n const date = now.toISOString().split('T')[0];\n const time = now.toTimeString().split(' ')[0].substring(0, 5);\n return { date, time };\n}\n\n// Get addresses\nconst collectionAddress = consignment.Address?.find(addr => addr.Type === 'Collection');\nconst deliveryAddress = consignment.Address?.find(addr => addr.Type === 'Delivery');\n\n// Get service codes\nconst services = Array.isArray(consignment.Service) ? consignment.Service : [consignment.Service];\nconst hasEorA = services.some(service => service?.Code === 'E' || service?.Code === 'A');\nconst shipmentKindId = hasEorA ? '30' : '29';\n\n// Pickup en delivery addresses\nlet pickupAddressData, deliveryAddressData;\n\npickupAddressData = {\n name: collectionAddress?.CompanyName || '',\n address1: collectionAddress?.Addr1 || '',\n address2: collectionAddress?.Addr2 || '',\n zipcode: collectionAddress?.PostCode || '',\n city: collectionAddress?.Town || '',\n country: collectionAddress?.Country || '',\n contact: collectionAddress?.ContactName || '',\n phone: collectionAddress?.Telephone || '',\n driverinfo: consignment.ManifestNote || '',\n reference: consignment.CollectionReference || '',\n};\n\ndeliveryAddressData = {\n name: deliveryAddress?.CompanyName || '',\n address1: deliveryAddress?.Addr1 || '',\n address2: deliveryAddress?.Addr2 || '',\n zipcode: deliveryAddress?.PostCode || '',\n city: deliveryAddress?.Town || '',\n country: deliveryAddress?.Country || '',\n contact: consignment.BookInContactName || deliveryAddress?.ContactName || '',\n phone: deliveryAddress?.Telephone || '',\n email: consignment.BookInEmailAddress || '',\n deliveryreference: consignment.BookInReference || '',\n driverinfo: consignment.ManifestNote || '',\n datetill: consignment.DueDate || '',\n timetill: consignment.DueTime || ''\n};\n\n// Get pallet barcode (only first if multiple) - Pallet is now an array\nconst palletBarcode = Array.isArray(consignment.Pallet) ? consignment.Pallet[0] : (consignment.Pallet || '');\n\n// Create parties array\nconst parties = [];\nif (depotData.depot) {\n parties.push({\n partytype_id: '16',\n name: depotData.depot.CompanyName || '',\n address1: depotData.depot.AddressLine1 || '',\n address2: depotData.depot.AddressLine2 || '',\n zipcode: depotData.depot.Postcode || '',\n cityname: depotData.depot.Town || '',\n country_id: depotData.depot.Countrycode || '',\n phone: depotData.depot.Phone || '',\n email: depotData.depot.Email || ''\n });\n}\nif (depotData.collection) {\n parties.push({\n partytype_id: '0',\n name: depotData.collection.CompanyName || '',\n address1: depotData.collection.AddressLine1 || '',\n address2: depotData.collection.AddressLine2 || '',\n zipcode: depotData.collection.Postcode || '',\n cityname: depotData.collection.Town || '',\n country_id: depotData.collection.Countrycode || '',\n phone: depotData.collection.Phone || '',\n email: depotData.collection.Email || ''\n });\n}\nif (depotData.delivery) {\n parties.push({\n partytype_id: '0',\n name: depotData.delivery.CompanyName || '',\n address1: depotData.delivery.AddressLine1 || '',\n address2: depotData.delivery.AddressLine2 || '',\n zipcode: depotData.delivery.Postcode || '',\n cityname: depotData.delivery.Town || '',\n country_id: depotData.delivery.Countrycode || '',\n phone: depotData.delivery.Phone || '',\n email: depotData.delivery.Email || ''\n });\n}\n\n// Load address party (partytype_id 1)\nparties.push({\n partytype_id: '1',\n name: collectionAddress?.CompanyName || '',\n address1: collectionAddress?.Addr1 || '',\n address2: collectionAddress?.Addr2 || '',\n zipcode: collectionAddress?.PostCode || '',\n cityname: collectionAddress?.Town || '',\n country_id: collectionAddress?.Country || '',\n contact: collectionAddress?.ContactName || '',\n phone: collectionAddress?.Telephone || ''\n});\n\n// Unload Line truncated
},
"id": "0bcb16fc-4aae-49fa-903e-caf369bb289a",
"name": "Transform to Transpas Format1",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
-704,
752
]
},
{
"parameters": {
"content": "## Timezone staat ingesteld op London",
"height": 112,
"width": 448
},
"type": "n8n-nodes-base.stickyNote",
"position": [
-384,
-368
],
"typeVersion": 1,
"id": "f7447818-b904-4f70-be79-688c644266e0",
"name": "Sticky Note"
}
],
"pinData": {
"Schedule Trigger": [
{
"json": {
"timestamp": "2026-06-24T12:20:26.009+01:00",
"Readable date": "June 24th 2026, 12:20:26 pm",
"Readable time": "12:20:26 pm",
"Day of week": "Wednesday",
"Year": "2026",
"Month": "June",
"Day of month": "24",
"Hour": "12",
"Minute": "20",
"Second": "26",
"Timezone": "Europe/London (UTC+01:00)"
},
"pairedItem": {
"item": 0
}
}
]
},
"connections": {
"Schedule Trigger": {
"main": [
[
{
"node": "Get Last Timestamp from Redis",
"type": "main",
"index": 0
}
]
]
},
"Filter New Manifests": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Get Last Timestamp from Redis": {
"main": [
[
{
"node": "Parse Last Timestamp",
"type": "main",
"index": 0
}
]
]
},
"Parse Last Timestamp": {
"main": [
[
{
"node": "Prepare Timestamp Data",
"type": "main",
"index": 0
},
{
"node": "Get Palletways Consignments1",
"type": "main",
"index": 0
},
{
"node": "Download XLSX1",
"type": "main",
"index": 0
}
]
]
},
"Prepare Timestamp Data": {
"main": [
[
{
"node": "Save Timestamp to Redis",
"type": "main",
"index": 0
}
]
]
},
"When clicking ‘Execute workflow’": {
"main": [
[
{
"node": "Download XLSX1",
"type": "main",
"index": 0
},
{
"node": "Get Palletways Consignments1",
"type": "main",
"index": 0
}
]
]
},
"Splits items": {
"main": [
[
{
"node": "Filter New Manifests",
"type": "main",
"index": 0
}
]
]
},
"Get Palletways Consignments1": {
"main": [
[
{
"node": "Parse XML Response1",
"type": "main",
"index": 0
}
]
]
},
"Parse XML Response1": {
"main": [
[
{
"node": "Splits items",
"type": "main",
"index": 0
}
]
]
},
"Download XLSX1": {
"main": [
[
{
"node": "Extract from XLSX1",
"type": "main",
"index": 0
}
]
]
},
"Extract from XLSX1": {
"main": [
[
{
"node": "If1",
"type": "main",
"index": 0
}
]
]
},
"If1": {
"main": [
[
{
"node": "Edit Fields1",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[
{
"node": "Filter",
"type": "main",
"index": 0
}
],
[
{
"node": "filter",
"type": "main",
"index": 0
}
]
]
},
"filter": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Filter": {
"main": [
[
{
"node": "Loop Over Items1",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items1": {
"main": [
[],
[
{
"node": "Get Consignment Data",
"type": "main",
"index": 0
}
]
]
},
"Get Consignment Data": {
"main": [
[
{
"node": "Extract Depot IDs",
"type": "main",
"index": 0
},
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"Extract Depot IDs": {
"main": [
[
{
"node": "Lookup Depot Info",
"type": "main",
"index": 0
}
]
]
},
"Lookup Depot Info": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"Check API Response": {
"main": [
[
{
"node": "Log Error",
"type": "main",
"index": 0
}
],
[
{
"node": "Transform to Transpas Format",
"type": "main",
"index": 0
}
]
]
},
"Transform to Transpas Format": {
"main": [
[
{
"node": "Generate XML",
"type": "main",
"index": 0
}
]
]
},
"Generate XML": {
"main": [
[
{
"node": "Upload to FTP1",
"type": "main",
"index": 0
}
]
]
},
"Merge": {
"main": [
[
{
"node": "Check API Response",
"type": "main",
"index": 0
}
]
]
},
"Upload to FTP1": {
"main": [
[
{
"node": "Loop Over Items1",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"timezone": "Europe/London",
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false,
"executionTimeout": 60,
"timeSavedMode": "fixed",
"saveDataErrorExecution": "all"
},
"versionId": "ac7f5d1a-d01d-41c3-bf9d-0e597beb3912",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "bef8d409866a58c0777dfe7cca1b9c2400fd051c056d361501393ab423006b5f"
},
"nodeGroups": [],
"id": "mdqbJvyeoTnw7FUF",
"tags": [
{
"updatedAt": "2025-11-11T04:59:39.416Z",
"createdAt": "2025-11-11T04:59:39.416Z",
"id": "ClOZrAjJKoGLFvaO",
"name": "Palletways"
}
]
}