Files
n8n-workflows/palletways/Palletways Truckload Manifest.json

338 lines
20 KiB
JSON

{
"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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\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('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n xml.push('<transportbookings>');\n xml.push(' <transportbooking>');\n xml.push(' <autoacceptmethod>0</autoacceptmethod>');\n xml.push(` <edireference>${xmlEsc(trackingId)}</edireference>`);\n xml.push(' <customer_id>55107</customer_id>');\n //xml.push(' <customer_id matchmode=\"8\">55107</customer_id>');\n xml.push(' <department_id>1</department_id>');\n xml.push(' <currency_id>1</currency_id>');\n xml.push(' <shipments>');\n xml.push(' <shipment>');\n xml.push(` <edireference>${xmlEsc(trackingId)}</edireference>`);\n xml.push(` <reference>${xmlEsc(trackingId)}</reference>`);\n\n xml.push(' <plangroup_id>33</plangroup_id>');\n xml.push(' <debtor_id>55107</debtor_id>');\n xml.push(' <blockplanning>false</blockplanning>');\n xml.push(' <followup>false</followup>');\n xml.push(' <version>2</version>');\n // xml.push(` <fixedprice>${xmlEsc(csvPrice)}</fixedprice>`);\n\n // sender\n xml.push(' <sender>');\n xml.push(` <name>${xmlEsc(nz(collectionAddress.CompanyName, ''))}</name>`);\n xml.push(` <address1>${xmlEsc(nz(collectionAddress.Addr1, ''))}</address1>`);\n xml.push(` <address2>${xmlEsc(nz(collectionAddress.Addr2, ''))}</address2>`);\n xml.push(` <zipcode>${xmlEsc(nz(collectionAddress.PostCode, ''))}</zipcode>`);\n xml.push(` <city_id matchmode=\"4\">${xmlEsc(nz(collectionAddress.Town, ''))}</city_id>`);\n xml.push(` <country_id matchmode=\"2\">${xmlEsc(cc(collectionAddress.Country || 'NL'))}</country_id>`);\n xml.push(` <contact>${xmlEsc(nz(collectionAddress.ContactName, ''))}</contact>`);\n xml.push(` <phone>${xmlEsc(nz(collectionAddress.Telephone, ''))}</phone>`);\n xml.push(' </sender>');\n\n // receiver\n xml.push(' <receiver>');\n xml.push(` <name>${xmlEsc(nz(deliveryAddress.CompanyName, ''))}</name>`);\n xml.push(` <address1>${xmlEsc(nz(deliveryAddress.Addr1, ''))}</address1>`);\n xml.push(` <address2>${xmlEsc(nz(deliveryAddress.Addr2, ''))}</address2>`);\n xml.push(` <zipcode>${xmlEsc(nz(deliveryAddress.PostCode, ''))}</zipcode>`);\n xml.push(` <city_id matchmode=\"4\">${xmlEsc(nz(deliveryAddress.Town, ''))}</city_id>`);\n xml.push(` <country_id matchmode=\"2\">${xmlEsc(cc(deliveryAddress.Country || 'NL'))}</country_id>`);\n xml.push(` <contact>${xmlEsc(nz(deliveryAddress.ContactName, ''))}</contact>`);\n xml.push(` <phone>${xmlEsc(nz(deliveryAddress.Telephone, ''))}</phone>`);\n xml.push(' </receiver>');\n\n const todayStr = new Date().toISOString().split('T')[0];\n\n // pickupaddress\n xml.push(' <pickupaddress>');\n xml.push(' <address_id matchmode=\"11\" overwrite=\"false\"></address_id>');\n xml.push(` <date>${xmlEsc(todayStr)}</date>`);\n xml.push(` <datetill>${xmlEsc(todayStr)}</datetill>`);\n xml.push(' <name>Palletways (UK) Ltd</name>');\n xml.push(' <address1>Bijsterhuizen 1103 A</address1>');\n xml.push(' <zipcode>6546 AS</zipcode>');\n xml.push(' <city_id matchmode=\"4\">Nijmegen</city_id>');\n xml.push(' <country_id matchmode=\"2\">NL</country_id>');\n xml.push(' </pickupaddress>');\n\n // deliveryaddress\n xml.push(' <deliveryaddress>');\n xml.push(' <address_id matchmode=\"11\" overwrite=\"false\"></address_id>');\n xml.push(` <date>${xmlEsc(dueDate)}</date>`);\n xml.push(` <datetill>${xmlEsc(dueDate)}</datetill>`);\n xml.push(` <timetill>${xmlEsc(dueTime)}</timetill>`);\n xml.push(` <name>${xmlEsc(nz(deliveryAddress.CompanyName, ''))}</name>`);\n xml.push(` <address1>${xmlEsc(nz(deliveryAddress.Addr1, ''))}</address1>`);\n xml.push(` <address2>${xmlEsc(nz(deliveryAddress.Addr2, ''))}</address2>`);\n xml.push(` <zipcode>${xmlEsc(nz(deliveryAddress.PostCode, ''))}</zipcode>`);\n xml.push(` <city_id matchmode=\"4\">${xmlEsc(nz(deliveryAddress.Town, ''))}</city_id>`);\n xml.push(` <country_id matchmode=\"2\">${xmlEsc(cc(deliveryAddress.Country || 'NL'))}</country_id>`);\n xml.push(` <contact>${xmlEsc(nz(deliveryAddress.ContactName, ''))}</contact>`);\n xml.push(` <phone>${xmlEsc(nz(deliveryAddress.Telephone, ''))}</phone>`);\n xml.push(` <driverinfo>${xmlEsc(driverInfo)}</driverinfo>`);\n xml.push(' </deliveryaddress>');\n\n // references\n xml.push(' <references>');\n xml.push(\n ` <reference><referencekind_id>15</referencekind_id><description>${xmlEsc(\n trackingId\n )}</description></reference>`\n );\n xml.push(\n ` <reference><referencekind_id>16</referencekind_id><description>${xmlEsc(\n number\n )}</description></reference>`\n );\n if (accountName) {\n xml.push(\n ` <reference><referencekind_id>31</referencekind_id><description>${xmlEsc(\n accountName\n )}</description></reference>`\n );\n }\n xml.push(' </references>');\n\n // parties\n xml.push(' <parties>');\n xml.push(' <party>');\n xml.push(' <partytype_id>5</partytype_id>');\n xml.push(` <name>${xmlEsc(payingDepot.name)}</name>`);\n xml.push(` <address1>${xmlEsc(payingDepot.address1)}</address1>`);\n xml.push(` <address2>${xmlEsc(payingDepot.address2)}</address2>`);\n xml.push(` <zipcode>${xmlEsc(payingDepot.zipcode)}</zipcode>`);\n xml.push(` <city_id matchmode=\"4\">${xmlEsc(payingDepot.city)}</city_id>`);\n xml.push(` <country_id matchmode=\"2\">${xmlEsc(payingDepot.country)}</country_id>`);\n xml.push(` <phone>${xmlEsc(payingDepot.phone)}</phone>`);\n xml.push(` <email>${xmlEsc(payingDepot.email)}</email>`);\n xml.push(' </party>');\n xml.push(' </parties>');\n\n // cargo\n xml.push(' <cargo>');\n xml.push(` <unitamount>${xmlEsc(lifts)}</unitamount>`);\n xml.push(' <unit_id>591</unit_id>');\n xml.push(' <product_id>123</product_id>');\n xml.push(' <productdescription>General Cargo</productdescription>');\n xml.push(` <weight>${xmlEsc(weight)}</weight>`);\n xml.push(` <qty1>${xmlEsc(euroQty1)}</qty1>`);\n xml.push(` <bool1>${bool1 ? 'true' : 'false'}</bool1>`);\n xml.push(` <bool2>${bool2 ? 'true' : 'false'}</bool2>`);\n // eerste barcode onder shipment\n if (shipmentBarcode) {\n xml.push(` <barcode>${xmlEsc(shipmentBarcode)}</barcode>`);\n xml.push(' <goodslines>');\n }\n for (const g of goods) {\n xml.push(' <goodsline>');\n xml.push(` <sequence>${xmlEsc(g.seq)}</sequence>`);\n xml.push(` <unitamount>${xmlEsc(g.unitamount)}</unitamount>`);\n xml.push(` <unit_id matchmode=\"1\">${xmlEsc(g.unit_id)}</unit_id>`);\n xml.push(` <weight>${xmlEsc(g.weight)}</weight>`);\n\n // barcode alleen als aanwezig\n if (g.barcode) {\n xml.push(` <barcode>${xmlEsc(g.barcode)}</barcode>`);\n }\n\n // oversized: productdescription op goodsline\n if (g.oversized) {\n xml.push(` <productdescription>Oversized pallet</productdescription>`);\n }\n\n xml.push(' </goodsline>');\n }\n\n xml.push(' </goodslines>');\n xml.push(' </cargo>');\n\n xml.push(' </shipment>');\n xml.push(' </shipments>');\n xml.push(' </transportbooking>');\n xml.push('</transportbookings>');\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": []
}