Files
n8n-workflows/palletways/Palletways geboekte orders.json

855 lines
61 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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, '&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`;\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 += ' </pickupaddress>\\n';\n\n// Delivery address\nxml += ' <deliveryaddress>\\n';\nxml += ' <address_id matchmode=\"5\"></address_id>\\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 += ' </deliveryaddress>\\n';\n\n// References\nxml += ' <references>\\n';\n(data.shipment.references || []).forEach(ref => {\n xml += ' <reference>\\n';\n xml += ` ${createElement('referencekind_id', ref.referencekind_id, { matchmode: '0', autocreate: 'false' })}\\n`;\n xml += ` ${createElement('description', ref.description)}\\n`;\n xml += ' </reference>\\n';\n});\nxml += ' </references>\\n';\n\n// Parties\nxml += ' <parties>\\n';\n(data.shipment.parties || []).forEach(party => {\n xml += ' <party>\\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 += ' </party>\\n';\n});\nxml += ' </parties>\\n';\n\n// Cargo\nxml += ' <cargo>\\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 += ' <goodslines>\\n';\n data.shipment.cargo.goodslines.forEach(line => {\n xml += ' <goodsline>\\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 += ' </goodsline>\\n';\n });\n xml += ' </goodslines>\\n';\n}\n\nxml += ' </cargo>\\n';\nxml += ' </shipment>\\n';\nxml += ' </shipments>\\n';\nxml += ' </transportbooking>\\n';\nxml += ' </transportbookings>\\n';\nxml += '</import>';\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"
}
]
}