diff --git a/palletways/Palletways Notes import --_ TLN.json b/palletways/Palletways Notes import --_ TLN.json new file mode 100644 index 0000000..8a52c49 --- /dev/null +++ b/palletways/Palletways Notes import --_ TLN.json @@ -0,0 +1,994 @@ +{ + "name": "Palletways Notes import --> TLN", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "minutes" + } + ] + } + }, + "id": "40267cf4-e7fe-4d0b-b4f1-d476fd66f603", + "name": "Schedule Trigger", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.1, + "position": [ + -864, + 304 + ] + }, + { + "parameters": { + "options": { + "explicitArray": false, + "mergeAttrs": true + } + }, + "id": "86168699-6d8f-479f-bfd0-9532e3ddae53", + "name": "Parse XML Response", + "type": "n8n-nodes-base.xml", + "typeVersion": 1, + "position": [ + 32, + 400 + ] + }, + { + "parameters": { + "jsCode": "// n8n Code node (Run once for all items)\n// Split Response.Detail.Data naar losse items\n// Werkt met:\n// - Data als array\n// - Data als enkel object\n// - lege/missende Data\n\nconst input = $input.first().json;\n\n// Pak root netjes\nconst root = Array.isArray(input) ? (input[0] ?? {}) : input;\nconst response = root.Response ?? root;\nconst status = response.Status ?? {};\n\nlet data = response?.Detail?.Data;\n\n// Geen data\nif (!data) {\n return [];\n}\n\n// Als er maar 1 record is, maak er alsnog een array van\nif (!Array.isArray(data)) {\n data = [data];\n}\n\n// Maak 1 n8n item per record\nreturn data.map((row, index) => ({\n json: {\n ...row,\n _status: status,\n _index: index,\n },\n}));" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 512, + 384 + ], + "id": "564a970e-65b2-45a3-b334-c795e80f3553", + "name": "Split 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": [ + -416, + 304 + ], + "id": "aa5f5031-5c40-48ee-964d-0376a0960e22", + "name": "Parse Last Timestamp" + }, + { + "parameters": { + "operation": "get", + "key": "notes_lastrun_timestamp", + "options": {} + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + -640, + 304 + ], + "id": "e04add49-6606-4ff1-8a29-cc48e10fa0f8", + "name": "Get Last Timestamp from Redis", + "credentials": { + "redis": { + "id": "uPJ4dFmf6tm25qHY", + "name": "Redis account 2" + } + }, + "continueOnFail": true + }, + { + "parameters": { + "operation": "set", + "key": "notes_lastrun_timestamp", + "value": "={{ $json.redisValue }}" + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 32, + 208 + ], + "id": "4e70a238-4473-4f3f-842c-4b89e0ad4708", + "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": [ + -192, + 208 + ], + "id": "a58be23e-a8b6-4c0f-b170-631277ec16e1", + "name": "Prepare Timestamp Data" + }, + { + "parameters": { + "content": "## Timezone staat ingesteld op London", + "height": 96, + "width": 336 + }, + "type": "n8n-nodes-base.stickyNote", + "position": [ + -848, + 144 + ], + "typeVersion": 1, + "id": "28aac797-03af-4f71-9f29-7a860ce2a66b", + "name": "Sticky Note" + }, + { + "parameters": { + "url": "=https://api.palletways.com/getnotes/daterange/{{ $json.lastRunDate }}/{{ $json.lastRunTime }}/{{ $json.currentDate }}/{{ $json.currentTime }}/?apikey=SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D", + "options": { + "response": { + "response": { + "responseFormat": "text" + } + }, + "timeout": 30000 + } + }, + "id": "32fccfa6-3482-4da0-8e9f-4dd9f7ccbadf", + "name": "Get Palletways Notes", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [ + -192, + 400 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "5e894f03-9d8a-4f32-a43e-e0a5170d23ba", + "leftValue": "={{ $json.PayingDepot }}", + "rightValue": "464", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.filter", + "typeVersion": 2.3, + "position": [ + 752, + 384 + ], + "id": "c2cfb535-ae39-4b77-83d6-580ce92d8894", + "name": "Filter Paying depot" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ $json.NoteCodeID }}", + "rightValue": "67999", + "operator": { + "type": "string", + "operation": "equals" + }, + "id": "9c23662b-e4d3-4531-a35b-ae6e6a4f0a99" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CMR" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "575c640a-12ca-4d9d-9398-70a5ce8e437b", + "leftValue": "={{ $json.NoteCodeID }}", + "rightValue": "32", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Book-in" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "bdf4413b-2759-4909-b1cf-90e58fc8b42d", + "leftValue": "={{ $json.NoteCodeID }}", + "rightValue": "219999", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Afgehaald" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "c2c1f674-77bb-44a7-b5d1-8c3570b45abe", + "leftValue": "={{ $json.NoteCodeID }}", + "rightValue": "36", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Afgeleverd" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "bdd8c8ed-546d-4ccc-9e38-a4ebe7f4e235", + "leftValue": "={{ $json.NoteCodeID }}", + "rightValue": "218999", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "ETA" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "563f1487-1d9d-45d6-91e0-6bbe03f9761e", + "leftValue": "={{ $json.NoteCodeID }}", + "rightValue": "49001", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Bij Afleverdepot" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "c8b65b34-6be9-4504-9f31-043dbbaae464", + "leftValue": "={{ $json.NoteCodeID }}", + "rightValue": "348999", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "geladen depot" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "480b4d6f-0d6f-458a-9152-6bae2d5e8757", + "leftValue": "={{ $json.NoteCodeID }}", + "rightValue": "48001", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Out for delivery" + } + ] + }, + "options": { + "fallbackOutput": "extra" + } + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + 976, + 320 + ], + "id": "d689f805-a258-4a1a-9776-6a4762a563f2", + "name": "Switch" + }, + { + "parameters": { + "url": "=https://portal.palletways.com/show_pod.php?fullsize=true&file={{ $json.NoteText }}", + "options": { + "response": { + "response": { + "responseFormat": "file" + } + } + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 1680, + 64 + ], + "id": "0c00fd51-6018-4a56-b4d0-39a8b430238e", + "name": "Download CMR" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "9cca43eb-c3b7-49c7-a927-3fd6a406e43d", + "leftValue": "={{ $('Parse XML Response').item.json.Response.Status.Code }}", + "rightValue": "OK", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 256, + 400 + ], + "id": "e8a7eae7-1398-4a7b-8697-476d9e1f1f48", + "name": "Check for notes" + }, + { + "parameters": { + "jsCode": "// n8n Code node (Run once for all items)\n// Split Response.Detail.Data[] naar losse items\n\nconst input = $input.first().json;\n\n// jouw response komt als [ { Response: ... } ] -> pak netjes de juiste root\nconst root = Array.isArray(input) ? (input[0] ?? {}) : input;\n\nconst response = root.Response ?? root;\nconst status = response.Status ?? {};\nconst data = response?.Detail?.Data;\n\nif (!Array.isArray(data)) {\n throw new Error('Kan Response.Detail.Data (array) niet vinden in de input.');\n}\n\n// Maak 1 n8n item per record in Data[]\nreturn data.map((row, index) => ({\n json: {\n ...row,\n // handig: status info meenemen (mag je weghalen als je dit niet wilt)\n _status: status,\n _index: index,\n },\n}));" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -416, + 96 + ], + "id": "a6f825a7-aff3-48f3-b248-1f4925ae9620", + "name": "Split Manifests1" + }, + { + "parameters": { + "operation": "upload", + "path": "=/prod/ToTP/TLN/{{ String($json.consignmentNumber).replace(/[^A-Za-z0-9._-]/g, '_') }}_{{ $now.toFormat('yyyyMMddHHmmssSSS') }}_{{ $execution.id }}_{{ $itemIndex }}.xml", + "binaryData": false, + "fileContent": "={{ $json.xml }}", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + 2640, + 496 + ], + "id": "0c0d86af-f0ff-413a-8d6b-e28158bf657c", + "name": "FTP2", + "credentials": { + "ftp": { + "id": "oLAZ4OgmkOopMHAq", + "name": "FTP De Wit Transport" + } + } + }, + { + "parameters": { + "jsCode": "// n8n Code node\n// Zet op: Run once for all items\n\nconst MAP = {\n id: 'TrackingID',\n status: 'NoteCodeID', // of 'StatusCode' als je die wilt gebruiken\n date: 'NoteDate',\n time: 'NoteTime',\n note: 'NoteText',\n etaDate: 'BookInDate',\n etaTime: 'BookInTime',\n carrier: '', // bv. 'Carrier', 'Creditor', etc. Leeg laten als je hem niet uit input wilt halen\n};\n\nconst carrierFixed = '1024680'; // bv. '1024680' om hard te zetten. Leeg laten als je hem niet wilt meesturen\n\nconst xmlEsc = (s) =>\n String(s ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst first = (...vals) =>\n vals.find(v => v !== undefined && v !== null && String(v).trim() !== '');\n\nfunction toAT(dateStr, timeStr = '') {\n const d = String(dateStr ?? '').trim();\n const t = String(timeStr ?? '').trim();\n\n // YYYY-MM-DD\n let m = d.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (m) {\n const [, year, month, day] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n // DD/MM/YYYY\n m = d.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);\n if (m) {\n const [, day, month, year] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n throw new Error(`Ongeldige datum: \"${d}\"`);\n}\n\nconst items = $input.all();\n\nreturn items.map((item, index) => {\n const row = item.json ?? {};\n\n const trackingId = first(row[MAP.id]);\n const status = first(row[MAP.status]);\n const date = first(row[MAP.date]);\n const time = first(row[MAP.time]);\n const note = first(row[MAP.note], '');\n const carrier = first(\n carrierFixed,\n MAP.carrier ? row[MAP.carrier] : '',\n ''\n );\n\n if (!trackingId) {\n throw new Error(`Item ${index + 1}: ${MAP.id} ontbreekt`);\n }\n\n if (!status) {\n throw new Error(`Item ${index + 1}: ${MAP.status} ontbreekt`);\n }\n\n if (!date) {\n throw new Error(`Item ${index + 1}: ${MAP.date} ontbreekt`);\n }\n\n const atDateTime = toAT(date, time);\n\n const etaDate = first(row[MAP.etaDate], '');\n const etaTime = first(row[MAP.etaTime], '');\n\n let etaAT = '';\n if (etaDate) {\n etaAT = toAT(etaDate, etaTime);\n }\n\n const carrierXml = carrier\n ? `\\n ${xmlEsc(carrier)}`\n : '';\n\n const noteXml = note\n ? `\\n ${xmlEsc(note)}`\n : '';\n\n const etaXml = etaAT\n ? `\\n ${xmlEsc(etaAT)}`\n : '';\n\n const xml = `\n\n ${xmlEsc(atDateTime)}\n TLN\n \n ${xmlEsc(trackingId)}\n ${xmlEsc(status)}\n ${xmlEsc(atDateTime)}${carrierXml}${noteXml}${etaXml}\n \n`;\n\n const fileNameBase = `${trackingId}_cmr`;\n const fileName = `${fileNameBase}.xml`;\n\n return {\n json: {\n ...row,\n trackingId,\n consignmentNumber: trackingId,\n mappedStatus: status,\n atDateTime,\n etaAT,\n carrier,\n fileNameBase,\n fileName,\n xml,\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1680, + 208 + ], + "id": "5b6bbc7d-3948-48c5-8335-d9483bff46ba", + "name": "Create bookin xml" + }, + { + "parameters": { + "jsCode": "// n8n Code node\n// Zet op: Run once for all items\n\nconst MAP = {\n id: 'TrackingID',\n status: 'NoteCodeID', // of 'StatusCode' als je die wilt gebruiken\n date: 'NoteDate',\n time: 'NoteTime',\n note: 'NoteText',\n etaDate: 'BookInDate',\n etaTime: 'BookInTime',\n carrier: '', // bv. 'Carrier', 'Creditor', etc. Leeg laten als je hem niet uit input wilt halen\n};\n\nconst carrierFixed = '1024680'; // bv. '1024680' om hard te zetten. Leeg laten als je hem niet wilt meesturen\n\nconst xmlEsc = (s) =>\n String(s ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst first = (...vals) =>\n vals.find(v => v !== undefined && v !== null && String(v).trim() !== '');\n\nfunction toAT(dateStr, timeStr = '') {\n const d = String(dateStr ?? '').trim();\n const t = String(timeStr ?? '').trim();\n\n // YYYY-MM-DD\n let m = d.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (m) {\n const [, year, month, day] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n // DD/MM/YYYY\n m = d.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);\n if (m) {\n const [, day, month, year] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n throw new Error(`Ongeldige datum: \"${d}\"`);\n}\n\nconst items = $input.all();\n\nreturn items.map((item, index) => {\n const row = item.json ?? {};\n\n const consignmentNumber = first(row[MAP.id]);\n const status = first(row[MAP.status]);\n const date = first(row[MAP.date]);\n const time = first(row[MAP.time]);\n const note = first(row[MAP.note], '');\n const carrier = first(\n carrierFixed,\n MAP.carrier ? row[MAP.carrier] : '',\n ''\n );\n\n if (!consignmentNumber) {\n throw new Error(`Item ${index + 1}: ${MAP.id} ontbreekt`);\n }\n\n if (!status) {\n throw new Error(`Item ${index + 1}: ${MAP.status} ontbreekt`);\n }\n\n if (!date) {\n throw new Error(`Item ${index + 1}: ${MAP.date} ontbreekt`);\n }\n\n const atDateTime = toAT(date, time);\n\n // ETA uit losse datum + tijd\n const etaDate = first(row[MAP.etaDate], '');\n const etaTime = first(row[MAP.etaTime], '');\n\n let etaAT = '';\n if (etaDate) {\n etaAT = toAT(etaDate, etaTime);\n }\n\n const carrierXml = carrier\n ? `\\n ${xmlEsc(carrier)}`\n : '';\n\n const noteXml = note\n ? `\\n ${xmlEsc(note)}`\n : '';\n\n const etaXml = etaAT\n ? `\\n ${xmlEsc(etaAT)}`\n : '';\n\n const xml = `\n\n ${xmlEsc(atDateTime)}\n TLN\n \n ${xmlEsc(consignmentNumber)}\n ${xmlEsc(status)}\n ${xmlEsc(atDateTime)}${carrierXml}${noteXml}${etaXml}\n \n`;\n\n return {\n json: {\n ...row,\n consignmentNumber,\n mappedStatus: status,\n atDateTime,\n etaAT,\n carrier,\n xml,\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1680, + 368 + ], + "id": "2604c649-d725-4005-8879-d9a135c8f521", + "name": "Create POC XML" + }, + { + "parameters": { + "jsCode": "// n8n Code node\n// Zet op: Run once for all items\n\nconst MAP = {\n id: 'TrackingID',\n status: 'NoteCodeID', // of 'StatusCode'\n date: 'NoteDate',\n time: 'NoteTime',\n note: 'NoteText',\n etaDate: 'BookInDate',\n etaTime: 'BookInTime',\n signedBy: 'PodSignature', // naam van degene die tekende\n carrier: '', // bv. 'Carrier', 'Creditor', etc. Leeg laten als je hem niet uit input wilt halen\n};\n\nconst carrierFixed = '1024680'; // bv. '1024680' om hard te zetten. Leeg laten als je hem niet wilt meesturen\n\nconst xmlEsc = (s) =>\n String(s ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst first = (...vals) =>\n vals.find(v => v !== undefined && v !== null && String(v).trim() !== '');\n\nfunction toAT(dateStr, timeStr = '') {\n const d = String(dateStr ?? '').trim();\n const t = String(timeStr ?? '').trim();\n\n let m = d.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (m) {\n const [, year, month, day] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n m = d.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);\n if (m) {\n const [, day, month, year] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n throw new Error(`Ongeldige datum: \"${d}\"`);\n}\n\nconst items = $input.all();\n\nreturn items.map((item, index) => {\n const row = item.json ?? {};\n\n const consignmentNumber = first(row[MAP.id]);\n const status = first(row[MAP.status]);\n const date = first(row[MAP.date]);\n const time = first(row[MAP.time]);\n const note = first(row[MAP.note], '');\n const signedBy = first(row[MAP.signedBy], '');\n const carrier = first(\n carrierFixed,\n MAP.carrier ? row[MAP.carrier] : '',\n ''\n );\n\n if (!consignmentNumber) {\n throw new Error(`Item ${index + 1}: ${MAP.id} ontbreekt`);\n }\n\n if (!status) {\n throw new Error(`Item ${index + 1}: ${MAP.status} ontbreekt`);\n }\n\n if (!date) {\n throw new Error(`Item ${index + 1}: ${MAP.date} ontbreekt`);\n }\n\n const atDateTime = toAT(date, time);\n\n const etaDate = first(row[MAP.etaDate], '');\n const etaTime = first(row[MAP.etaTime], '');\n\n let etaAT = '';\n if (etaDate) {\n etaAT = toAT(etaDate, etaTime);\n }\n\n const carrierXml = carrier\n ? `\\n ${xmlEsc(carrier)}`\n : '';\n\n const noteXml = note\n ? `\\n ${xmlEsc(note)}`\n : '';\n\n const etaXml = etaAT\n ? `\\n ${xmlEsc(etaAT)}`\n : '';\n\n const nameXml = signedBy\n ? `\\n ${xmlEsc(signedBy)}`\n : '';\n\n const xml = `\n\n ${xmlEsc(atDateTime)}\n TLN\n \n ${xmlEsc(consignmentNumber)}\n ${xmlEsc(status)}\n ${xmlEsc(atDateTime)}${carrierXml}${noteXml}${etaXml}${nameXml}\n \n`;\n\n return {\n json: {\n ...row,\n consignmentNumber,\n mappedStatus: status,\n atDateTime,\n etaAT,\n signedBy,\n carrier,\n xml,\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1680, + 528 + ], + "id": "39c97c8d-ef34-4319-8a9c-a4caf8c77a78", + "name": "Afgeleverd" + }, + { + "parameters": { + "jsCode": "// n8n Code node\n// Zet op: Run once for all items\n\nconst MAP = {\n id: 'TrackingID',\n status: 'NoteCodeID', // of 'StatusCode' als je die wilt gebruiken\n date: 'NoteDate',\n time: 'NoteTime',\n note: 'NoteText',\n etaDate: 'ETADate',\n etaTime: 'ETATime',\n carrier: '', // bv. 'Carrier', 'Creditor', etc. Leeg laten als je hem niet uit input wilt halen\n};\n\nconst carrierFixed = '1024680'; // bv. '1024680' om hard te zetten. Leeg laten als je hem niet wilt meesturen\n\nconst xmlEsc = (s) =>\n String(s ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst first = (...vals) =>\n vals.find(v => v !== undefined && v !== null && String(v).trim() !== '');\n\nfunction toAT(dateStr, timeStr = '') {\n const d = String(dateStr ?? '').trim();\n const t = String(timeStr ?? '').trim();\n\n // YYYY-MM-DD\n let m = d.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (m) {\n const [, year, month, day] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n // DD/MM/YYYY\n m = d.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);\n if (m) {\n const [, day, month, year] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n throw new Error(`Ongeldige datum: \"${d}\"`);\n}\n\nconst items = $input.all();\n\nreturn items.map((item, index) => {\n const row = item.json ?? {};\n\n const consignmentNumber = first(row[MAP.id]);\n const status = first(row[MAP.status]);\n const date = first(row[MAP.date]);\n const time = first(row[MAP.time]);\n const note = first(row[MAP.note], '');\n const carrier = first(\n carrierFixed,\n MAP.carrier ? row[MAP.carrier] : '',\n ''\n );\n\n if (!consignmentNumber) {\n throw new Error(`Item ${index + 1}: ${MAP.id} ontbreekt`);\n }\n\n if (!status) {\n throw new Error(`Item ${index + 1}: ${MAP.status} ontbreekt`);\n }\n\n if (!date) {\n throw new Error(`Item ${index + 1}: ${MAP.date} ontbreekt`);\n }\n\n const atDateTime = toAT(date, time);\n\n const etaDate = first(row[MAP.etaDate], '');\n const etaTime = first(row[MAP.etaTime], '');\n\n let etaAT = '';\n if (etaDate) {\n etaAT = toAT(etaDate, etaTime);\n }\n\n const carrierXml = carrier\n ? `\\n ${xmlEsc(carrier)}`\n : '';\n\n const noteXml = note\n ? `\\n ${xmlEsc(note)}`\n : '';\n\n const etaXml = etaAT\n ? `\\n ${xmlEsc(etaAT)}`\n : '';\n\n const xml = `\n\n ${xmlEsc(atDateTime)}\n TLN\n \n ${xmlEsc(consignmentNumber)}\n ${xmlEsc(status)}\n ${xmlEsc(atDateTime)}${carrierXml}${noteXml}${etaXml}\n \n`;\n\n return {\n json: {\n ...row,\n consignmentNumber,\n mappedStatus: status,\n atDateTime,\n etaAT,\n carrier,\n xml,\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1680, + 704 + ], + "id": "208500b0-8b64-4567-9143-5089d37d737a", + "name": "Create ETA XML" + }, + { + "parameters": { + "jsCode": "// n8n Code node\n// Zet op: Run once for all items\n\nconst MAP = {\n id: 'TrackingID',\n status: 'StatusCode', // of 'StatusCode'\n date: 'NoteDate',\n time: 'NoteTime',\n note: 'NoteText',\n etaDate: 'BookInDate',\n etaTime: 'BookInTime',\n signedBy: 'PodSignature', // naam van degene die tekende\n carrier: '', // hier kun je een veldnaam zetten\n};\n\nconst carrierFixed = '1024680'; // vul hier bv '1024680' in om hard te zetten\n\nconst xmlEsc = (s) =>\n String(s ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst first = (...vals) =>\n vals.find(v => v !== undefined && v !== null && String(v).trim() !== '');\n\nfunction toAT(dateStr, timeStr = '') {\n const d = String(dateStr ?? '').trim();\n const t = String(timeStr ?? '').trim();\n\n let m = d.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (m) {\n const [, year, month, day] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n m = d.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);\n if (m) {\n const [, day, month, year] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n throw new Error(`Ongeldige datum: \"${d}\"`);\n}\n\nconst items = $input.all();\n\nreturn items.map((item, index) => {\n const row = item.json ?? {};\n\n const consignmentNumber = first(row[MAP.id]);\n const status = first(row[MAP.status]);\n const date = first(row[MAP.date]);\n const time = first(row[MAP.time]);\n const note = first(row[MAP.note], '');\n const signedBy = first(row[MAP.signedBy], '');\n const carrier = first(carrierFixed, row[MAP.carrier], '');\n\n if (!consignmentNumber) {\n throw new Error(`Item ${index + 1}: ${MAP.id} ontbreekt`);\n }\n\n if (!status) {\n throw new Error(`Item ${index + 1}: ${MAP.status} ontbreekt`);\n }\n\n if (!date) {\n throw new Error(`Item ${index + 1}: ${MAP.date} ontbreekt`);\n }\n\n const atDateTime = toAT(date, time);\n\n const etaDate = first(row[MAP.etaDate], '');\n const etaTime = first(row[MAP.etaTime], '');\n\n let etaAT = '';\n if (etaDate) {\n etaAT = toAT(etaDate, etaTime);\n }\n\n const noteXml = note\n ? `\\n ${xmlEsc(note)}`\n : '';\n\n const carrierXml = carrier\n ? `\\n ${xmlEsc(carrier)}`\n : '';\n\n const etaXml = etaAT\n ? `\\n ${xmlEsc(etaAT)}`\n : '';\n\n const nameXml = signedBy\n ? `\\n ${xmlEsc(signedBy)}`\n : '';\n\n const xml = `\n\n ${xmlEsc(atDateTime)}\n TLN\n \n ${xmlEsc(consignmentNumber)}\n ${xmlEsc(status)}\n ${xmlEsc(atDateTime)}${carrierXml}${noteXml}${etaXml}${nameXml}\n \n`;\n\n return {\n json: {\n ...row,\n consignmentNumber,\n mappedStatus: status,\n atDateTime,\n etaAT,\n signedBy,\n carrier,\n xml,\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1680, + 864 + ], + "id": "8431b66e-d4b6-4d85-b6e1-9bdc5ddb46ac", + "name": "Delivery depot XML" + }, + { + "parameters": { + "jsCode": "const { PDFDocument } = require('pdf-lib');\n\nconst items = $input.all();\nconst BIN_PROP = 'data';\n\nfunction replaceExtWithPdf(name) {\n const fileName = String(name || 'document');\n return /\\.[^.]+$/.test(fileName)\n ? fileName.replace(/\\.[^.]+$/, '') + '.pdf'\n : fileName + '.pdf';\n}\n\nfunction basename(path) {\n return String(path || '').split('/').pop();\n}\n\nconst out = [];\n\nfor (let i = 0; i < items.length; i++) {\n const item = items[i];\n const bin = item.binary?.[BIN_PROP];\n if (!bin) continue;\n\n const originalName =\n bin.fileName ||\n item.json?.filename ||\n basename(item.json?.path) ||\n `file_${i + 1}`;\n\n const lowerName = originalName.toLowerCase();\n const mimeType = String(bin.mimeType || '').toLowerCase();\n\n const isPdf =\n mimeType === 'application/pdf' || lowerName.endsWith('.pdf');\n\n const isJpg =\n mimeType === 'image/jpeg' ||\n mimeType === 'image/jpg' ||\n lowerName.endsWith('.jpg') ||\n lowerName.endsWith('.jpeg');\n\n const isPng =\n mimeType === 'image/png' || lowerName.endsWith('.png');\n\n const isTif =\n mimeType === 'image/tiff' ||\n mimeType === 'image/tif' ||\n lowerName.endsWith('.tif') ||\n lowerName.endsWith('.tiff');\n\n // tif overslaan\n if (isTif) {\n continue;\n }\n\n // pdf ongewijzigd doorgeven\n if (isPdf) {\n out.push({\n json: {\n ...item.json,\n filename: originalName,\n },\n binary: {\n [BIN_PROP]: bin,\n },\n });\n continue;\n }\n\n // alleen jpg/png omzetten\n if (!isJpg && !isPng) {\n continue;\n }\n\n try {\n const buf = await this.helpers.getBinaryDataBuffer(i, BIN_PROP);\n\n if (!buf || !buf.length) {\n throw new Error('Leeg bestand');\n }\n\n const pdfDoc = await PDFDocument.create();\n\n let image;\n if (isJpg) {\n image = await pdfDoc.embedJpg(buf);\n } else {\n image = await pdfDoc.embedPng(buf);\n }\n\n // Gebruik size() uit pdf-lib\n const size = image.size();\n const width = Number(size.width);\n const height = Number(size.height);\n\n if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {\n throw new Error(`Ongeldige afbeeldingsgrootte: width=${width}, height=${height}`);\n }\n\n const page = pdfDoc.addPage();\n page.setSize(width, height);\n\n page.drawImage(image, {\n x: 0,\n y: 0,\n width,\n height,\n });\n\n const pdfBytes = await pdfDoc.save();\n const outputFileName = replaceExtWithPdf(originalName);\n\n const outBin = await this.helpers.prepareBinaryData(\n Buffer.from(pdfBytes),\n outputFileName,\n 'application/pdf'\n );\n\n out.push({\n json: {\n ...item.json,\n filename: outputFileName,\n converted: true,\n },\n binary: {\n [BIN_PROP]: outBin,\n },\n });\n } catch (err) {\n out.push({\n json: {\n ...item.json,\n filename: originalName,\n converted: false,\n error: err.message || String(err),\n },\n });\n }\n}\n\nreturn out;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1872, + 64 + ], + "id": "e5791874-cc39-45f3-81c9-da2c84f62738", + "name": "image to pdf1" + }, + { + "parameters": { + "jsCode": "// n8n Code node\n// Zet op: Run once for all items\n\nconst MAP = {\n id: 'TrackingID',\n status: 'NoteCodeID', // of 'StatusCode'\n date: 'NoteDate',\n time: 'NoteTime',\n note: 'NoteText',\n etaDate: 'BookInDate',\n etaTime: 'BookInTime',\n signedBy: 'PodSignature',\n carrier: '',\n};\n\nconst BIN_PROP = 'data';\nconst carrierFixed = '1024680';\n\nconst xmlEsc = (s) =>\n String(s ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst first = (...vals) =>\n vals.find(v => v !== undefined && v !== null && String(v).trim() !== '');\n\nfunction toAT(dateStr, timeStr = '') {\n const d = String(dateStr ?? '').trim();\n const t = String(timeStr ?? '').trim();\n\n let m = d.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (m) {\n const [, year, month, day] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n m = d.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);\n if (m) {\n const [, day, month, year] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n throw new Error(`Ongeldige datum: \"${d}\"`);\n}\n\nconst items = $input.all();\nconst out = [];\n\nfor (let index = 0; index < items.length; index++) {\n const item = items[index];\n const row = item.json ?? {};\n\n const trackingId = first(row[MAP.id]);\n const status = first(row[MAP.status]);\n const date = first(row[MAP.date]);\n const time = first(row[MAP.time]);\n const note = first(row[MAP.note], '');\n const signedBy = first(row[MAP.signedBy], '');\n const carrier = first(carrierFixed, row[MAP.carrier], '');\n\n if (!trackingId) {\n throw new Error(`Item ${index + 1}: ${MAP.id} ontbreekt`);\n }\n\n if (!status) {\n throw new Error(`Item ${index + 1}: ${MAP.status} ontbreekt`);\n }\n\n if (!date) {\n throw new Error(`Item ${index + 1}: ${MAP.date} ontbreekt`);\n }\n\n const atDateTime = toAT(date, time);\n\n const etaDate = first(row[MAP.etaDate], '');\n const etaTime = first(row[MAP.etaTime], '');\n\n let etaAT = '';\n if (etaDate) {\n etaAT = toAT(etaDate, etaTime);\n }\n\n const noteXml = note\n ? `\\n ${xmlEsc(note)}`\n : '';\n\n const carrierXml = carrier\n ? `\\n ${xmlEsc(carrier)}`\n : '';\n\n const etaXml = etaAT\n ? `\\n ${xmlEsc(etaAT)}`\n : '';\n\n const nameXml = signedBy\n ? `\\n ${xmlEsc(signedBy)}`\n : '';\n\n // Bijlage als base64 toevoegen\n let signatureXml = '';\n\n const bin = item.binary?.[BIN_PROP];\n\n if (bin) {\n const buffer = await this.helpers.getBinaryDataBuffer(index, BIN_PROP);\n\n if (buffer && buffer.length > 0) {\n const base64 = buffer.toString('base64');\n\n const fileName = bin.fileName || row.filename || `${trackingId}_cmr.pdf`;\n const mimeType = String(bin.mimeType || '').toLowerCase();\n\n let fileType = 'pdf';\n\n if (mimeType.includes('jpeg') || fileName.toLowerCase().endsWith('.jpg') || fileName.toLowerCase().endsWith('.jpeg')) {\n fileType = 'jpg';\n } else if (mimeType.includes('png') || fileName.toLowerCase().endsWith('.png')) {\n fileType = 'png';\n } else {\n fileType = 'pdf';\n }\n\n signatureXml =\n `\\n ${base64}`;\n }\n }\n\n const xml = `\n\n ${xmlEsc(atDateTime)}\n TLN\n \n ${xmlEsc(trackingId)}\n ${xmlEsc(status)}\n ${xmlEsc(atDateTime)}${carrierXml}${noteXml}${etaXml}${nameXml}${signatureXml}\n \n`;\n\n const fileNameBase = `${trackingId}_cmr`;\n const fileName = `${fileNameBase}.xml`;\n\n out.push({\n json: {\n ...row,\n trackingId,\n consignmentNumber: trackingId,\n mappedStatus: status,\n atDateTime,\n etaAT,\n signedBy,\n carrier,\n hasAttachment: !!bin,\n attachmentAddedAsBase64: !!signatureXml,\n fileNameBase,\n fileName,\n xml,\n },\n });\n}\n\nreturn out;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2064, + 64 + ], + "id": "117d36f5-0f46-4dfc-b3b7-ca0842155b5f", + "name": "Create XML1" + }, + { + "parameters": { + "url": "=https://portal.palletways.com/show_pod.php?fullsize=true&file={{ $json.NoteText }}", + "options": { + "response": { + "response": { + "responseFormat": "file" + } + } + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 1680, + -64 + ], + "id": "682278be-158a-4bf2-ba73-bf0aba55baf4", + "name": "Download CMR1" + }, + { + "parameters": { + "jsCode": "const { PDFDocument } = require('pdf-lib');\n\nconst items = $input.all();\nconst BIN_PROP = 'data';\n\nfunction replaceExtWithPdf(name) {\n const fileName = String(name || 'document');\n return /\\.[^.]+$/.test(fileName)\n ? fileName.replace(/\\.[^.]+$/, '') + '.pdf'\n : fileName + '.pdf';\n}\n\nfunction basename(path) {\n return String(path || '').split('/').pop();\n}\n\nfunction getPairedIndex(item, fallbackIndex) {\n if (typeof item.pairedItem?.item === 'number') return item.pairedItem.item;\n if (Array.isArray(item.pairedItem) && typeof item.pairedItem[0]?.item === 'number') return item.pairedItem[0].item;\n return fallbackIndex;\n}\n\nfunction getOriginalJson(item, i) {\n try {\n const originalItems = $items('Bewaar originele data', 0, 0);\n const pairedIndex = getPairedIndex(item, i);\n return originalItems[pairedIndex]?.json ?? {};\n } catch (e) {\n return {};\n }\n}\n\nconst out = [];\n\nfor (let i = 0; i < items.length; i++) {\n const item = items[i];\n\n // Herstel de originele JSON, want HTTP Request kan JSON vervangen/verliezen\n const originalJson = {\n ...getOriginalJson(item, i),\n ...(item.json ?? {}),\n };\n\n const bin = item.binary?.[BIN_PROP];\n\n if (!bin) {\n out.push({\n json: {\n ...originalJson,\n converted: false,\n attachmentError: `Geen binary property '${BIN_PROP}' gevonden`,\n },\n });\n continue;\n }\n\n const originalName =\n bin.fileName ||\n originalJson?.filename ||\n basename(originalJson?.path) ||\n basename(originalJson?.NoteText) ||\n `file_${i + 1}`;\n\n const lowerName = originalName.toLowerCase();\n const mimeType = String(bin.mimeType || '').toLowerCase();\n\n const isPdf =\n mimeType === 'application/pdf' || lowerName.endsWith('.pdf');\n\n const isJpg =\n mimeType === 'image/jpeg' ||\n mimeType === 'image/jpg' ||\n lowerName.endsWith('.jpg') ||\n lowerName.endsWith('.jpeg');\n\n const isPng =\n mimeType === 'image/png' || lowerName.endsWith('.png');\n\n const isTif =\n mimeType === 'image/tiff' ||\n mimeType === 'image/tif' ||\n lowerName.endsWith('.tif') ||\n lowerName.endsWith('.tiff');\n\n // tif overslaan\n if (isTif) {\n out.push({\n json: {\n ...originalJson,\n filename: originalName,\n converted: false,\n attachmentSkipped: true,\n attachmentError: 'TIF/TIFF wordt overgeslagen',\n },\n });\n continue;\n }\n\n // pdf ongewijzigd doorgeven\n if (isPdf) {\n out.push({\n json: {\n ...originalJson,\n filename: originalName,\n converted: false,\n },\n binary: {\n [BIN_PROP]: bin,\n },\n });\n continue;\n }\n\n // alleen jpg/png omzetten\n if (!isJpg && !isPng) {\n out.push({\n json: {\n ...originalJson,\n filename: originalName,\n converted: false,\n attachmentSkipped: true,\n attachmentError: `Bestandstype niet ondersteund: ${mimeType || originalName}`,\n },\n });\n continue;\n }\n\n try {\n const buf = await this.helpers.getBinaryDataBuffer(i, BIN_PROP);\n\n if (!buf || !buf.length) {\n throw new Error('Leeg bestand');\n }\n\n const pdfDoc = await PDFDocument.create();\n\n let image;\n if (isJpg) {\n image = await pdfDoc.embedJpg(buf);\n } else {\n image = await pdfDoc.embedPng(buf);\n }\n\n const size = image.size();\n const width = Number(size.width);\n const height = Number(size.height);\n\n if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {\n throw new Error(`Ongeldige afbeeldingsgrootte: width=${width}, height=${height}`);\n }\n\n const page = pdfDoc.addPage();\n page.setSize(width, height);\n\n page.drawImage(image, {\n x: 0,\n y: 0,\n width,\n height,\n });\n\n const pdfBytes = await pdfDoc.save();\n const outputFileName = replaceExtWithPdf(originalName);\n\n const outBin = await this.helpers.prepareBinaryData(\n Buffer.from(pdfBytes),\n outputFileName,\n 'application/pdf'\n );\n\n out.push({\n json: {\n ...originalJson,\n filename: outputFileName,\n converted: true,\n },\n binary: {\n [BIN_PROP]: outBin,\n },\n });\n } catch (err) {\n out.push({\n json: {\n ...originalJson,\n filename: originalName,\n converted: false,\n attachmentError: err.message || String(err),\n },\n });\n }\n}\n\nreturn out;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1872, + -64 + ], + "id": "a6d77e7e-ec90-4723-b70e-b93bfd7c0e25", + "name": "image to pdf" + }, + { + "parameters": { + "jsCode": "// n8n Code node\n// Zet op: Run once for all items\n\nconst MAP = {\n id: 'TrackingID',\n status: 'NoteCodeID', // of 'StatusCode'\n date: 'NoteDate',\n time: 'NoteTime',\n note: 'NoteText',\n etaDate: 'BookInDate',\n etaTime: 'BookInTime',\n signedBy: 'PodSignature',\n carrier: '',\n};\n\nconst BIN_PROP = 'data';\nconst carrierFixed = '1024680';\n\nconst xmlEsc = (s) =>\n String(s ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst first = (...vals) =>\n vals.find(v => v !== undefined && v !== null && String(v).trim() !== '');\n\nfunction toAT(dateStr, timeStr = '') {\n const d = String(dateStr ?? '').trim();\n const t = String(timeStr ?? '').trim();\n\n let m = d.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (m) {\n const [, year, month, day] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n m = d.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);\n if (m) {\n const [, day, month, year] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n throw new Error(`Ongeldige datum: \"${d}\"`);\n}\n\nfunction getFileType(bin, fileName) {\n const mimeType = String(bin?.mimeType || '').toLowerCase();\n const lowerName = String(fileName || '').toLowerCase();\n\n if (mimeType.includes('pdf') || lowerName.endsWith('.pdf')) return 'pdf';\n if (mimeType.includes('jpeg') || lowerName.endsWith('.jpg') || lowerName.endsWith('.jpeg')) return 'jpg';\n if (mimeType.includes('png') || lowerName.endsWith('.png')) return 'png';\n\n // Omdat image to pdf1 alles naar pdf omzet, is pdf hier de veilige fallback.\n return 'pdf';\n}\n\nconst items = $input.all();\nconst out = [];\n\nfor (let index = 0; index < items.length; index++) {\n const item = items[index];\n const row = item.json ?? {};\n\n const trackingId = first(row[MAP.id]);\n const status = first(row[MAP.status]);\n const date = first(row[MAP.date]);\n const time = first(row[MAP.time]);\n const note = first(row[MAP.note], '');\n const signedBy = first(row[MAP.signedBy], '');\n const carrier = first(carrierFixed, row[MAP.carrier], '');\n\n if (!trackingId) {\n throw new Error(\n `Item ${index + 1}: TrackingID ontbreekt. Beschikbare velden: ${Object.keys(row).join(', ')}`\n );\n }\n\n if (!status) {\n throw new Error(\n `Item ${index + 1}: ${MAP.status} ontbreekt. Beschikbare velden: ${Object.keys(row).join(', ')}`\n );\n }\n\n if (!date) {\n throw new Error(\n `Item ${index + 1}: ${MAP.date} ontbreekt. Beschikbare velden: ${Object.keys(row).join(', ')}`\n );\n }\n\n const atDateTime = toAT(date, time);\n\n const etaDate = first(row[MAP.etaDate], '');\n const etaTime = first(row[MAP.etaTime], '');\n\n let etaAT = '';\n if (etaDate) {\n etaAT = toAT(etaDate, etaTime);\n }\n\n const noteXml = note\n ? `\\n ${xmlEsc(note)}`\n : '';\n\n const carrierXml = carrier\n ? `\\n ${xmlEsc(carrier)}`\n : '';\n\n const etaXml = etaAT\n ? `\\n ${xmlEsc(etaAT)}`\n : '';\n\n const nameXml = signedBy\n ? `\\n ${xmlEsc(signedBy)}`\n : '';\n\n let signatureXml = '';\n let attachmentAddedAsBase64 = false;\n let attachmentBase64Length = 0;\n\n const bin = item.binary?.[BIN_PROP];\n\n if (bin) {\n const buffer = await this.helpers.getBinaryDataBuffer(index, BIN_PROP);\n\n if (buffer && buffer.length > 0) {\n const base64 = buffer.toString('base64');\n const attachmentFileName = bin.fileName || row.filename || `${trackingId}_cmr.pdf`;\n const fileType = getFileType(bin, attachmentFileName);\n\n signatureXml =\n `\\n ${base64}`;\n\n attachmentAddedAsBase64 = true;\n attachmentBase64Length = base64.length;\n }\n }\n\n const xml = `\n\n ${xmlEsc(atDateTime)}\n TLN\n \n ${xmlEsc(trackingId)}\n ${xmlEsc(status)}\n ${xmlEsc(atDateTime)}${carrierXml}${noteXml}${etaXml}${nameXml}${signatureXml}\n \n`;\n\n const fileNameBase = `${trackingId}_cmr`;\n const fileName = `${fileNameBase}.xml`;\n\n out.push({\n json: {\n ...row,\n trackingId,\n consignmentNumber: trackingId,\n mappedStatus: status,\n atDateTime,\n etaAT,\n signedBy,\n carrier,\n hasAttachment: !!bin,\n attachmentAddedAsBase64,\n attachmentBase64Length,\n fileNameBase,\n fileName,\n xml,\n },\n });\n}\n\nreturn out;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2064, + -64 + ], + "id": "f6c89c6e-fb4e-4a78-8c7c-0048a6215a79", + "name": "Create XML" + }, + { + "parameters": { + "jsCode": "// n8n Code node\n// Zet op: Run once for all items\n\nconst MAP = {\n id: 'TrackingID',\n status: 'StatusCode', // of 'StatusCode'\n date: 'NoteDate',\n time: 'NoteTime',\n note: 'NoteText',\n etaDate: 'BookInDate',\n etaTime: 'BookInTime',\n signedBy: 'PodSignature', // naam van degene die tekende\n carrier: '', // hier kun je een veldnaam zetten\n};\n\nconst carrierFixed = '1024680'; // vul hier bv '1024680' in om hard te zetten\n\nconst xmlEsc = (s) =>\n String(s ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst first = (...vals) =>\n vals.find(v => v !== undefined && v !== null && String(v).trim() !== '');\n\nfunction toAT(dateStr, timeStr = '') {\n const d = String(dateStr ?? '').trim();\n const t = String(timeStr ?? '').trim();\n\n let m = d.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (m) {\n const [, year, month, day] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n m = d.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);\n if (m) {\n const [, day, month, year] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n throw new Error(`Ongeldige datum: \"${d}\"`);\n}\n\nconst items = $input.all();\n\nreturn items.map((item, index) => {\n const row = item.json ?? {};\n\n const consignmentNumber = first(row[MAP.id]);\n const status = first(row[MAP.status]);\n const date = first(row[MAP.date]);\n const time = first(row[MAP.time]);\n const note = first(row[MAP.note], '');\n const signedBy = first(row[MAP.signedBy], '');\n const carrier = first(carrierFixed, row[MAP.carrier], '');\n\n if (!consignmentNumber) {\n throw new Error(`Item ${index + 1}: ${MAP.id} ontbreekt`);\n }\n\n if (!status) {\n throw new Error(`Item ${index + 1}: ${MAP.status} ontbreekt`);\n }\n\n if (!date) {\n throw new Error(`Item ${index + 1}: ${MAP.date} ontbreekt`);\n }\n\n const atDateTime = toAT(date, time);\n\n const etaDate = first(row[MAP.etaDate], '');\n const etaTime = first(row[MAP.etaTime], '');\n\n let etaAT = '';\n if (etaDate) {\n etaAT = toAT(etaDate, etaTime);\n }\n\n const noteXml = note\n ? `\\n ${xmlEsc(note)}`\n : '';\n\n const carrierXml = carrier\n ? `\\n ${xmlEsc(carrier)}`\n : '';\n\n const etaXml = etaAT\n ? `\\n ${xmlEsc(etaAT)}`\n : '';\n\n const nameXml = signedBy\n ? `\\n ${xmlEsc(signedBy)}`\n : '';\n\n const xml = `\n\n ${xmlEsc(atDateTime)}\n TLN\n \n ${xmlEsc(consignmentNumber)}\n ${xmlEsc(status)}\n ${xmlEsc(atDateTime)}${carrierXml}${noteXml}${etaXml}${nameXml}\n \n`;\n\n return {\n json: {\n ...row,\n consignmentNumber,\n mappedStatus: status,\n atDateTime,\n etaAT,\n signedBy,\n carrier,\n xml,\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1680, + 1040 + ], + "id": "68ad47dc-543f-4e47-bc36-ac10ee80b5e9", + "name": "Geladen Hub" + }, + { + "parameters": { + "jsCode": "// n8n Code node\n// Zet op: Run once for all items\n\nconst MAP = {\n id: 'TrackingID',\n status: 'StatusCode', // of 'StatusCode'\n date: 'NoteDate',\n time: 'NoteTime',\n note: 'NoteText',\n etaDate: 'BookInDate',\n etaTime: 'BookInTime',\n signedBy: 'PodSignature', // naam van degene die tekende\n carrier: '', // hier kun je een veldnaam zetten\n};\n\nconst carrierFixed = '1024680'; // vul hier bv '1024680' in om hard te zetten\n\nconst xmlEsc = (s) =>\n String(s ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst first = (...vals) =>\n vals.find(v => v !== undefined && v !== null && String(v).trim() !== '');\n\nfunction toAT(dateStr, timeStr = '') {\n const d = String(dateStr ?? '').trim();\n const t = String(timeStr ?? '').trim();\n\n let m = d.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (m) {\n const [, year, month, day] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n m = d.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);\n if (m) {\n const [, day, month, year] = m;\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n const hh = tm ? tm[1] : '00';\n const mm = tm ? tm[2] : '00';\n const ss = tm && tm[3] ? tm[3] : '00';\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n throw new Error(`Ongeldige datum: \"${d}\"`);\n}\n\nconst items = $input.all();\n\nreturn items.map((item, index) => {\n const row = item.json ?? {};\n\n const consignmentNumber = first(row[MAP.id]);\n const status = first(row[MAP.status]);\n const date = first(row[MAP.date]);\n const time = first(row[MAP.time]);\n const note = first(row[MAP.note], '');\n const signedBy = first(row[MAP.signedBy], '');\n const carrier = first(carrierFixed, row[MAP.carrier], '');\n\n if (!consignmentNumber) {\n throw new Error(`Item ${index + 1}: ${MAP.id} ontbreekt`);\n }\n\n if (!status) {\n throw new Error(`Item ${index + 1}: ${MAP.status} ontbreekt`);\n }\n\n if (!date) {\n throw new Error(`Item ${index + 1}: ${MAP.date} ontbreekt`);\n }\n\n const atDateTime = toAT(date, time);\n\n const etaDate = first(row[MAP.etaDate], '');\n const etaTime = first(row[MAP.etaTime], '');\n\n let etaAT = '';\n if (etaDate) {\n etaAT = toAT(etaDate, etaTime);\n }\n\n const noteXml = note\n ? `\\n ${xmlEsc(note)}`\n : '';\n\n const carrierXml = carrier\n ? `\\n ${xmlEsc(carrier)}`\n : '';\n\n const etaXml = etaAT\n ? `\\n ${xmlEsc(etaAT)}`\n : '';\n\n const nameXml = signedBy\n ? `\\n ${xmlEsc(signedBy)}`\n : '';\n\n const xml = `\n\n ${xmlEsc(atDateTime)}\n TLN\n \n ${xmlEsc(consignmentNumber)}\n ${xmlEsc(status)}\n ${xmlEsc(atDateTime)}${carrierXml}${noteXml}${etaXml}${nameXml}\n \n`;\n\n return {\n json: {\n ...row,\n consignmentNumber,\n mappedStatus: status,\n atDateTime,\n etaAT,\n signedBy,\n carrier,\n xml,\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1680, + 1216 + ], + "id": "8456fbdc-4d33-4751-b529-e89ff7c49bed", + "name": "Out for delivery" + } + ], + "pinData": {}, + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "Get Last Timestamp from Redis", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse XML Response": { + "main": [ + [ + { + "node": "Check for notes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Split Manifests": { + "main": [ + [ + { + "node": "Filter Paying depot", + "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 Notes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Timestamp Data": { + "main": [ + [ + { + "node": "Save Timestamp to Redis", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Palletways Notes": { + "main": [ + [ + { + "node": "Parse XML Response", + "type": "main", + "index": 0 + } + ] + ] + }, + "Filter Paying depot": { + "main": [ + [ + { + "node": "Switch", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch": { + "main": [ + [ + { + "node": "Download CMR1", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Create bookin xml", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Create POC XML", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Afgeleverd", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Create ETA XML", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Delivery depot XML", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Geladen Hub", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Out for delivery", + "type": "main", + "index": 0 + } + ], + [] + ] + }, + "Download CMR": { + "main": [ + [ + { + "node": "image to pdf1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check for notes": { + "main": [ + [ + { + "node": "Split Manifests", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create bookin xml": { + "main": [ + [ + { + "node": "FTP2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create POC XML": { + "main": [ + [ + { + "node": "FTP2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Afgeleverd": { + "main": [ + [ + { + "node": "FTP2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create ETA XML": { + "main": [ + [ + { + "node": "FTP2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Delivery depot XML": { + "main": [ + [ + { + "node": "FTP2", + "type": "main", + "index": 0 + } + ] + ] + }, + "image to pdf1": { + "main": [ + [ + { + "node": "Create XML1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create XML1": { + "main": [ + [ + { + "node": "FTP2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Download CMR1": { + "main": [ + [ + { + "node": "image to pdf", + "type": "main", + "index": 0 + } + ] + ] + }, + "image to pdf": { + "main": [ + [ + { + "node": "Create XML", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create XML": { + "main": [ + [ + { + "node": "FTP2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Geladen Hub": { + "main": [ + [ + { + "node": "FTP2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Out for delivery": { + "main": [ + [ + { + "node": "FTP2", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "timezone": "Europe/London", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false, + "timeSavedMode": "fixed", + "errorWorkflow": "1Ps5lukDf2sgcGL7" + }, + "versionId": "3d92e4ff-a291-43d8-b65d-eb7c37bb0c54", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "bef8d409866a58c0777dfe7cca1b9c2400fd051c056d361501393ab423006b5f" + }, + "nodeGroups": [], + "id": "CyVjh2cxoyAsO1kN", + "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 diff --git a/palletways/Palletways Status TP--_PW.json b/palletways/Palletways Status TP--_PW.json new file mode 100644 index 0000000..7fea80d --- /dev/null +++ b/palletways/Palletways Status TP--_PW.json @@ -0,0 +1,1005 @@ +{ + "name": "Palletways Status TP-->PW", + "nodes": [ + { + "parameters": { + "options": {} + }, + "id": "fb058d6b-f234-41f2-ace2-617e5a579f22", + "name": "Parse XML to JSON", + "type": "n8n-nodes-base.xml", + "typeVersion": 1, + "position": [ + 768, + -96 + ] + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "b2e41c2d-3ee9-4ecb-a13f-df6fa1c7ade0", + "leftValue": "={{ $json.shipment.status.description }}", + "rightValue": "Send to OBC", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Send to OBC" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "6e4f9c09-50d7-43b9-b89b-7e8fd9ee9801", + "leftValue": "={{ $json.shipment.status.code }}", + "rightValue": "800", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "in uitlevering" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "e29fd6d4-e802-4578-8683-af0fc45df778", + "leftValue": "={{ $json.shipment.status.code }}", + "rightValue": "600", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Begin lossen" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "f8a3fe31-c4f3-47bf-a5a0-5e5fbb6e8bf7", + "leftValue": "={{ $json.shipment.status.code }}", + "rightValue": "601", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Afgeleverd" + } + ] + }, + "options": { + "fallbackOutput": "extra" + } + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.3, + "position": [ + 992, + -112 + ], + "id": "daba04a6-1031-4457-805e-a5d06b990d33", + "name": "Switch" + }, + { + "parameters": { + "path": "={{ $json.path }}", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + 224, + -64 + ], + "id": "4698ff26-cd96-4210-ae9e-890d758cb724", + "name": "Download file", + "credentials": { + "ftp": { + "id": "oLAZ4OgmkOopMHAq", + "name": "FTP De Wit Transport" + } + } + }, + { + "parameters": { + "operation": "delete", + "path": "={{ $('Download file').item.json.path }}", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + 1664, + -640 + ], + "id": "06af28b9-d0d9-4bea-8afe-84ae0184c433", + "name": "FTP1", + "credentials": { + "ftp": { + "id": "oLAZ4OgmkOopMHAq", + "name": "FTP De Wit Transport" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.palletways.com/createsystemnote/?apikey=SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D&commit=true'", + "sendBody": true, + "contentType": "multipart-form-data", + "bodyParameters": { + "parameters": [ + { + "name": "apikey", + "value": "SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus=" + }, + { + "name": "inputformat", + "value": "json" + }, + { + "name": "outputformat", + "value": "json" + }, + { + "name": "commit", + "value": "true" + }, + { + "name": "data", + "value": "={{ JSON.stringify($json) }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 1664, + -832 + ], + "id": "1283e55c-a44e-46ef-a863-8048cdc1a323", + "name": "Post Note", + "disabled": true + }, + { + "parameters": { + "operation": "delete", + "path": "={{ $('Download file').item.json.path }}", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + 1888, + -832 + ], + "id": "84c0e11c-366a-42f8-b701-e7aa0f2757fb", + "name": "Verwijder note bestand", + "credentials": { + "ftp": { + "id": "oLAZ4OgmkOopMHAq", + "name": "FTP De Wit Transport" + } + } + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "*/3 6-22 * * 1-5" + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + -416, + -64 + ], + "id": "77d08df2-d3b9-44b9-bb25-5a94ad506ad1", + "name": "Schedule Trigger" + }, + { + "parameters": { + "operation": "list", + "path": "/prod/FromTP/status/pw/", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + -192, + -64 + ], + "id": "bc287028-ffb8-4678-9996-0c8f26098080", + "name": "List files", + "credentials": { + "ftp": { + "id": "oLAZ4OgmkOopMHAq", + "name": "FTP De Wit Transport" + } + } + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "a9b06c99-6c78-4262-8bb9-432cb840c103", + "leftValue": "={{ $json.path }}", + "rightValue": ".xml", + "operator": { + "type": "string", + "operation": "contains" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.filter", + "typeVersion": 2.2, + "position": [ + 32, + -64 + ], + "id": "cd04cdd3-641e-44a7-9bfb-e60d84180392", + "name": "Filter alleen .xml" + }, + { + "parameters": { + "operation": "xml", + "options": {} + }, + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 464, + -64 + ], + "id": "8a1a147a-4603-41ee-8229-049ed4b29290", + "name": "Extract from File" + }, + { + "parameters": { + "operation": "delete", + "path": "={{ $('Download file').item.json.path }}", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + 1440, + -448 + ], + "id": "2f83780b-bb47-43fc-b0d9-2350217b52db", + "name": "FTP2", + "credentials": { + "ftp": { + "id": "oLAZ4OgmkOopMHAq", + "name": "FTP De Wit Transport" + } + } + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "leftValue": "={{ $json.shipment.status.planleg.Legtype_planleg }}", + "rightValue": "laden", + "operator": { + "type": "string", + "operation": "equals" + }, + "id": "29ffb651-831a-4bed-80c5-20d934de7c55" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "laden" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "783d8d52-efe5-4aeb-bbc6-3b41fdd3bc1f", + "leftValue": "={{ $json.shipment.status.planleg.Legtype_planleg }}", + "rightValue": "lossen", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "lossen" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "7662e24c-218c-44e5-a6d1-1853a846c7e4", + "leftValue": "={{ $json.shipment.status.planleg.Legtype_planleg }}", + "rightValue": "direct", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "direct" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.3, + "position": [ + 1216, + -656 + ], + "id": "e4bd6b02-2a0d-4b09-9271-34872b025d93", + "name": "Send to OBC" + }, + { + "parameters": { + "mode": "raw", + "jsonOutput": "={\n \"notes\": {\n \"header\": {\n \"create_date\": \"{{$now.toFormat('dd/LL/yy')}}\",\n \"create_time\": \"{{$now.toFormat('HH:mm')}}\",\n \"orig_filename\": \"myfilename.xml\",\n \"notes_qty\": 1\n },\n \"note\": [\n {\n \"pw_id\": \"{{ $json.shipment.edireference }}\",\n \"barcode\": \"\",\n \"note_create_date\": \"{{$now.toFormat('dd/LL/yy')}}\",\n \"note_create_time\": \"{{$now.toFormat('HH:mm')}}\",\n \"customer_name\": \"CustomerName1\",\n \"bin_ref\": \"TheBinRef_1\",\n \"free_text\": \"Gepland om te laden {{ \n (() => {\n const raw = $json.shipment.status.planleg.Begin_adres_eta || '';\n const parts = raw.split(' ');\n if (parts.length < 2) return '';\n const timePart = parts[1];\n const [hStr, mStr, sStr] = timePart.split(':');\n let h = Number(hStr);\n let m = Number(mStr);\n let s = Number(sStr || 0);\n if (s > 0) { m += 1; s = 0; }\n let totalMinutes = h * 60 + m;\n totalMinutes = Math.ceil(totalMinutes / 5) * 5;\n h = Math.floor(totalMinutes / 60);\n m = totalMinutes % 60;\n const hh = String(h).padStart(2, '0');\n const mm = String(m).padStart(2, '0');\n return `${hh}:${mm}`;\n })() \n}} chauffeur {{ $json.shipment.status.planleg.chauffeur }} kenteken {{ $json.shipment.status.planleg.Kenteken }}\",\n \"note_group\": \"JIN\",\n \"note_type\": \"GIN\",\n \"note_description\": \"nn\"\n }\n \n ]\n }\n} ", + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1440, + -832 + ], + "id": "41fd7a10-8708-40a5-96b7-b69b831b8fef", + "name": "Create Collection Note" + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.palletways.com/createsystemnote/?apikey=SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D&commit=true", + "sendBody": true, + "contentType": "multipart-form-data", + "bodyParameters": { + "parameters": [ + { + "name": "apikey", + "value": "SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus=" + }, + { + "name": "inputformat", + "value": "json" + }, + { + "name": "outputformat", + "value": "json" + }, + { + "name": "commit", + "value": "true" + }, + { + "name": "data", + "value": "={{ JSON.stringify($json) }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 1440, + -256 + ], + "id": "ebcdf42c-6a8f-4409-bbeb-e2c32bb53be7", + "name": "Post Note2" + }, + { + "parameters": { + "method": "POST", + "url": "=https://portal.palletways.com/api/setETA/{{ $json.shipment.reference }}/{{ $json.shipment.status.planleg.Eind_addres_eta }}?apikey=SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D&", + "sendBody": true, + "contentType": "multipart-form-data", + "bodyParameters": { + "parameters": [ + { + "name": "apikey", + "value": "SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus=" + }, + { + "name": "inputformat", + "value": "json" + }, + { + "name": "outputformat", + "value": "json" + }, + { + "name": "commit", + "value": "true" + }, + { + "name": "data", + "value": "={{ JSON.stringify($json) }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 1440, + -640 + ], + "id": "acbb4e23-4bdf-4d0b-acb6-080f892ad313", + "name": "Post ETA" + }, + { + "parameters": { + "mode": "raw", + "jsonOutput": "={\n \"notes\": {\n \"header\": {\n \"create_date\": \"{{$now.toFormat('dd/LL/yy')}}\",\n \"create_time\": \"{{$now.toFormat('HH:mm')}}\",\n \"orig_filename\": \"myfilename.xml\",\n \"notes_qty\": 1\n },\n \"note\": [\n {\n \"pw_id\": \"{{ $json.shipment.edireference }}\",\n \"barcode\": \"{{ $json.shipment.status.planleg.Barcode }}\",\n \"note_create_date\": \"{{$now.toFormat('dd/LL/yy')}}\",\n \"note_create_time\": \"{{$now.toFormat('HH:mm')}}\",\n \"customer_name\": \"CustomerName1\",\n \"bin_ref\": \"TheBinRef_1\",\n \"free_text\": \"Gescand op voertuig {{ $json.shipment.status.planleg.Kenteken }} bij Depot 464\",\n \"note_group\": \"VL\",\n \"note_type\": \"SCN\",\n \"note_description\": \"{{ $json.shipment.status.planleg.Kenteken }}\"\n }\n \n ]\n }\n} ", + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1216, + -256 + ], + "id": "0777ac33-300a-4f3c-92f0-b56d0dc74417", + "name": "Create OFD Note" + }, + { + "parameters": { + "operation": "delete", + "path": "={{ $('Download file').item.json.path }}", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + 1664, + -256 + ], + "id": "df07a69f-5d5e-4b2b-99b1-f0bdaf5aa053", + "name": "FTP3", + "credentials": { + "ftp": { + "id": "oLAZ4OgmkOopMHAq", + "name": "FTP De Wit Transport" + } + } + }, + { + "parameters": { + "operation": "delete", + "path": "={{ $('Download file').item.json.path }}", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + 1216, + 320 + ], + "id": "a51d1062-ea66-43e2-97ae-2eb311b9ea1f", + "name": "FTP", + "credentials": { + "ftp": { + "id": "oLAZ4OgmkOopMHAq", + "name": "FTP De Wit Transport" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.palletways.com/createsystemnote/?apikey=SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D&commit=true", + "sendBody": true, + "contentType": "multipart-form-data", + "bodyParameters": { + "parameters": [ + { + "name": "apikey", + "value": "SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus=" + }, + { + "name": "inputformat", + "value": "json" + }, + { + "name": "outputformat", + "value": "json" + }, + { + "name": "commit", + "value": "true" + }, + { + "name": "data", + "value": "={{ JSON.stringify($json) }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 1440, + -64 + ], + "id": "6894bbb0-8fc2-47bb-b72b-6520044424e0", + "name": "Post Note3" + }, + { + "parameters": { + "operation": "delete", + "path": "={{ $('Download file').item.json.path }}", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + 1664, + -64 + ], + "id": "fdd4e0c5-246f-4ac4-b158-700b6232d075", + "name": "FTP4", + "credentials": { + "ftp": { + "id": "oLAZ4OgmkOopMHAq", + "name": "FTP De Wit Transport" + } + } + }, + { + "parameters": { + "mode": "raw", + "jsonOutput": "={\n \"notes\": {\n \"header\": {\n \"create_date\": \"{{$now.toFormat('dd/LL/yy')}}\",\n \"create_time\": \"{{$now.toFormat('HH:mm')}}\",\n \"orig_filename\": \"myfilename.xml\",\n \"notes_qty\": 1\n },\n \"note\": [\n {\n \"pw_id\": \"{{ $json.shipment.edireference }}\",\n \"barcode\": \"{{ $json.shipment.status.planleg.Barcode }}\",\n \"note_create_date\": \"{{$now.toFormat('dd/LL/yy')}}\",\n \"note_create_time\": \"{{$now.toFormat('HH:mm')}}\",\n \"customer_name\": \"CustomerName1\",\n \"bin_ref\": \"TheBinRef_1\",\n \"free_text\": \"Driver arrived at location: {{ $json.shipment.status.position.latitude }},{{ $json.shipment.status.position.longitude }} \",\n \"note_group\": \"VP\",\n \"note_type\": \"DAR\",\n \"note_description\": \"{{ $json.shipment.status.planleg.Kenteken }}\"\n }\n \n ]\n }\n} ", + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1216, + -64 + ], + "id": "cabb6ce3-0af7-4eb9-9bd9-d0da4cffbede", + "name": "Create Begin Lossen" + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.palletways.com/createsystemnote/?apikey=SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D&commit=true", + "sendBody": true, + "contentType": "multipart-form-data", + "bodyParameters": { + "parameters": [ + { + "name": "apikey", + "value": "SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus=" + }, + { + "name": "inputformat", + "value": "json" + }, + { + "name": "outputformat", + "value": "json" + }, + { + "name": "commit", + "value": "true" + }, + { + "name": "data", + "value": "={{ JSON.stringify($json) }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 1440, + 128 + ], + "id": "39df5a62-2fd7-4a6d-95b5-7eceb7484177", + "name": "Post Note4" + }, + { + "parameters": { + "operation": "delete", + "path": "={{ $('Download file').item.json.path }}", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + 1664, + 128 + ], + "id": "455c7c4a-f470-40b5-b246-703a783fdf2a", + "name": "FTP5", + "credentials": { + "ftp": { + "id": "oLAZ4OgmkOopMHAq", + "name": "FTP De Wit Transport" + } + } + }, + { + "parameters": { + "mode": "raw", + "jsonOutput": "={\n \"notes\": {\n \"header\": {\n \"create_date\": \"{{$now.toFormat('dd/LL/yy')}}\",\n \"create_time\": \"{{$now.toFormat('HH:mm')}}\",\n \"orig_filename\": \"myfilename.xml\",\n \"notes_qty\": 1\n },\n \"note\": [\n {\n \"pw_id\": \"{{ $json.shipment.edireference }}\",\n \"barcode\": \"{{ $json.shipment.status.planleg.Barcode }}\",\n \"note_create_date\": \"{{$now.toFormat('dd/LL/yy')}}\",\n \"note_create_time\": \"{{$now.toFormat('HH:mm')}}\",\n \"customer_name\": \"{{ $json.shipment.status.planleg.signdby }}\",\n \"bin_ref\": \"TheBinRef_1\",\n \"free_text\": \"\",\n \"note_group\": \"POD\",\n \"note_type\": \"POD\",\n \"note_description\": \"{{ $json.shipment.status.planleg.Kenteken }}\"\n }\n \n ]\n }\n} ", + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1216, + 128 + ], + "id": "75b42d2d-8065-4762-953f-fc1bd1c7f777", + "name": "Create Afgeleverd note" + } + ], + "pinData": {}, + "connections": { + "Parse XML to JSON": { + "main": [ + [ + { + "node": "Switch", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch": { + "main": [ + [ + { + "node": "Send to OBC", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Create OFD Note", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Create Begin Lossen", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Create Afgeleverd note", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "FTP", + "type": "main", + "index": 0 + } + ] + ] + }, + "Download file": { + "main": [ + [ + { + "node": "Extract from File", + "type": "main", + "index": 0 + } + ] + ] + }, + "Post Note": { + "main": [ + [ + { + "node": "Verwijder note bestand", + "type": "main", + "index": 0 + } + ] + ] + }, + "Schedule Trigger": { + "main": [ + [ + { + "node": "List files", + "type": "main", + "index": 0 + } + ] + ] + }, + "List files": { + "main": [ + [ + { + "node": "Filter alleen .xml", + "type": "main", + "index": 0 + } + ] + ] + }, + "Filter alleen .xml": { + "main": [ + [ + { + "node": "Download file", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract from File": { + "main": [ + [ + { + "node": "Parse XML to JSON", + "type": "main", + "index": 0 + } + ] + ] + }, + "Send to OBC": { + "main": [ + [ + { + "node": "Create Collection Note", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Post ETA", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "FTP2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create Collection Note": { + "main": [ + [ + { + "node": "Post Note", + "type": "main", + "index": 0 + } + ] + ] + }, + "Post Note2": { + "main": [ + [ + { + "node": "FTP3", + "type": "main", + "index": 0 + } + ] + ] + }, + "Post ETA": { + "main": [ + [ + { + "node": "FTP1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create OFD Note": { + "main": [ + [ + { + "node": "Post Note2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Post Note3": { + "main": [ + [ + { + "node": "FTP4", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create Begin Lossen": { + "main": [ + [ + { + "node": "Post Note3", + "type": "main", + "index": 0 + } + ] + ] + }, + "Post Note4": { + "main": [ + [ + { + "node": "FTP5", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create Afgeleverd note": { + "main": [ + [ + { + "node": "Post Note4", + "type": "main", + "index": 0 + } + ] + ] + }, + "FTP5": { + "main": [ + [] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "timezone": "Europe/Amsterdam", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false, + "timeSavedMode": "fixed", + "timeSavedPerExecution": 1, + "errorWorkflow": "1Ps5lukDf2sgcGL7" + }, + "versionId": "52fea8f1-0e86-4704-859e-15d013105cf1", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "bef8d409866a58c0777dfe7cca1b9c2400fd051c056d361501393ab423006b5f" + }, + "nodeGroups": [], + "id": "8btNgoEz6exsoU4x", + "tags": [] +} \ No newline at end of file diff --git a/palletways/Palletways Truckload Manifest.json b/palletways/Palletways Truckload Manifest.json new file mode 100644 index 0000000..1347c9a --- /dev/null +++ b/palletways/Palletways Truckload Manifest.json @@ -0,0 +1,338 @@ +{ + "name": "Palletways Truckload Manifest", + "nodes": [ + { + "parameters": { + "url": "=https://api.palletways.com/lookupDepotNo/{{ $json.Response.Detail.Data.Manifest.Depot.Number }}?apikey=SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D", + "options": { + "response": { + "response": { + "responseFormat": "text" + } + }, + "timeout": 10000 + } + }, + "id": "71e6c357-2553-4bb1-8673-9f79a1f29a34", + "name": "Get Paying Depot Address", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [ + 1408, + -368 + ], + "continueOnFail": true + }, + { + "parameters": { + "options": { + "explicitArray": false, + "mergeAttrs": true + } + }, + "id": "b6c0ed30-64e2-4981-848c-5ca4333851ff", + "name": "Parse Paying Depot XML", + "type": "n8n-nodes-base.xml", + "typeVersion": 1, + "position": [ + 1632, + -368 + ] + }, + { + "parameters": { + "mode": "combine", + "combineBy": "combineByPosition", + "options": {} + }, + "type": "n8n-nodes-base.merge", + "typeVersion": 3.2, + "position": [ + 1856, + -304 + ], + "id": "d9f28643-b9d4-450b-b1f6-055261bbf730", + "name": "Merge" + }, + { + "parameters": { + "functionCode": "// Zet deze Code node bij voorkeur op: \"Run once for all items\"\n\nconst get = (obj, path) =>\n path.split('.').reduce((a, k) => (a && a[k] !== undefined ? a[k] : undefined), obj);\n\nconst nz = (v, d = '') => (v === undefined || v === null ? d : v);\nconst ensureArray = (v) => (Array.isArray(v) ? v : v == null ? [] : [v]);\n\nconst cc = (c) => {\n if (!c) return 'NL';\n const u = String(c).toUpperCase();\n return u === 'UK' ? 'GB' : u;\n};\n\nconst xmlEsc = (s) =>\n String(s ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst parseNum = (v) => {\n if (v === undefined || v === null || v === '') return 0;\n if (typeof v === 'number') return v;\n let s = String(v).trim();\n s = s.replace(',', '.');\n const n = Number(s);\n return isNaN(n) ? 0 : n;\n};\n\nconst firstOf = (obj, paths, def = undefined) => {\n for (const p of paths) {\n const val = get(obj, p);\n if (val !== undefined) return val;\n }\n return def;\n};\n\nfunction buildOne(input) {\n const cons = get(input, 'Response.Detail.Data.Manifest.Depot.Account.Consignment') || {};\n const depotRoot = get(input, 'Response.Detail.Data') || {};\n\n const accountName = nz(get(input, 'Response.Detail.Data.Manifest.Depot.Account.Name'), '');\n const number = nz(cons.Number, '');\n\n // Basisvelden\n const trackingId = nz(cons.TrackingID, nz(cons.Reference, ''));\n const reference = nz(cons.Reference, trackingId);\n const weight = parseNum(nz(cons.Weight, 0));\n const dueDate = nz(cons.DueDate, new Date().toISOString().split('T')[0]);\n const dueTime = nz(cons.DueTime, '') === '20:00' ? '' : nz(cons.DueTime, '');\n const driverInfo = nz(cons.ManifestNote, '');\n\n // Adressen\n const addresses = ensureArray(cons.Address);\n const deliveryAddress = addresses.find((a) => /delivery/i.test(a?.Type || '')) || {};\n const collectionAddress = addresses.find((a) => /collection/i.test(a?.Type || '')) || {};\n\n // Paying depot\n const payingDepot = {\n name: nz(\n depotRoot.CompanyName,\n nz(get(input, 'Response.Detail.Data.Manifest.Depot.Account.Name'), 'Palletways')\n ),\n address1: nz(depotRoot.AddressLine1, ''),\n address2: nz(depotRoot.AddressLine2, ''),\n zipcode: nz(depotRoot.Postcode, ''),\n city: nz(depotRoot.Town, ''),\n country: cc(nz(depotRoot.Countrycode, 'NL')),\n phone: nz(depotRoot.Phone, ''),\n email: nz(depotRoot.Email, ''),\n };\n\n // Return/EU => cargo qty1 (leeg als ontbreekt)\n let euroQty1 = '';\n const returnLines = ensureArray(get(cons, 'Return'));\n for (const r of returnLines) {\n const t = String(nz(r?.Type, '')).toUpperCase();\n if (t === 'EU') {\n const raw = r?.Amount;\n if (raw !== undefined && raw !== null && String(raw).trim() !== '') {\n euroQty1 = String(parseNum(raw));\n }\n break;\n }\n }\n\n // cargo unitamount = lifts\n const lifts = parseNum(nz(cons.Lifts, 1)) || 1;\n\n // BookInRequest -> bool2\n const bookInRaw = String(nz(firstOf(cons, ['BookInRequest', 'Delivery.BookInRequest'], ''))).toLowerCase();\n const bool2 = ['yes', 'true', '1', 'y', 'ja'].includes(bookInRaw);\n\n // taillift -> bool1\n const taillift = String(nz(firstOf(cons, ['TailLift', 'Delivery.TailLift'], ''))).toLowerCase();\n const bool1 = ['yes', 'true', '1', 'y', 'ja'].includes(taillift);\n\n // Prijs\n const csvPrice = parseNum(firstOf(input, ['bedrag'], 0));\n\n // ---- Goodsline opbouw ----\n // 1 goodsline per \"stuk\" in BillUnit (Amount), unitamount=1\n const billUnitsRaw = ensureArray(get(cons, 'BillUnit'));\n const billUnits = billUnitsRaw\n .map((bu) => ({\n type: String(nz(bu?.Type, 'HP')).trim(),\n amount: parseNum(nz(bu?.Amount, 0)),\n }))\n .filter((x) => x.type && x.amount > 0);\n\n // Barcodes uit cons.Pallet (kan string of array zijn)\n const palletsArr = ensureArray(get(cons, 'Pallet'))\n .map((x) => String(x || '').trim())\n .filter((x) => x);\n\n // Eerste barcode ook op shipment-niveau\n const shipmentBarcode = palletsArr.length > 0 ? palletsArr[0] : '';\n\n // goodsUnits: 1 entry = 1 goodsline\n const goodsUnits = [];\n\n if (billUnits.length > 0) {\n for (const bu of billUnits) {\n let cnt = Math.round(bu.amount);\n if (cnt <= 0) cnt = 1;\n for (let i = 0; i < cnt; i++) {\n goodsUnits.push({ unit_id: bu.type });\n }\n }\n } else if (palletsArr.length > 0) {\n // geen BillUnit, wel pallets => 1 goodsline per pallet-barcode\n for (let i = 0; i < palletsArr.length; i++) goodsUnits.push({ unit_id: 'HP' });\n } else {\n goodsUnits.push({ unit_id: 'HP' });\n }\n\n const totalLines = goodsUnits.length || 1;\n const weightPerLine = totalLines > 0 ? (weight / totalLines) : 0;\n\n // Als goodsLines > lifts: vanaf lifts-index => oversized (geen barcode + goodsline productdescription)\n const hasOversized = totalLines > lifts;\n\n const goods = [];\n for (let i = 0; i < goodsUnits.length; i++) {\n const oversized = hasOversized && i >= lifts;\n\n // Barcode alleen invullen als beschikbaar Ên niet oversized\n const barcode = (!oversized && palletsArr[i]) ? palletsArr[i] : '';\n\n goods.push({\n seq: i + 1,\n unitamount: 1,\n unit_id: goodsUnits[i].unit_id,\n barcode,\n oversized,\n weight: weightPerLine,\n });\n }\n\n // --------- XML ----------\n const xml = [];\n xml.push('');\n xml.push('');\n xml.push(' ');\n xml.push(' 0');\n xml.push(` ${xmlEsc(trackingId)}`);\n xml.push(' 55107');\n //xml.push(' 55107');\n xml.push(' 1');\n xml.push(' 1');\n xml.push(' ');\n xml.push(' ');\n xml.push(` ${xmlEsc(trackingId)}`);\n xml.push(` ${xmlEsc(trackingId)}`);\n\n xml.push(' 33');\n xml.push(' 55107');\n xml.push(' false');\n xml.push(' false');\n xml.push(' 2');\n // xml.push(` ${xmlEsc(csvPrice)}`);\n\n // sender\n xml.push(' ');\n xml.push(` ${xmlEsc(nz(collectionAddress.CompanyName, ''))}`);\n xml.push(` ${xmlEsc(nz(collectionAddress.Addr1, ''))}`);\n xml.push(` ${xmlEsc(nz(collectionAddress.Addr2, ''))}`);\n xml.push(` ${xmlEsc(nz(collectionAddress.PostCode, ''))}`);\n xml.push(` ${xmlEsc(nz(collectionAddress.Town, ''))}`);\n xml.push(` ${xmlEsc(cc(collectionAddress.Country || 'NL'))}`);\n xml.push(` ${xmlEsc(nz(collectionAddress.ContactName, ''))}`);\n xml.push(` ${xmlEsc(nz(collectionAddress.Telephone, ''))}`);\n xml.push(' ');\n\n // receiver\n xml.push(' ');\n xml.push(` ${xmlEsc(nz(deliveryAddress.CompanyName, ''))}`);\n xml.push(` ${xmlEsc(nz(deliveryAddress.Addr1, ''))}`);\n xml.push(` ${xmlEsc(nz(deliveryAddress.Addr2, ''))}`);\n xml.push(` ${xmlEsc(nz(deliveryAddress.PostCode, ''))}`);\n xml.push(` ${xmlEsc(nz(deliveryAddress.Town, ''))}`);\n xml.push(` ${xmlEsc(cc(deliveryAddress.Country || 'NL'))}`);\n xml.push(` ${xmlEsc(nz(deliveryAddress.ContactName, ''))}`);\n xml.push(` ${xmlEsc(nz(deliveryAddress.Telephone, ''))}`);\n xml.push(' ');\n\n const todayStr = new Date().toISOString().split('T')[0];\n\n // pickupaddress\n xml.push(' ');\n xml.push(' ');\n xml.push(` ${xmlEsc(todayStr)}`);\n xml.push(` ${xmlEsc(todayStr)}`);\n xml.push(' Palletways (UK) Ltd');\n xml.push(' Bijsterhuizen 1103 A');\n xml.push(' 6546 AS');\n xml.push(' Nijmegen');\n xml.push(' NL');\n xml.push(' ');\n\n // deliveryaddress\n xml.push(' ');\n xml.push(' ');\n xml.push(` ${xmlEsc(dueDate)}`);\n xml.push(` ${xmlEsc(dueDate)}`);\n xml.push(` ${xmlEsc(dueTime)}`);\n xml.push(` ${xmlEsc(nz(deliveryAddress.CompanyName, ''))}`);\n xml.push(` ${xmlEsc(nz(deliveryAddress.Addr1, ''))}`);\n xml.push(` ${xmlEsc(nz(deliveryAddress.Addr2, ''))}`);\n xml.push(` ${xmlEsc(nz(deliveryAddress.PostCode, ''))}`);\n xml.push(` ${xmlEsc(nz(deliveryAddress.Town, ''))}`);\n xml.push(` ${xmlEsc(cc(deliveryAddress.Country || 'NL'))}`);\n xml.push(` ${xmlEsc(nz(deliveryAddress.ContactName, ''))}`);\n xml.push(` ${xmlEsc(nz(deliveryAddress.Telephone, ''))}`);\n xml.push(` ${xmlEsc(driverInfo)}`);\n xml.push(' ');\n\n // references\n xml.push(' ');\n xml.push(\n ` 15${xmlEsc(\n trackingId\n )}`\n );\n xml.push(\n ` 16${xmlEsc(\n number\n )}`\n );\n if (accountName) {\n xml.push(\n ` 31${xmlEsc(\n accountName\n )}`\n );\n }\n xml.push(' ');\n\n // parties\n xml.push(' ');\n xml.push(' ');\n xml.push(' 5');\n xml.push(` ${xmlEsc(payingDepot.name)}`);\n xml.push(` ${xmlEsc(payingDepot.address1)}`);\n xml.push(` ${xmlEsc(payingDepot.address2)}`);\n xml.push(` ${xmlEsc(payingDepot.zipcode)}`);\n xml.push(` ${xmlEsc(payingDepot.city)}`);\n xml.push(` ${xmlEsc(payingDepot.country)}`);\n xml.push(` ${xmlEsc(payingDepot.phone)}`);\n xml.push(` ${xmlEsc(payingDepot.email)}`);\n xml.push(' ');\n xml.push(' ');\n\n // cargo\n xml.push(' ');\n xml.push(` ${xmlEsc(lifts)}`);\n xml.push(' 591');\n xml.push(' 123');\n xml.push(' General Cargo');\n xml.push(` ${xmlEsc(weight)}`);\n xml.push(` ${xmlEsc(euroQty1)}`);\n xml.push(` ${bool1 ? 'true' : 'false'}`);\n xml.push(` ${bool2 ? 'true' : 'false'}`);\n // eerste barcode onder shipment\n if (shipmentBarcode) {\n xml.push(` ${xmlEsc(shipmentBarcode)}`);\n xml.push(' ');\n }\n for (const g of goods) {\n xml.push(' ');\n xml.push(` ${xmlEsc(g.seq)}`);\n xml.push(` ${xmlEsc(g.unitamount)}`);\n xml.push(` ${xmlEsc(g.unit_id)}`);\n xml.push(` ${xmlEsc(g.weight)}`);\n\n // barcode alleen als aanwezig\n if (g.barcode) {\n xml.push(` ${xmlEsc(g.barcode)}`);\n }\n\n // oversized: productdescription op goodsline\n if (g.oversized) {\n xml.push(` Oversized pallet`);\n }\n\n xml.push(' ');\n }\n\n xml.push(' ');\n xml.push(' ');\n\n xml.push(' ');\n xml.push(' ');\n xml.push(' ');\n xml.push('');\n\n const xmlStr = xml.join('\\n');\n const fileName = trackingId ? `${trackingId}.xml` : `booking_${Date.now()}.xml`;\n\n return { filename: fileName, xml: xmlStr };\n}\n\n// -------- input handling (meerdere zendingen) --------\nlet inputs = [];\nif (typeof $input?.all === 'function') {\n inputs = $input.all().map((i) => i.json);\n} else if (Array.isArray($json)) {\n inputs = $json;\n} else {\n inputs = [$json || {}];\n}\n\n// output: 1 item per consignment\nreturn inputs.map((inp) => ({ json: buildOne(inp) }));" + }, + "id": "42726aff-fee0-4bee-adc8-500bbb1d4387", + "name": "Transform to Transpas Format v3", + "type": "n8n-nodes-base.function", + "typeVersion": 1, + "position": [ + 2080, + -304 + ] + }, + { + "parameters": { + "operation": "upload", + "path": "=/prod/ToTP/palletways/tlm_{{ $json.filename }}", + "binaryData": false, + "fileContent": "={{ $json.xml }}", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + 2304, + -304 + ], + "id": "fa3517ff-052f-40b6-b833-3e984e69deb9", + "name": "FTP", + "credentials": { + "ftp": { + "id": "oLAZ4OgmkOopMHAq", + "name": "FTP De Wit Transport" + } + } + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "minutes", + "minutesInterval": 20 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 288, + -208 + ], + "id": "baab84b8-4064-4126-8eb1-5e612c9a6b25", + "name": "Schedule Trigger" + }, + { + "parameters": { + "operation": "list", + "path": "/from_pw/TLM/", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + 512, + -208 + ], + "id": "1bdb9819-1a83-4221-bf90-3a356450fbff", + "name": "FTP List", + "credentials": { + "ftp": { + "id": "m8VXjzicG82nM2ph", + "name": "FTP Palletways" + } + } + }, + { + "parameters": { + "path": "={{ $json.path }}", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + 736, + -208 + ], + "id": "34f7bef7-9c4a-43b4-ac67-d2aa3b01ee99", + "name": "FTP Download", + "credentials": { + "ftp": { + "id": "m8VXjzicG82nM2ph", + "name": "FTP Palletways" + } + } + }, + { + "parameters": { + "operation": "xml", + "options": {} + }, + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1.1, + "position": [ + 960, + -208 + ], + "id": "69aebc53-64c6-4596-a060-a3a19b66dcbf", + "name": "Extract from File" + }, + { + "parameters": { + "operation": "delete", + "path": "={{ $('FTP Download').item.json.path }}", + "options": {} + }, + "type": "n8n-nodes-base.ftp", + "typeVersion": 1, + "position": [ + 1184, + -112 + ], + "id": "804fd72e-e41a-4886-817b-76d2f34ea2a7", + "name": "FTP1", + "credentials": { + "ftp": { + "id": "m8VXjzicG82nM2ph", + "name": "FTP Palletways" + } + } + }, + { + "parameters": { + "options": {} + }, + "type": "n8n-nodes-base.xml", + "typeVersion": 1, + "position": [ + 1184, + -304 + ], + "id": "1756f324-880b-4967-8f67-75fd55e1e0de", + "name": "XML" + } + ], + "pinData": {}, + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "FTP List", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Paying Depot Address": { + "main": [ + [ + { + "node": "Parse Paying Depot XML", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse Paying Depot XML": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 0 + } + ] + ] + }, + "Merge": { + "main": [ + [ + { + "node": "Transform to Transpas Format v3", + "type": "main", + "index": 0 + } + ] + ] + }, + "Transform to Transpas Format v3": { + "main": [ + [ + { + "node": "FTP", + "type": "main", + "index": 0 + } + ] + ] + }, + "FTP": { + "main": [ + [] + ] + }, + "FTP List": { + "main": [ + [ + { + "node": "FTP Download", + "type": "main", + "index": 0 + } + ] + ] + }, + "FTP Download": { + "main": [ + [ + { + "node": "Extract from File", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract from File": { + "main": [ + [ + { + "node": "FTP1", + "type": "main", + "index": 0 + }, + { + "node": "XML", + "type": "main", + "index": 0 + } + ] + ] + }, + "XML": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 1 + }, + { + "node": "Get Paying Depot Address", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "timezone": "Europe/Amsterdam", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false, + "timeSavedMode": "fixed", + "errorWorkflow": "1Ps5lukDf2sgcGL7" + }, + "versionId": "7ed6bf92-c7c4-4a29-ab9c-5bbad7f720b8", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "bef8d409866a58c0777dfe7cca1b9c2400fd051c056d361501393ab423006b5f" + }, + "nodeGroups": [], + "id": "JJuapLJ3cHDAHYac", + "tags": [] +} \ No newline at end of file