Werkende versie

This commit is contained in:
adminjeroen committed 2026-08-06 20:19:41 +02:00
1 parent 59409c1edd
commit 9a0a02a292
1 file changed
+166 -84
@@ -1,5 +1,5 @@
{ {
"name": "Lidl Frankrijk - Opdracht naar Postgres en Transpas -", "name": "Lidl Frankrijk - Opdracht naar Postgres en Transpas",
"nodes": [ "nodes": [
{ {
"parameters": { "parameters": {
@@ -9,9 +9,9 @@
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
1136, 1136,
416 576
], ],
"id": "2d78e121-cdb5-48b6-a98b-0aeec510cbb0", "id": "798a0f29-9cfd-4e9e-951b-ef9ab1252c2e",
"name": "Selecteer Excel-bijlagen", "name": "Selecteer Excel-bijlagen",
"notesInFlow": true, "notesInFlow": true,
"onError": "continueErrorOutput", "onError": "continueErrorOutput",
@@ -24,17 +24,17 @@
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
2352, 2032,
368 576
], ],
"id": "6d4504e4-7b7b-4369-827c-2cee9951b0cf", "id": "d28f2932-ef00-494c-b882-8797e5823abf",
"name": "Normaliseer en valideer orders", "name": "Normaliseer en valideer orders",
"onError": "continueErrorOutput" "onError": "continueErrorOutput"
}, },
{ {
"parameters": { "parameters": {
"operation": "executeQuery", "operation": "executeQuery",
"query": "WITH input AS MATERIALIZED (\n SELECT $1::jsonb AS j\n),\nsrc AS MATERIALIZED (\n SELECT\n j,\n j->'business_payload' AS p,\n md5((j->'business_payload')::text) AS payload_hash\n FROM input\n),\nprevious AS MATERIALIZED (\n SELECT o.*\n FROM public.lidl_fr_orders o\n JOIN src s ON s.p->>'transport_number' = o.transport_number\n FOR UPDATE\n),\nupserted AS (\n INSERT INTO public.lidl_fr_orders AS existing (\n transport_number, current_status, is_cancelled, revision, current_payload_hash,\n pickup_date, pickup_time, pickup_time_till, pickup_name, pickup_address1,\n pickup_zipcode, pickup_city, pickup_phone, pickup_country,\n delivery_date, delivery_date_till, delivery_time, delivery_time_till,\n delivery_name, delivery_address1, delivery_zipcode, delivery_city, delivery_country,\n transport_code, colli, loose_boxes, transport_units, product_group,\n load_carrier_count, load_carrier_type, temperature_group,\n min_temperature, max_temperature, reference_text,\n external_document_number, external_document_branch, forwarder, gross_weight,\n source_file_name, source_message_id, source_subject, source_from,\n source_received_at, raw_row\n )\n SELECT\n p->>'transport_number',\n p->>'status',\n COALESCE((p->>'is_cancelled')::boolean, false),\n 1,\n payload_hash,\n NULLIF(p->>'pickup_date', '')::date,\n NULLIF(p->>'pickup_time', '')::time,\n NULLIF(p->>'pickup_time_till', '')::time,\n p->>'pickup_name',\n p->>'pickup_address1',\n p->>'pickup_zipcode',\n p->>'pickup_city',\n p->>'pickup_phone',\n p->>'pickup_country',\n NULLIF(p->>'delivery_date', '')::date,\n NULLIF(p->>'delivery_date_till', '')::date,\n NULLIF(p->>'delivery_time', '')::time,\n NULLIF(p->>'delivery_time_till', '')::time,\n p->>'delivery_name',\n p->>'delivery_address1',\n p->>'delivery_zipcode',\n p->>'delivery_city',\n p->>'delivery_country',\n p->>'transport_code',\n NULLIF(p->>'colli', '')::numeric,\n NULLIF(p->>'loose_boxes', '')::numeric,\n NULLIF(p->>'transport_units', '')::numeric,\n p->>'product_group',\n NULLIF(p->>'load_carrier_count', '')::numeric,\n p->>'load_carrier_type',\n p->>'temperature_group',\n NULLIF(p->>'min_temperature', '')::numeric,\n NULLIF(p->>'max_temperature', '')::numeric,\n p->>'reference_text',\n p->>'external_document_number',\n p->>'external_document_branch',\n p->>'forwarder',\n NULLIF(p->>'gross_weight', '')::numeric,\n j->>'source_file_name',\n j->>'source_message_id',\n j->>'source_subject',\n j->>'source_from',\n NULLIF(j->>'source_received_at', '')::timestamptz,\n COALESCE(j->'raw_row', '{}'::jsonb)\n FROM src\n ON CONFLICT (transport_number) DO UPDATE SET\n current_status = EXCLUDED.current_status,\n is_cancelled = EXCLUDED.is_cancelled,\n revision = CASE\n WHEN existing.current_payload_hash IS DISTINCT FROM EXCLUDED.current_payload_hash\n THEN existing.revision + 1\n ELSE existing.revision\n END,\n current_payload_hash = EXCLUDED.current_payload_hash,\n pickup_date = EXCLUDED.pickup_date,\n pickup_time = EXCLUDED.pickup_time,\n pickup_time_till = EXCLUDED.pickup_time_till,\n pickup_name = EXCLUDED.pickup_name,\n pickup_address1 = EXCLUDED.pickup_address1,\n pickup_zipcode = EXCLUDED.pickup_zipcode,\n pickup_city = EXCLUDED.pickup_city,\n pickup_phone = EXCLUDED.pickup_phone,\n pickup_country = EXCLUDED.pickup_country,\n delivery_date = EXCLUDED.delivery_date,\n delivery_date_till = EXCLUDED.delivery_date_till,\n delivery_time = EXCLUDED.delivery_time,\n delivery_time_till = EXCLUDED.delivery_time_till,\n delivery_name = EXCLUDED.delivery_name,\n delivery_address1 = EXCLUDED.delivery_address1,\n delivery_zipcode = EXCLUDED.delivery_zipcode,\n delivery_city = EXCLUDED.delivery_city,\n delivery_country = EXCLUDED.delivery_country,\n transport_code = EXCLUDED.transport_code,\n colli = EXCLUDED.colli,\n loose_boxes = EXCLUDED.loose_boxes,\n transport_units = EXCLUDED.transport_units,\n product_group = EXCLUDED.product_group,\n load_carrier_count = EXCLUDED.load_carrier_count,\n load_carrier_type = EXCLUDED.load_carrier_type,\n temperature_group = EXCLUDED.temperature_group,\n min_temperature = EXCLUDED.min_temperature,\n max_temperature = EXCLUDED.max_temperature,\n reference_text = EXCLUDED.reference_text,\n external_document_number = EXCLUDED.external_document_number,\n externaLine truncated "query": "WITH input AS MATERIALIZED (\n SELECT $1::jsonb AS j\n),\nsrc AS MATERIALIZED (\n SELECT\n j,\n j->'business_payload' AS p,\n md5((j->'business_payload')::text) AS payload_hash\n FROM input\n),\nprevious AS MATERIALIZED (\n SELECT o.*\n FROM public.lidl_fr_orders o\n JOIN src s ON s.p->>'transport_number' = o.transport_number\n FOR UPDATE\n),\nupserted AS (\n INSERT INTO public.lidl_fr_orders AS existing (\n transport_number, current_status, is_cancelled, revision, current_payload_hash,\n pickup_date, pickup_time, pickup_time_till, pickup_name, pickup_address1,\n pickup_zipcode, pickup_city, pickup_phone, pickup_country,\n delivery_date, delivery_date_till, delivery_time, delivery_time_till,\n delivery_name, delivery_address1, delivery_zipcode, delivery_city, delivery_country,\n transport_code, colli, loose_boxes, transport_units, product_group,\n load_carrier_count, load_carrier_type, temperature_group,\n min_temperature, max_temperature, reference_text,\n external_document_number, external_document_branch, forwarder, gross_weight,\n source_file_name, source_message_id, source_subject, source_from,\n source_received_at, raw_row\n )\n SELECT\n p->>'transport_number',\n p->>'status',\n COALESCE((p->>'is_cancelled')::boolean, false),\n 1,\n payload_hash,\n NULLIF(p->>'pickup_date', '')::date,\n NULLIF(p->>'pickup_time', '')::time,\n NULLIF(p->>'pickup_time_till', '')::time,\n p->>'pickup_name',\n p->>'pickup_address1',\n p->>'pickup_zipcode',\n p->>'pickup_city',\n p->>'pickup_phone',\n p->>'pickup_country',\n NULLIF(p->>'delivery_date', '')::date,\n NULLIF(p->>'delivery_date_till', '')::date,\n NULLIF(p->>'delivery_time', '')::time,\n NULLIF(p->>'delivery_time_till', '')::time,\n p->>'delivery_name',\n p->>'delivery_address1',\n p->>'delivery_zipcode',\n p->>'delivery_city',\n p->>'delivery_country',\n p->>'transport_code',\n NULLIF(p->>'colli', '')::numeric,\n NULLIF(p->>'loose_boxes', '')::numeric,\n NULLIF(p->>'transport_units', '')::numeric,\n p->>'product_group',\n NULLIF(p->>'load_carrier_count', '')::numeric,\n p->>'load_carrier_type',\n p->>'temperature_group',\n NULLIF(p->>'min_temperature', '')::numeric,\n NULLIF(p->>'max_temperature', '')::numeric,\n p->>'reference_text',\n p->>'external_document_number',\n p->>'external_document_branch',\n p->>'forwarder',\n NULLIF(p->>'gross_weight', '')::numeric,\n j->>'source_file_name',\n j->>'source_message_id',\n j->>'source_subject',\n j->>'source_from',\n NULLIF(j->>'source_received_at', '')::timestamptz,\n COALESCE(j->'raw_row', '{}'::jsonb)\n FROM src\n ON CONFLICT (transport_number) DO UPDATE SET\n current_status = EXCLUDED.current_status,\n is_cancelled = EXCLUDED.is_cancelled,\n revision = CASE\n WHEN existing.current_payload_hash IS DISTINCT FROM EXCLUDED.current_payload_hash\n THEN existing.revision + 1\n ELSE existing.revision\n END,\n current_payload_hash = EXCLUDED.current_payload_hash,\n pickup_date = EXCLUDED.pickup_date,\n pickup_time = EXCLUDED.pickup_time,\n pickup_time_till = EXCLUDED.pickup_time_till,\n pickup_name = EXCLUDED.pickup_name,\n pickup_address1 = EXCLUDED.pickup_address1,\n pickup_zipcode = EXCLUDED.pickup_zipcode,\n pickup_city = EXCLUDED.pickup_city,\n pickup_phone = EXCLUDED.pickup_phone,\n pickup_country = EXCLUDED.pickup_country,\n delivery_date = EXCLUDED.delivery_date,\n delivery_date_till = EXCLUDED.delivery_date_till,\n delivery_time = EXCLUDED.delivery_time,\n delivery_time_till = EXCLUDED.delivery_time_till,\n delivery_name = EXCLUDED.delivery_name,\n delivery_address1 = EXCLUDED.delivery_address1,\n delivery_zipcode = EXCLUDED.delivery_zipcode,\n delivery_city = EXCLUDED.delivery_city,\n delivery_country = EXCLUDED.delivery_country,\n transport_code = EXCLUDED.transport_code,\n colli = EXCLUDED.colli,\n loose_boxes = EXCLUDED.loose_boxes,\n transport_units = EXCLUDED.transport_units,\n product_group = EXCLUDED.product_group,\n load_carrier_count = EXCLUDED.load_carrier_count,\n load_carrier_type = EXCLUDED.load_carrier_type,\n temperature_group = EXCLUDED.temperature_group,\n min_temperature = EXCLUDED.min_temperature,\n max_temperature = EXCLUDED.max_temperature,\n reference_text = EXCLUDED.reference_text,\n external_document_number = EXCLUDED.external_document_number,\n externaLine truncated
"options": { "options": {
"queryReplacement": "={{ [JSON.stringify($json)] }}" "queryReplacement": "={{ [JSON.stringify($json)] }}"
} }
@@ -42,18 +42,20 @@
"type": "n8n-nodes-base.postgres", "type": "n8n-nodes-base.postgres",
"typeVersion": 2.6, "typeVersion": 2.6,
"position": [ "position": [
2672, 2256,
352 576
], ],
"id": "8ef1a7e3-819c-46c5-b04c-cc1db01de2d7", "id": "65f00226-090b-45a2-a401-493fd409474c",
"name": "Upsert order status en zoek pakbon", "name": "Upsert order status en zoek pakbon",
"notesInFlow": true,
"credentials": { "credentials": {
"postgres": { "postgres": {
"id": "m4DQjg3b1iDYGlBp", "id": "m4DQjg3b1iDYGlBp",
"name": "Postgres Flower direct" "name": "Postgres Flower direct"
} }
}, },
"onError": "continueErrorOutput" "onError": "continueErrorOutput",
"notes": "Documentselectie is per transportnummer gecorreleerd met LEFT JOIN LATERAL. Hierdoor kan een document van een andere order niet meer als nieuwste document voor alle orders worden hergebruikt."
}, },
{ {
"parameters": { "parameters": {
@@ -83,27 +85,28 @@
"type": "n8n-nodes-base.filter", "type": "n8n-nodes-base.filter",
"typeVersion": 2.2, "typeVersion": 2.2,
"position": [ "position": [
2960, 2704,
320 240
], ],
"id": "fa659a1a-13b4-4897-b2e6-e637da769216", "id": "fc63eb5b-448a-4d40-a6c4-9b21a516c261",
"name": "Alleen nieuwe of gewijzigde revisies", "name": "Alleen nieuwe of gewijzigde revisies"
"disabled": true
}, },
{ {
"parameters": { "parameters": {
"mode": "runOnceForEachItem", "mode": "runOnceForEachItem",
"jsCode": "// Per item uitvoeren.\n// De ordergegevens komen rechtstreeks uit PostgreSQL of via de gekoppelde item-link\n// wanneer het pakbonbestand eerst van de NAS is gelezen.\n\nconst CFG = {\n customerId: '60971',\n departmentId: '',\n shipmentKindId: '',\n ediproviderId: '',\n companyId: '1',\n unitMatchmode: '1',\n documentTypeId: 'PAKBON',\n documentTypeMatchmode: '1',\n ftpFolder: '/test/ToTP/TPE',\n\n deliveryDriverInfo: [\n 'Transport flowers at +4°C to +6°C – Transport plants at +15°C –',\n 'When transporting a mixed load of flowers and plants, set the temperature to +12°C.',\n 'Exchange the pallets at the unloading location using PAKi eVoucher number 048849 04-CAMBRAI',\n ].join(' '),\n};\n\nconst linkedOrder = $('Upsert order status en zoek pakbon').item.json;\nconst j = $json?.order_id ? $json : linkedOrder;\nconst inputItem = $input.item;\n\nfunction xmlEsc(value) {\n return String(value ?? '')\n .replace(\n /&(?!amp;|lt;|gt;|quot;|apos;|#\\d+;|#x[0-9A-Fa-f]+;)/g,\n '&amp;',\n )\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\nfunction tag(name, value, attrs = '') {\n if (\n value === undefined ||\n value === null ||\n String(value).trim() === ''\n ) {\n return '';\n }\n\n const attrText = attrs ? ` ${attrs}` : '';\n return `<${name}${attrText}>${xmlEsc(value)}</${name}>`;\n}\n\nfunction emptyTag(name, attrs = '') {\n const attrText = attrs ? ` ${attrs}` : '';\n return `<${name}${attrText} />`;\n}\n\nfunction safeFilePart(value) {\n return String(value ?? '')\n .replace(/[^a-zA-Z0-9._-]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .slice(0, 100) || 'order';\n}\n\nfunction block(name, lines, indent = ' ') {\n const present = lines.filter(Boolean);\n\n if (!present.length) {\n return '';\n }\n\n return [\n `${indent}<${name}>`,\n ...present.map(line => `${indent} ${line}`),\n `${indent}</${name}>`,\n ].join('\\n');\n}\n\n/**\n * Zet een PostgreSQL-/ISO-datum om naar de lokale kalenderdatum\n * in Nederland.\n *\n * Voorbeeld:\n * 2026-08-02T22:00:00.000Z -> 2026-08-03\n */\nfunction formatDate(value) {\n if (value === undefined || value === null || value === '') {\n return null;\n }\n\n const text = String(value).trim();\n\n // Een datum zonder tijd kan direct worden gebruikt.\n const dateOnly = text.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n\n if (dateOnly) {\n return `${dateOnly[1]}-${dateOnly[2]}-${dateOnly[3]}`;\n }\n\n const date = new Date(text);\n\n if (Number.isNaN(date.getTime())) {\n throw new Error(`Ongeldige datum ontvangen: ${text}`);\n }\n\n const parts = new Intl.DateTimeFormat('en-CA', {\n timeZone: 'Europe/Amsterdam',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n }).formatToParts(date);\n\n const result = Object.fromEntries(\n parts\n .filter(part => part.type !== 'literal')\n .map(part => [part.type, part.value]),\n );\n\n return `${result.year}-${result.month}-${result.day}`;\n}\n\nconst pickupDate = formatDate(j.pickup_date);\nconst deliveryDate = formatDate(j.delivery_date);\nconst deliveryDateTill = formatDate(\n j.delivery_date_till || j.delivery_date,\n);\n\nlet pakbonBase64 = '';\n\nif (!j.is_cancelled && inputItem.binary?.pakbon) {\n const buffer = await this.helpers.getBinaryDataBuffer(0, 'pakbon');\n pakbonBase64 = buffer.toString('base64');\n}\n\nconst rootOptional = [\n tag('ediprovider_id', CFG.ediproviderId),\n tag('company_id', CFG.companyId),\n].filter(Boolean);\n\nconst bookingOptional = [\n tag('department_id', CFG.departmentId),\n].filter(Boolean);\n\nconst shipmentOptional = [\n tag(\n 'shipmentkind_id',\n CFG.shipmentKindId,\n 'matchmode=\"1\"',\n ),\n].filter(Boolean);\n\nconst pickup = block(\n 'pickupaddress',\n [\n emptyTag('address_id', 'matchmode=\"11\"'),\n tag('date', pickupDate),\n tag('datetill', pickupDate),\n tag('time', j.pickup_time),\n tag('timetill', j.pickup_time_till),\n tag('name', j.pickup_name),\n tag('address1', j.pickup_address1),\n tag('zipcode', j.pickup_zipcode),\n tag('city_id', j.pickup_city, 'matchmode=\"4\"'),\n tag('country_id', j.pickup_country, 'matchmode=\"2\"'),\n tag('phone', j.pickup_phone),\n\n // Transportnummer is de laadreferentie.\n tag('reference', j.transport_number),\n\n // De naam/code van het afleveradres komt als chauffeursinformatie\n // bij het laadadres.\n tag('driverinfo', j.delivery_name),\n ],\n ' ',\n);\n\nconst delivery = block(\n 'deliveryaddress',\n [\n emptyTag('address_id', 'matchmode=\"11\"'),\n tag('date', deliveryDate),\n tag('datetill', deliveryDateTill),\n tag('time', j.delivery_time),\n tag('timetill', j.delivery_time_till),\n tag('name', j.delivery_name),\n tag('address1', j.delivery_address1),\n tag('zipcode', j.delivery_zipcode),\n tag('city_id', j.deliveLine truncated "jsCode": "// Per item uitvoeren.\n// Deze node ontvangt binnen de loop exact één order.\n// Bij een order met pakbon is de order-JSON door de Merge-node gecombineerd\n// met binary.pakbon. Bij een order zonder pakbon komt de order direct binnen.\n\nconst CFG = {\n customerId: '60971',\n departmentId: '',\n shipmentKindId: '',\n ediproviderId: '',\n companyId: '1',\n unitMatchmode: '1',\n documentTypeId: 'PAKBON',\n documentTypeMatchmode: '1',\n ftpFolder: '/test/ToTP/TPE',\n\n deliveryDriverInfo: [\n 'Transport flowers at +4°C to +6°C – Transport plants at +15°C –',\n 'When transporting a mixed load of flowers and plants, set the temperature to +12°C.',\n 'Exchange the pallets at the unloading location using PAKi eVoucher number 048849 04-CAMBRAI',\n ].join(' '),\n};\n\nconst inputItem = $input.item;\nconst j = inputItem.json ?? {};\n\nif (!j.order_id) {\n throw new Error('Order-ID ontbreekt in de huidige loop-iteratie.');\n}\n\nif (!j.transport_number) {\n throw new Error(`Transportnummer ontbreekt voor order ${j.order_id}.`);\n}\n\nif (\n j.pakbon_id &&\n !j.is_cancelled &&\n !String(j.pakbon_file_path ?? '').trim()\n) {\n throw new Error(\n `Pakbonpad ontbreekt voor transportnummer ${j.transport_number}.`,\n );\n}\n\nfunction xmlEsc(value) {\n return String(value ?? '')\n .replace(\n /&(?!amp;|lt;|gt;|quot;|apos;|#\\d+;|#x[0-9A-Fa-f]+;)/g,\n '&amp;',\n )\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\nfunction tag(name, value, attrs = '') {\n if (\n value === undefined ||\n value === null ||\n String(value).trim() === ''\n ) {\n return '';\n }\n\n const attrText = attrs ? ` ${attrs}` : '';\n return `<${name}${attrText}>${xmlEsc(value)}</${name}>`;\n}\n\nfunction emptyTag(name, attrs = '') {\n const attrText = attrs ? ` ${attrs}` : '';\n return `<${name}${attrText} />`;\n}\n\nfunction safeFilePart(value) {\n return String(value ?? '')\n .replace(/[^a-zA-Z0-9._-]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .slice(0, 100) || 'order';\n}\n\nfunction block(name, lines, indent = ' ') {\n const present = lines.filter(Boolean);\n\n if (!present.length) {\n return '';\n }\n\n return [\n `${indent}<${name}>`,\n ...present.map(line => `${indent} ${line}`),\n `${indent}</${name}>`,\n ].join('\\n');\n}\n\n/**\n * Zet een PostgreSQL-/ISO-datum om naar de lokale kalenderdatum\n * in Nederland.\n *\n * Voorbeeld:\n * 2026-08-02T22:00:00.000Z -> 2026-08-03\n */\nfunction formatDate(value) {\n if (value === undefined || value === null || value === '') {\n return null;\n }\n\n const text = String(value).trim();\n\n // Een datum zonder tijd kan direct worden gebruikt.\n const dateOnly = text.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n\n if (dateOnly) {\n return `${dateOnly[1]}-${dateOnly[2]}-${dateOnly[3]}`;\n }\n\n const date = new Date(text);\n\n if (Number.isNaN(date.getTime())) {\n throw new Error(`Ongeldige datum ontvangen: ${text}`);\n }\n\n const parts = new Intl.DateTimeFormat('en-CA', {\n timeZone: 'Europe/Amsterdam',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n }).formatToParts(date);\n\n const result = Object.fromEntries(\n parts\n .filter(part => part.type !== 'literal')\n .map(part => [part.type, part.value]),\n );\n\n return `${result.year}-${result.month}-${result.day}`;\n}\n\nconst pickupDate = formatDate(j.pickup_date);\nconst deliveryDate = formatDate(j.delivery_date);\nconst deliveryDateTill = formatDate(\n j.delivery_date_till || j.delivery_date,\n);\n\nlet pakbonBase64 = '';\n\nif (!j.is_cancelled && inputItem.binary?.pakbon) {\n const buffer = await this.helpers.getBinaryDataBuffer(0, 'pakbon');\n pakbonBase64 = buffer.toString('base64');\n}\n\nconst rootOptional = [\n tag('ediprovider_id', CFG.ediproviderId),\n tag('company_id', CFG.companyId),\n].filter(Boolean);\n\nconst bookingOptional = [\n tag('department_id', CFG.departmentId),\n].filter(Boolean);\n\nconst shipmentOptional = [\n tag(\n 'shipmentkind_id',\n CFG.shipmentKindId,\n 'matchmode=\"1\"',\n ),\n].filter(Boolean);\n\nconst pickup = block(\n 'pickupaddress',\n [\n emptyTag('address_id', 'matchmode=\"11\"'),\n tag('date', pickupDate),\n tag('datetill', pickupDate),\n tag('time', j.pickup_time),\n tag('timetill', j.pickup_time_till),\n tag('name', j.pickup_name),\n tag('address1', j.pickup_address1),\n tag('zipcode', j.pickup_zipcode),\n tag('city_id', j.pickup_city, 'matchmode=\"4\"'),\n tag('country_id', j.pickup_country, 'matchmode=\"2\"'),\n tag('phone', j.pickup_phone),\n\n // Transportnummer is de laadreferentie.\n tag('reference', j.transport_number),\n\n // De naam/code van het afleveradres komt als chauffeursinformatie\n // bij het laadadres.\n tag('driverinfo', j.delivery_name),\n ],\n ' ',\n);\n\nconst delivery Line truncated
}, },
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
3632, 3600,
320 240
], ],
"id": "13565bdb-edfc-4501-b165-741a6c86ddd6", "id": "35b1334e-b9fc-47d0-8e0d-d0b15006ac43",
"name": "Bouw Transpas TPE XML", "name": "Bouw Transpas TPE XML",
"onError": "continueErrorOutput" "notesInFlow": true,
"onError": "continueErrorOutput",
"notes": "Ontvangt per iteratie één order. Bij een pakbon bevat hetzelfde item zowel de juiste order-JSON als binary.pakbon."
}, },
{ {
"parameters": { "parameters": {
@@ -116,10 +119,10 @@
"type": "n8n-nodes-base.postgres", "type": "n8n-nodes-base.postgres",
"typeVersion": 2.6, "typeVersion": 2.6,
"position": [ "position": [
3872, 3824,
304 240
], ],
"id": "0ce5c72a-9b85-477d-a1cf-f8814821411d", "id": "9fe032cb-b9c3-47c1-ba49-50b8f74e7e76",
"name": "Log exportpoging", "name": "Log exportpoging",
"credentials": { "credentials": {
"postgres": { "postgres": {
@@ -140,10 +143,10 @@
"type": "n8n-nodes-base.ftp", "type": "n8n-nodes-base.ftp",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
4176, 4048,
288 240
], ],
"id": "cddb120b-27ae-4e65-91f5-63e9a78e045d", "id": "6c961ced-bbed-4366-b056-25f798d608da",
"name": "Upload order naar Transpas FTP", "name": "Upload order naar Transpas FTP",
"credentials": { "credentials": {
"ftp": { "ftp": {
@@ -164,10 +167,10 @@
"type": "n8n-nodes-base.postgres", "type": "n8n-nodes-base.postgres",
"typeVersion": 2.6, "typeVersion": 2.6,
"position": [ "position": [
4576, 4272,
176 240
], ],
"id": "74d1eabc-f4c6-4bf3-9e54-393da0f7a5d0", "id": "67539e73-d05a-4ed6-8a95-1f9362c30f63",
"name": "Markeer export geslaagd", "name": "Markeer export geslaagd",
"credentials": { "credentials": {
"postgres": { "postgres": {
@@ -185,10 +188,10 @@
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
4464, 4272,
352 672
], ],
"id": "99f0884c-a60c-4b8d-b142-72176bf12618", "id": "36e474cd-f4aa-4e10-bce1-803f8bb3cf23",
"name": "Maak FTP foutmelding", "name": "Maak FTP foutmelding",
"onError": "continueErrorOutput" "onError": "continueErrorOutput"
}, },
@@ -203,10 +206,10 @@
"type": "n8n-nodes-base.postgres", "type": "n8n-nodes-base.postgres",
"typeVersion": 2.6, "typeVersion": 2.6,
"position": [ "position": [
4752, 4496,
336 672
], ],
"id": "ed112513-d2d0-4f4c-ad41-adee052c4bff", "id": "7ac9974c-3c70-47c8-a5f1-89b5de72cf1a",
"name": "Markeer exportfout", "name": "Markeer exportfout",
"credentials": { "credentials": {
"postgres": { "postgres": {
@@ -244,10 +247,10 @@
"type": "n8n-nodes-base.filter", "type": "n8n-nodes-base.filter",
"typeVersion": 2.2, "typeVersion": 2.2,
"position": [ "position": [
3184, 2928,
224 144
], ],
"id": "4d787260-1d2c-47d6-b2ac-022636fecd5a", "id": "a2edfac1-53f7-48db-909f-b1ff0eee2ea8",
"name": "Met opgeslagen pakbon" "name": "Met opgeslagen pakbon"
}, },
{ {
@@ -278,10 +281,10 @@
"type": "n8n-nodes-base.filter", "type": "n8n-nodes-base.filter",
"typeVersion": 2.2, "typeVersion": 2.2,
"position": [ "position": [
3312, 3376,
400 432
], ],
"id": "ebc1c06d-376d-4268-ba8b-a4014de05a94", "id": "7e99ea48-ec49-45ad-b118-ac09f750a2f8",
"name": "Zonder pakbonbestand" "name": "Zonder pakbonbestand"
}, },
{ {
@@ -296,10 +299,10 @@
"type": "n8n-nodes-base.readWriteFile", "type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1.1, "typeVersion": 1.1,
"position": [ "position": [
3408, 3184,
224 -32
], ],
"id": "b38a126c-6526-4122-909f-e783d9f582af", "id": "bbcf61d6-f82c-463e-970f-f01eb7ee6612",
"name": "Lees pakbon van NAS", "name": "Lees pakbon van NAS",
"onError": "continueErrorOutput" "onError": "continueErrorOutput"
}, },
@@ -330,10 +333,10 @@
"type": "n8n-nodes-base.filter", "type": "n8n-nodes-base.filter",
"typeVersion": 2.2, "typeVersion": 2.2,
"position": [ "position": [
1408, 1360,
400 576
], ],
"id": "5d1963ea-8bd8-44e8-974b-3e4d96f190f7", "id": "d723f334-765a-46fd-8832-9b7a9fb3c45f",
"name": "Bestand is CSV" "name": "Bestand is CSV"
}, },
{ {
@@ -343,10 +346,10 @@
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
1632, 1584,
400 576
], ],
"id": "f8b5ca41-0887-4ded-a493-49a29a783dba", "id": "7d11ffdd-b21f-4e02-b52f-9b3833ac43fc",
"name": "Detecteer CSV scheidingsteken", "name": "Detecteer CSV scheidingsteken",
"notesInFlow": true, "notesInFlow": true,
"onError": "continueErrorOutput", "onError": "continueErrorOutput",
@@ -366,10 +369,10 @@
"type": "n8n-nodes-base.extractFromFile", "type": "n8n-nodes-base.extractFromFile",
"typeVersion": 1.1, "typeVersion": 1.1,
"position": [ "position": [
1904, 1808,
384 576
], ],
"id": "ca5eeac7-94ec-4299-b785-152f774d6dda", "id": "26c9ac30-a1c9-483d-b9cf-9df73dbd7f3c",
"name": "Lees CSV op kolompositie", "name": "Lees CSV op kolompositie",
"notesInFlow": true, "notesInFlow": true,
"onError": "continueErrorOutput", "onError": "continueErrorOutput",
@@ -403,10 +406,10 @@
"type": "n8n-nodes-base.filter", "type": "n8n-nodes-base.filter",
"typeVersion": 2.2, "typeVersion": 2.2,
"position": [ "position": [
2960, 2704,
512 432
], ],
"id": "84799366-a4ce-4edd-a5b9-67e3233eae48", "id": "4d3d3c22-20fe-4704-ba01-8ca02895a694",
"name": "Alleen ongewijzigde revisies", "name": "Alleen ongewijzigde revisies",
"notesInFlow": true, "notesInFlow": true,
"notes": "Ook een mail zonder nieuwe Transpas-export is correct verwerkt en krijgt daarom de groene categorie." "notes": "Ook een mail zonder nieuwe Transpas-export is correct verwerkt en krijgt daarom de groene categorie."
@@ -425,9 +428,9 @@
"typeVersion": 1.2, "typeVersion": 1.2,
"position": [ "position": [
16, 16,
416 576
], ],
"id": "685995b6-e0ed-4949-bbd8-d06eddd2f996", "id": "73618086-2fda-4f64-8f61-6b8bd103b559",
"name": "Controleer FTP elke 5 minuten" "name": "Controleer FTP elke 5 minuten"
}, },
{ {
@@ -448,9 +451,9 @@
"typeVersion": 3.4, "typeVersion": 3.4,
"position": [ "position": [
240, 240,
416 576
], ],
"id": "0267471f-1841-49c5-8fdc-c62f28df5de8", "id": "ba1e6552-09b5-4892-a405-f4ab64ec8add",
"name": "FTP bronmap instellen", "name": "FTP bronmap instellen",
"notesInFlow": true, "notesInFlow": true,
"notes": "Vul hier de FTP-map in waarin Lidl de XLSX-, XLS- of CSV-bestanden plaatst." "notes": "Vul hier de FTP-map in waarin Lidl de XLSX-, XLS- of CSV-bestanden plaatst."
@@ -465,9 +468,9 @@
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
464, 464,
416 576
], ],
"id": "178fe9f0-1c61-4d19-b94b-18c784338601", "id": "16229f15-ce52-42e8-b458-76aff2d5646c",
"name": "Toon bestanden in FTP-map", "name": "Toon bestanden in FTP-map",
"credentials": { "credentials": {
"ftp": { "ftp": {
@@ -484,9 +487,9 @@
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
688, 688,
416 576
], ],
"id": "342f1aa0-48cb-4635-8744-467be0439c1d", "id": "f08457e0-77bf-44a3-8325-960526ce4c7e",
"name": "Selecteer FTP-bronbestanden", "name": "Selecteer FTP-bronbestanden",
"notesInFlow": true, "notesInFlow": true,
"notes": "Selecteert maximaal één CSV/XLS/XLSX per uitvoering. Zo kan één bronbestand met meerdere zendingregels na afloop maar één keer worden verplaatst." "notes": "Selecteert maximaal één CSV/XLS/XLSX per uitvoering. Zo kan één bronbestand met meerdere zendingregels na afloop maar één keer worden verplaatst."
@@ -500,9 +503,9 @@
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
912, 912,
416 576
], ],
"id": "007781e2-68d5-4e3e-b991-86fc501d2f25", "id": "fb43da1c-4b18-4a6f-9087-a761df10ed72",
"name": "Download bestand van FTP", "name": "Download bestand van FTP",
"credentials": { "credentials": {
"ftp": { "ftp": {
@@ -513,7 +516,7 @@
}, },
{ {
"parameters": { "parameters": {
"content": "## FTP-bron\n\n1. De workflow leest `.xlsx`, `.xls` en `.csv`.\n2. Per workflow-run wordt bewust **één bronbestand** verwerkt.\n3. Het bestand mag meerdere zendingregels bevatten.\n4. Na succesvolle verwerking wordt het bronbestand **één keer** verplaatst naar `verwerkt`.\n5. Een volgend bestand wordt bij de volgende geplande uitvoering opgepakt.", "content": "## Verwerking per order\n\n1. Per uitvoering wordt één CSV/XLS/XLSX opgehaald.\n2. De orders uit het bestand worden met batchgrootte **1** verwerkt.\n3. Per order wordt apart gecontroleerd of een pakbon aanwezig is.\n4. Bij een pakbon worden order-JSON en binary bestand binnen dezelfde iteratie samengevoegd.\n5. Pas als de loop volledig klaar is, wordt het bronbestand één keer naar `verwerkt` verplaatst.\n6. Bij een exportfout wordt niet naar de loop teruggekeerd en blijft het bronbestand staan.",
"height": 260, "height": 260,
"width": 500 "width": 500
}, },
@@ -523,7 +526,7 @@
0, 0,
0 0
], ],
"id": "99f22c78-9c6f-4853-bb9a-8578f5e32c52", "id": "150cb6c4-801b-4240-b008-4e2c874cd728",
"name": "FTP bron uitleg" "name": "FTP bron uitleg"
}, },
{ {
@@ -531,15 +534,17 @@
"operation": "rename", "operation": "rename",
"oldPath": "={{ $('Selecteer Excel-bijlagen').item.json.source_ftp_path }}", "oldPath": "={{ $('Selecteer Excel-bijlagen').item.json.source_ftp_path }}",
"newPath": "={{ $('FTP bronmap instellen').first().json.ftp_source_folder + '/verwerkt/' + $('Selecteer Excel-bijlagen').item.json.source_file_name }}", "newPath": "={{ $('FTP bronmap instellen').first().json.ftp_source_folder + '/verwerkt/' + $('Selecteer Excel-bijlagen').item.json.source_file_name }}",
"options": {} "options": {
"createDirectories": false
}
}, },
"type": "n8n-nodes-base.ftp", "type": "n8n-nodes-base.ftp",
"typeVersion": 1.1, "typeVersion": 1.1,
"position": [ "position": [
4784, 2704,
80 624
], ],
"id": "3984f2f8-7ca4-4e5a-abe2-025818327ef9", "id": "4ce36db8-27c5-47de-a94e-d985bb22d376",
"name": "Verplaats bronbestand naar verwerkt", "name": "Verplaats bronbestand naar verwerkt",
"executeOnce": true, "executeOnce": true,
"notesInFlow": true, "notesInFlow": true,
@@ -549,7 +554,39 @@
"name": "FTP De Wit Transport" "name": "FTP De Wit Transport"
} }
}, },
"notes": "Voert de rename maar één keer uit. De bronselectie verwerkt maximaal één bestand per workflow-run." "notes": "Wordt alleen vanaf de 'done'-uitgang van de orderloop uitgevoerd. Het CSV-bestand wordt dus pas na alle succesvol verwerkte/ongwijzigde orders één keer verplaatst."
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
2480,
576
],
"id": "b24f676a-73c6-49ec-9b5a-4f47706d041c",
"name": "Verwerk orders één voor één",
"notesInFlow": true,
"notes": "Verwerkt steeds precies één order. Pas nadat deze order volledig klaar is, wordt de volgende order uit het CSV-bestand gestart."
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineByPosition",
"options": {}
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
3376,
144
],
"id": "64f5e12b-2d12-41fa-ba03-86056c187fe3",
"name": "Combineer order met gelezen pakbon",
"notesInFlow": true,
"notes": "Combineert binnen de loop de JSON van de huidige order met het binary pakbonbestand. Omdat de loop batchgrootte 1 is, kunnen order en document niet met een andere order mengen."
} }
], ],
"pinData": {}, "pinData": {},
@@ -613,12 +650,7 @@
"main": [ "main": [
[ [
{ {
"node": "Alleen nieuwe of gewijzigde revisies", "node": "Verwerk orders één voor één",
"type": "main",
"index": 0
},
{
"node": "Alleen ongewijzigde revisies",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
@@ -644,6 +676,11 @@
"Met opgeslagen pakbon": { "Met opgeslagen pakbon": {
"main": [ "main": [
[ [
{
"node": "Combineer order met gelezen pakbon",
"type": "main",
"index": 0
},
{ {
"node": "Lees pakbon van NAS", "node": "Lees pakbon van NAS",
"type": "main", "type": "main",
@@ -656,9 +693,9 @@
"main": [ "main": [
[ [
{ {
"node": "Bouw Transpas TPE XML", "node": "Combineer order met gelezen pakbon",
"type": "main", "type": "main",
"index": 0 "index": 1
} }
] ]
] ]
@@ -781,6 +818,17 @@
] ]
}, },
"Markeer export geslaagd": { "Markeer export geslaagd": {
"main": [
[
{
"node": "Verwerk orders één voor één",
"type": "main",
"index": 0
}
]
]
},
"Verwerk orders één voor één": {
"main": [ "main": [
[ [
{ {
@@ -788,6 +836,40 @@
"type": "main", "type": "main",
"index": 0 "index": 0
} }
],
[
{
"node": "Alleen nieuwe of gewijzigde revisies",
"type": "main",
"index": 0
},
{
"node": "Alleen ongewijzigde revisies",
"type": "main",
"index": 0
}
]
]
},
"Combineer order met gelezen pakbon": {
"main": [
[
{
"node": "Bouw Transpas TPE XML",
"type": "main",
"index": 0
}
]
]
},
"Alleen ongewijzigde revisies": {
"main": [
[
{
"node": "Verwerk orders één voor één",
"type": "main",
"index": 0
}
] ]
] ]
} }
@@ -798,11 +880,11 @@
"binaryMode": "separate", "binaryMode": "separate",
"availableInMCP": false "availableInMCP": false
}, },
"versionId": "4f45ad38-9fb7-4099-be89-17417dcdf73a", "versionId": "98e992f4-4db0-441d-87a8-e42df73acb21",
"meta": { "meta": {
"instanceId": "bef8d409866a58c0777dfe7cca1b9c2400fd051c056d361501393ab423006b5f" "instanceId": "bef8d409866a58c0777dfe7cca1b9c2400fd051c056d361501393ab423006b5f"
}, },
"nodeGroups": [], "nodeGroups": [],
"id": "LmKzYJsq8CIGXvYJ", "id": "bA4pz9VBLu3t8tX0",
"tags": [] "tags": []
} }