From 6c529e712018b32c4d3767cc7b1078bafc435f25 Mon Sep 17 00:00:00 2001 From: adminjeroen Date: Fri, 3 Jul 2026 07:42:04 +0200 Subject: [PATCH] Upload files to "palletways" --- palletways/Palletways geboekte orders.json | 855 +++++++++++++++++++++ 1 file changed, 855 insertions(+) create mode 100644 palletways/Palletways geboekte orders.json diff --git a/palletways/Palletways geboekte orders.json b/palletways/Palletways geboekte orders.json new file mode 100644 index 0000000..a4e8a3c --- /dev/null +++ b/palletways/Palletways geboekte orders.json @@ -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, pallet.w, spec.l, spec.w) && pallet.h <= spec.h;\n const weightOk = pallet.kg <= spec.maxWeight;\n\n // grote straffen als het niet past\n let penalty = 0;\n if (!dimsOk) penalty += 1e12;\n if (dimsOk && !weightOk) penalty += 1e6 + (pallet.kg - spec.maxWeight) * 1000;\n\n // strak-fit voorkeur\n const specArea = spec.l * spec.w;\n const palArea = pallet.l * pallet.w;\n const wasteArea = dimsOk ? Math.max(0, (specArea - palArea) / 1000) : 1e9; // schaal\n const heightSlack = dimsOk ? Math.max(0, (spec.h - pallet.h)) : 1e9;\n\n return penalty + wasteArea + heightSlack * 10;\n}\n\nfunction pickBestPalletIndexForType(pool, billType) {\n if (!pool.length) return -1;\n let bestIdx = 0;\n let bestScore = scorePalletForType(pool[0], billType);\n for (let i = 1; i < pool.length; i++) {\n const s = scorePalletForType(pool[i], billType);\n if (s < bestScore) {\n bestScore = s;\n bestIdx = i;\n }\n }\n return bestIdx;\n}\n\nfunction normalizeBillUnits(billUnitsRaw) {\n if (!billUnitsRaw || billUnitsRaw.length === 0) return [];\n return billUnitsRaw\n .map(bu => ({\n type: String(bu?.Type || '').trim(),\n amount: Math.max(1, parseInt(bu?.Amount ?? '1', 10) || 1)\n }))\n .filter(x => x.type);\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 || consignment.Reference || '',\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: '5',\n no: depotData.depot.DepotNumber || '',\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 no: depotData.collection.DepotNumber || '',\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 no: depotData.delivery.DepotNumber || '',\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 address party (partytype_id 2)\nparties.push({\n partytype_id: '2',\n name: deliveryAddress?.CompanyName || '',\n address1: deliveryAddress?.Addr1 || '',\n address2: deliveryAddress?.Addr2 || '',\n zipcode: deliveryAddress?.PostCode || '',\n cityname: deliveryAddress?.Town || '',\n country_id: deliveryAddress?.Country || '',\n contact: deliveryAddress?.ContactName || '',\n phone: deliveryAddress?.Telephone || ''\n});\n\n// ------------------- Create goodslines (NIEUWE LOGICA) -------------------\nconst goodslines = [];\n\nconst palletDetailsRaw = consignment.PalletDetail\n ? (Array.isArray(consignment.PalletDetail) ? consignment.PalletDetail : [consignment.PalletDetail])\n : [];\n\nconst pallets = Array.isArray(consignment.Pallet) ? consignment.Pallet : [consignment.Pallet || ''];\n\nconst billUnitsRaw = consignment.BillUnit\n ? (Array.isArray(consignment.BillUnit) ? consignment.BillUnit : [consignment.BillUnit])\n : [];\n\nconst billUnits = normalizeBillUnits(billUnitsRaw);\n\n// CASE A: BillUnits aanwezig => BillUnit leidend (matchen met palletdetail als die er is)\nif (billUnits.length > 0) {\n const pool = palletDetailsRaw.length > 0 ? buildPalletPool(palletDetailsRaw, pallets) : [];\n\n // 1) Voor elke BillUnit-unit: probeer 1 palletdetail te matchen\n const leftoverByType = {}; // type -> count (units zonder palletdetail)\n\n for (const bu of billUnits) {\n for (let n = 0; n < bu.amount; n++) {\n if (pool.length > 0) {\n const pickIdx = pickBestPalletIndexForType(pool, bu.type);\n const picked = pool.splice(pickIdx, 1)[0];\n\n goodslines.push({\n sequence: goodslines.length + 1,\n unitamount: '1',\n unit_id: bu.type, // <<< BillUnit type is leidend\n weight: picked.raw.Weight || '',\n length: mmToCmString(picked.raw.Length),\n width: mmToCmString(picked.raw.Width),\n height: mmToCmString(picked.raw.Height),\n barcode: picked.barcode || ''\n });\n } else {\n leftoverByType[bu.type] = (leftoverByType[bu.type] || 0) + 1;\n }\n }\n }\n\n // 2) Als er meer BillUnits zijn dan PalletDetails: goodsline(s) zonder afmetingen, wel aantal + type\n for (const [type, count] of Object.entries(leftoverByType)) {\n goodslines.push({\n sequence: goodslines.length + 1,\n unitamount: String(liftsAmount),\n unit_id: type,\n barcode: palletBarcode\n });\n }\n\n // 3) Als er MEER PalletDetails zijn dan BillUnits: alsnog meegeven (fallback classificatie)\n if (pool.length > 0) {\n for (const remaining of pool) {\n const cls = classifyBillingUnit(remaining.raw.Length, remaining.raw.Width, remaining.raw.Height, remaining.raw.Weight);\n goodslines.push({\n sequence: goodslines.length + 1,\n unitamount: '1',\n unit_id: cls.type,\n weight: remaining.raw.Weight || '',\n length: mmToCmString(remaining.raw.Length),\n width: mmToCmString(remaining.raw.Width),\n height: mmToCmString(remaining.raw.Height),\n barcode: remaining.barcode || ''\n });\n }\n }\n\n// CASE B: Geen BillUnits, wel PalletDetails => oude aanpak (classify op basis van palletdetail)\n} else if (palletDetailsRaw.length > 0) {\n palletDetailsRaw.forEach((palletDetail, index) => {\n const cls = classifyBillingUnit(palletDetail.Length, palletDetail.Width, palletDetail.Height, palletDetail.Weight);\n goodslines.push({\n sequence: goodslines.length + 1,\n unitamount: '1',\n unit_id: cls.type,\n weight: palletDetail.Weight || '',\n length: mmToCmString(palletDetail.Length),\n width: mmToCmString(palletDetail.Width),\n height: mmToCmString(palletDetail.Height),\n barcode: pallets[index] || ''\n });\n });\n}\n\n// Build the final XML structure\nconst now = getCurrentDateTime();\nconst xmlData = {\n ediprovider_id: '21',\n company_id: '1',\n customer_id: String(customerIdFromInput),\n edireference: consignment.TrackingID,\n reference: consignment.Reference,\n shipment: {\n edireference: consignment.TrackingID,\n reference: consignment.Reference,\n shipmentkind_id: shipmentKindId,\n sender: {\n name: collectionAddress?.CompanyName || '',\n address1: collectionAddress?.Addr1 || '',\n zipcode: collectionAddress?.PostCode || '',\n city_id: collectionAddress?.Town || '',\n country_id: collectionAddress?.Country || '',\n contact: collectionAddress?.ContactName || '',\n phone: collectionAddress?.Telephone || ''\n },\n receiver: {\n name: deliveryAddress?.CompanyName || '',\n address1: deliveryAddress?.Addr1 || '',\n zipcode: deliveryAddress?.PostCode || '',\n city_id: deliveryAddress?.Town || '',\n country_id: deliveryAddress?.Country || '',\n contact: deliveryAddress?.ContactName || '',\n phone: deliveryAddress?.Telephone || ''\n },\n pickupaddress: {\n ...pickupAddressData,\n date: now.date\n },\n deliveryaddress: {\n ...deliveryAddressData,\n date: now.date // <<< zelfde datum als pickupaddress\n },\n references: [\n { referencekind_id: '15', description: consignment.TrackingID },\n { referencekind_id: '16', description: consignment.Reference }\n ],\n parties: parties,\n cargo: {\n unitamount: String(liftsAmount),\n weight: consignment.Weight || '',\n bool1: consignment.TailLift === 'yes' ? '1' : '0',\n bool2: consignment.BookInRequest === 'yes' ? '1' : '0',\n barcode: palletBarcode,\n goodslines: goodslines\n }\n }\n};\n\nreturn [{ json: xmlData }];\n" + }, + "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, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\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}`;\n }\n\n // content escapen\n return `<${name}${attrStr}>${xmlEsc(content)}`;\n}\n\n// Build XML\nlet xml = '\\n';\nxml += '\\n';\nxml += ` ${createElement('ediprovider_id', data.ediprovider_id, { matchmode: '0' })}\\n`;\nxml += ` ${createElement('company_id', data.company_id, { matchmode: '0' })}\\n`;\nxml += ` \\n`;\nxml += ' \\n';\nxml += ' \\n';\nxml += ' 0\\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 += ' \\n';\nxml += ' \\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 += ' \\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 += ' \\n';\n\n// Receiver\nxml += ' \\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 += ' \\n';\n\n// Pickup address \nxml += ' \\n';\nxml += ' \\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`;\nxml += ` ${createElement('city_id', data.shipment.pickupaddress.city, { matchmode: '4' })}\\n`;\nxml += ` ${createElement('country_id', data.shipment.pickupaddress.country, { matchmode: '2' })}\\n`;\nxml += ` ${createElement('contact', data.shipment.pickupaddress.contact)}\\n`;\nxml += ` ${createElement('phone', data.shipment.pickupaddress.phone)}\\n`;\nxml += ` ${createElement('reference', data.shipment.pickupaddress.reference)}\\n`;\nxml += ` ${createElement('driverinfo', data.shipment.pickupaddress.driverinfo)}\\n`;\nxml += ' \\n';\n\n// Delivery address\nxml += ' \\n';\nxml += ' \\n';\nxml += ` ${createElement('date', data.shipment.deliveryaddress.date)}\\n`;\nxml += ` ${createElement('time', data.shipment.deliveryaddress.time)}\\n`;\nxml += ` ${createElement('datetill', data.shipment.deliveryaddress.datetill)}\\n`;\nxml += ` ${createElement('timetill', data.shipment.deliveryaddress.timetill)}\\n`;\nxml += ` ${createElement('name', data.shipment.deliveryaddress.name)}\\n`;\nxml += ` ${createElement('address1', data.shipment.deliveryaddress.address1)}\\n`;\nxml += ` ${createElement('address2', data.shipment.deliveryaddress.address2)}\\n`;\nxml += ` ${createElement('zipcode', data.shipment.deliveryaddress.zipcode)}\\n`;\nxml += ` ${createElement('city_id', data.shipment.deliveryaddress.city, { matchmode: '4' })}\\n`;\nxml += ` ${createElement('country_id', data.shipment.deliveryaddress.country, { matchmode: '2' })}\\n`;\nxml += ` ${createElement('contact', data.shipment.deliveryaddress.contact)}\\n`;\nxml += ` ${createElement('phone', data.shipment.deliveryaddress.phone)}\\n`;\nxml += ` ${createElement('email', data.shipment.deliveryaddress.email)}\\n`;\nxml += ` ${createElement('reference', data.shipment.deliveryaddress.deliveryreference)}\\n`;\nxml += ` ${createElement('driverinfo', data.shipment.deliveryaddress.driverinfo)}\\n`;\nxml += ' \\n';\n\n// References\nxml += ' \\n';\n(data.shipment.references || []).forEach(ref => {\n xml += ' \\n';\n xml += ` ${createElement('referencekind_id', ref.referencekind_id, { matchmode: '0', autocreate: 'false' })}\\n`;\n xml += ` ${createElement('description', ref.description)}\\n`;\n xml += ' \\n';\n});\nxml += ' \\n';\n\n// Parties\nxml += ' \\n';\n(data.shipment.parties || []).forEach(party => {\n xml += ' \\n';\n xml += ` ${createElement('partytype_id', party.partytype_id, { matchmode: '0' })}\\n`;\n xml += ` ${createElement('no', party.no)}\\n`;\n xml += ` ${createElement('name', party.name)}\\n`;\n xml += ` ${createElement('address1', party.address1)}\\n`;\n xml += ` ${createElement('address2', party.address2)}\\n`;\n xml += ` ${createElement('zipcode', party.zipcode)}\\n`;\n xml += ` ${createElement('country_id', party.country_id, { matchmode: '2' })}\\n`;\n xml += ` ${createElement('cityname', party.cityname)}\\n`;\n xml += ` ${createElement('contact', party.contact)}\\n`;\n xml += ` ${createElement('phone', party.phone)}\\n`;\n xml += ` ${createElement('email', party.email)}\\n`;\n xml += ' \\n';\n});\nxml += ' \\n';\n\n// Cargo\nxml += ' \\n';\nxml += ` ${createElement('unitamount', data.shipment.cargo.unitamount)}\\n`;\nxml += ` ${createElement('weight', data.shipment.cargo.weight)}\\n`;\nxml += ` ${createElement('bool1', data.shipment.cargo.bool1)}\\n`;\nxml += ` ${createElement('bool2', data.shipment.cargo.bool2)}\\n`;\nxml += ` ${createElement('barcode', data.shipment.cargo.barcode)}\\n`;\n\n// Goodslines\nif (data.shipment.cargo.goodslines && data.shipment.cargo.goodslines.length > 0) {\n xml += ' \\n';\n data.shipment.cargo.goodslines.forEach(line => {\n xml += ' \\n';\n xml += ` ${createElement('sequence', line.sequence)}\\n`;\n xml += ` ${createElement('unitamount', line.unitamount)}\\n`;\n xml += ` ${createElement('unit_id', line.unit_id, { matchmode: '1' })}\\n`;\n xml += ` ${createElement('weight', line.weight)}\\n`;\n xml += ` ${createElement('length', line.length)}\\n`;\n xml += ` ${createElement('width', line.width)}\\n`;\n xml += ` ${createElement('height', line.height)}\\n`;\n xml += ` ${createElement('barcode', line.barcode)}\\n`;\n xml += ' \\n';\n });\n xml += ' \\n';\n}\n\nxml += ' \\n';\nxml += ' \\n';\nxml += ' \\n';\nxml += ' \\n';\nxml += ' \\n';\nxml += '';\n\nreturn [{\n json: {\n xml,\n filename: `confirmed_${safeFilename(data.edireference)}.xml`\n }\n}];\n" + }, + "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 address party (partytype_id 2)\nparties.push({\n partytype_id: '2',\n name: deliveryAddress?.CompanyName || '',\n address1: deliveryAddress?.Addr1 || '',\n address2: deliveryAddress?.Addr2 || '',\n zipcode: deliveryAddress?.PostCode || '',\n cityname: deliveryAddress?.Town || '',\n country_id: deliveryAddress?.Country || '',\n contact: deliveryAddress?.ContactName || '',\n phone: deliveryAddress?.Telephone || ''\n});\n\n// Create goodslines\nconst goodslines = [];\nif (consignment.PalletDetail) {\n const palletDetails = Array.isArray(consignment.PalletDetail) ? consignment.PalletDetail : [consignment.PalletDetail];\n const pallets = Array.isArray(consignment.Pallet) ? consignment.Pallet : [consignment.Pallet || ''];\n\n palletDetails.forEach((palletDetail, index) => {\n goodslines.push({\n sequence: index + 1,\n unitamount: '1',\n unit_id: 'pallet',\n weight: palletDetail.Weight || '',\n length: palletDetail.Length || '',\n width: palletDetail.Width || '',\n height: palletDetail.Height || '',\n barcode: pallets[index] || ''\n });\n });\n} else if (consignment.BillUnit) {\n const billUnits = Array.isArray(consignment.BillUnit) ? consignment.BillUnit : [consignment.BillUnit];\n billUnits.forEach((billUnit, index) => {\n goodslines.push({\n sequence: index + 1,\n unitamount: billUnit.Amount || '1',\n unit_id: billUnit.Type || '',\n barcode: palletBarcode\n });\n });\n}\n\n// Build the final XML structure\nconst now = getCurrentDateTime();\nconst xmlData = {\n ediprovider_id: '21',\n company_id: '1',\n customer_id: String(customerIdFromInput), // <<< HIER vervangen\n edireference: consignment.TrackingID,\n reference: consignment.TrackingID,\n shipment: {\n edireference: consignment.TrackingID,\n reference: consignment.TrackingID,\n shipmentkind_id: shipmentKindId,\n sender: {\n name: collectionAddress?.CompanyName || '',\n address1: collectionAddress?.Addr1 || '',\n zipcode: collectionAddress?.PostCode || '',\n city_id: collectionAddress?.Town || '',\n country_id: collectionAddress?.Country || '',\n contact: collectionAddress?.ContactName || '',\n phone: collectionAddress?.Telephone || ''\n },\n receiver: {\n name: deliveryAddress?.CompanyName || '',\n address1: deliveryAddress?.Addr1 || '',\n zipcode: deliveryAddress?.PostCode || '',\n city_id: deliveryAddress?.Town || '',\n country_id: deliveryAddress?.Country || '',\n contact: deliveryAddress?.ContactName || '',\n phone: deliveryAddress?.Telephone || ''\n },\n pickupaddress: {\n ...pickupAddressData,\n date: now.date\n //time: now.time\n },\n deliveryaddress: {\n ...deliveryAddressData\n },\n references: [\n { referencekind_id: '16', description: consignment.TrackingID },\n { referencekind_id: '15', description: consignment.Reference }\n ],\n parties: parties,\n cargo: {\n unitamount: consignment.Lifts || '1',\n weight: consignment.Weight || '',\n bool1: consignment.TailLift === 'yes' ? '1' : '0',\n bool2: consignment.BookInRequest === 'yes' ? '1' : '0',\n barcode: palletBarcode,\n goodslines: goodslines\n }\n }\n};\n\nreturn [{ json: xmlData }];\n" + }, + "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" + } + ] +} \ No newline at end of file