diff --git a/Flower_direct/Lidl Frankrijk - Opdracht naar Postgres en Transpas.json b/Flower_direct/Lidl Frankrijk - Opdracht naar Postgres en Transpas.json index 5896cc0..e765da7 100644 --- a/Flower_direct/Lidl Frankrijk - Opdracht naar Postgres en Transpas.json +++ b/Flower_direct/Lidl Frankrijk - Opdracht naar Postgres en Transpas.json @@ -1,5 +1,5 @@ { - "name": "Lidl Frankrijk - Opdracht naar Postgres en Transpas -", + "name": "Lidl Frankrijk - Opdracht naar Postgres en Transpas", "nodes": [ { "parameters": { @@ -9,9 +9,9 @@ "typeVersion": 2, "position": [ 1136, - 416 + 576 ], - "id": "2d78e121-cdb5-48b6-a98b-0aeec510cbb0", + "id": "798a0f29-9cfd-4e9e-951b-ef9ab1252c2e", "name": "Selecteer Excel-bijlagen", "notesInFlow": true, "onError": "continueErrorOutput", @@ -24,17 +24,17 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 2352, - 368 + 2032, + 576 ], - "id": "6d4504e4-7b7b-4369-827c-2cee9951b0cf", + "id": "d28f2932-ef00-494c-b882-8797e5823abf", "name": "Normaliseer en valideer orders", "onError": "continueErrorOutput" }, { "parameters": { "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 external_document_branch = EXCLUDED.external_document_branch,\n forwarder = EXCLUDED.forwarder,\n gross_weight = EXCLUDED.gross_weight,\n source_file_name = EXCLUDED.source_file_name,\n source_message_id = EXCLUDED.source_message_id,\n source_subject = EXCLUDED.source_subject,\n source_from = EXCLUDED.source_from,\n source_received_at = EXCLUDED.source_received_at,\n raw_row = EXCLUDED.raw_row,\n last_seen_at = now(),\n status_changed_at = CASE\n WHEN existing.current_status IS DISTINCT FROM EXCLUDED.current_status\n OR existing.is_cancelled IS DISTINCT FROM EXCLUDED.is_cancelled\n THEN now()\n ELSE existing.status_changed_at\n END,\n reactivated_at = CASE\n WHEN existing.is_cancelled = true AND EXCLUDED.is_cancelled = false\n THEN now()\n ELSE existing.reactivated_at\n END,\n last_error = CASE\n WHEN existing.current_payload_hash IS DISTINCT FROM EXCLUDED.current_payload_hash\n THEN NULL\n ELSE existing.last_error\n END\n RETURNING *\n),\nclassified AS MATERIALIZED (\n SELECT\n u.*,\n CASE\n WHEN p.id IS NULL THEN 'created'\n WHEN p.is_cancelled = false AND u.is_cancelled = true THEN 'cancelled'\n WHEN p.is_cancelled = true AND u.is_cancelled = false THEN 'reactivated'\n WHEN p.current_payload_hash IS DISTINCT FROM u.current_payload_hash THEN 'updated'\n ELSE 'unchanged'\n END AS event_type,\n (p.id IS NULL OR p.current_payload_hash IS DISTINCT FROM u.current_payload_hash) AS state_changed\n FROM upserted u\n LEFT JOIN previous p ON p.id = u.id\n),\nevent_insert AS (\n INSERT INTO public.lidl_fr_order_events (\n order_id, transport_number, revision, event_type, status,\n is_cancelled, payload_hash, source_file_name, source_message_id, row_data\n )\n SELECT\n c.id, c.transport_number, c.revision, c.event_type, c.current_status,\n c.is_cancelled, c.current_payload_hash, c.source_file_name,\n c.source_message_id, c.raw_row\n FROM classified c\n WHERE c.state_changed\n ON CONFLICT (transport_number, revision) DO NOTHING\n RETURNING id\n),\nlatest_document AS MATERIALIZED (\n SELECT d.*\n FROM public.lidl_fr_documents d\n JOIN classified c ON c.transport_number = d.transport_number\n WHERE d.document_kind = 'PAKBON'\n AND d.active = true\n AND d.storage_status = 'stored'\n AND d.file_path IS NOT NULL\n ORDER BY d.received_at DESC, d.id DESC\n LIMIT 1\n)\nSELECT\n c.id AS order_id,\n c.transport_number,\n c.current_status,\n c.is_cancelled,\n c.revision,\n c.current_payload_hash AS payload_hash,\n c.event_type,\n (c.revision > COALESCE(c.last_transpas_export_revision, 0)) AS should_export,\n CASE\n WHEN c.is_cancelled THEN 'DELETE'\n WHEN c.has_active_export = false THEN 'NEW'\n ELSE 'UPDATE'\n END AS transpas_action,\n\n c.pickup_date,\n to_char(c.pickup_time, 'HH24:MI') AS pickup_time,\n to_char(c.pickup_time_till, 'HH24:MI') AS pickup_time_till,\n c.pickup_name,\n c.pickup_address1,\n c.pickup_zipcode,\n c.pickup_city,\n c.pickup_phone,\n c.pickup_country,\n\n c.delivery_date,\n c.delivery_date_till,\n to_char(c.delivery_time, 'HH24:MI') AS delivery_time,\n to_char(c.delivery_time_till, 'HH24:MI') AS delivery_time_till,\n c.delivery_name,\n c.delivery_address1,\n c.delivery_zipcode,\n c.delivery_city,\n c.delivery_country,\n\n c.transport_code,\n c.colli,\n c.loose_boxes,\n c.transport_units,\n c.product_group,\n c.load_carrier_count,\n c.load_carrier_type,\n c.temperature_group,\n c.min_temperature,\n c.max_temperature,\n c.reference_text,\n c.external_document_number,\n c.external_document_branch,\n c.forwarder,\n c.gross_weight,\n c.source_file_name,\n\n d.id AS pakbon_id,\n d.file_name AS pakbon_file_name,\n d.stored_file_name AS pakbon_stored_file_name,\n d.file_path AS pakbon_file_path,\n d.file_type AS pakbon_file_type,\n d.mime_type AS pakbon_mime_type,\n d.document_hash AS pakbon_document_hash\nFROM classified c\nLEFT JOIN latest_document d ON true;", + "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 external_document_branch = EXCLUDED.external_document_branch,\n forwarder = EXCLUDED.forwarder,\n gross_weight = EXCLUDED.gross_weight,\n source_file_name = EXCLUDED.source_file_name,\n source_message_id = EXCLUDED.source_message_id,\n source_subject = EXCLUDED.source_subject,\n source_from = EXCLUDED.source_from,\n source_received_at = EXCLUDED.source_received_at,\n raw_row = EXCLUDED.raw_row,\n last_seen_at = now(),\n status_changed_at = CASE\n WHEN existing.current_status IS DISTINCT FROM EXCLUDED.current_status\n OR existing.is_cancelled IS DISTINCT FROM EXCLUDED.is_cancelled\n THEN now()\n ELSE existing.status_changed_at\n END,\n reactivated_at = CASE\n WHEN existing.is_cancelled = true AND EXCLUDED.is_cancelled = false\n THEN now()\n ELSE existing.reactivated_at\n END,\n last_error = CASE\n WHEN existing.current_payload_hash IS DISTINCT FROM EXCLUDED.current_payload_hash\n THEN NULL\n ELSE existing.last_error\n END\n RETURNING *\n),\nclassified AS MATERIALIZED (\n SELECT\n u.*,\n CASE\n WHEN p.id IS NULL THEN 'created'\n WHEN p.is_cancelled = false AND u.is_cancelled = true THEN 'cancelled'\n WHEN p.is_cancelled = true AND u.is_cancelled = false THEN 'reactivated'\n WHEN p.current_payload_hash IS DISTINCT FROM u.current_payload_hash THEN 'updated'\n ELSE 'unchanged'\n END AS event_type,\n (p.id IS NULL OR p.current_payload_hash IS DISTINCT FROM u.current_payload_hash) AS state_changed\n FROM upserted u\n LEFT JOIN previous p ON p.id = u.id\n),\nevent_insert AS (\n INSERT INTO public.lidl_fr_order_events (\n order_id, transport_number, revision, event_type, status,\n is_cancelled, payload_hash, source_file_name, source_message_id, row_data\n )\n SELECT\n c.id, c.transport_number, c.revision, c.event_type, c.current_status,\n c.is_cancelled, c.current_payload_hash, c.source_file_name,\n c.source_message_id, c.raw_row\n FROM classified c\n WHERE c.state_changed\n ON CONFLICT (transport_number, revision) DO NOTHING\n RETURNING id\n)\nSELECT\n c.id AS order_id,\n c.transport_number,\n c.current_status,\n c.is_cancelled,\n c.revision,\n c.current_payload_hash AS payload_hash,\n c.event_type,\n (c.revision > COALESCE(c.last_transpas_export_revision, 0)) AS should_export,\n CASE\n WHEN c.is_cancelled THEN 'DELETE'\n WHEN c.has_active_export = false THEN 'NEW'\n ELSE 'UPDATE'\n END AS transpas_action,\n\n c.pickup_date,\n to_char(c.pickup_time, 'HH24:MI') AS pickup_time,\n to_char(c.pickup_time_till, 'HH24:MI') AS pickup_time_till,\n c.pickup_name,\n c.pickup_address1,\n c.pickup_zipcode,\n c.pickup_city,\n c.pickup_phone,\n c.pickup_country,\n\n c.delivery_date,\n c.delivery_date_till,\n to_char(c.delivery_time, 'HH24:MI') AS delivery_time,\n to_char(c.delivery_time_till, 'HH24:MI') AS delivery_time_till,\n c.delivery_name,\n c.delivery_address1,\n c.delivery_zipcode,\n c.delivery_city,\n c.delivery_country,\n\n c.transport_code,\n c.colli,\n c.loose_boxes,\n c.transport_units,\n c.product_group,\n c.load_carrier_count,\n c.load_carrier_type,\n c.temperature_group,\n c.min_temperature,\n c.max_temperature,\n c.reference_text,\n c.external_document_number,\n c.external_document_branch,\n c.forwarder,\n c.gross_weight,\n c.source_file_name,\n\n d.id AS pakbon_id,\n d.file_name AS pakbon_file_name,\n d.stored_file_name AS pakbon_stored_file_name,\n d.file_path AS pakbon_file_path,\n d.file_type AS pakbon_file_type,\n d.mime_type AS pakbon_mime_type,\n d.document_hash AS pakbon_document_hash\nFROM classified c\nLEFT JOIN LATERAL (\n SELECT doc.*\n FROM public.lidl_fr_documents doc\n WHERE doc.transport_number = c.transport_number\n AND doc.document_kind = 'PAKBON'\n AND doc.active = true\n AND doc.storage_status = 'stored'\n AND doc.file_path IS NOT NULL\n ORDER BY doc.received_at DESC, doc.id DESC\n LIMIT 1\n) d ON true;", "options": { "queryReplacement": "={{ [JSON.stringify($json)] }}" } @@ -42,18 +42,20 @@ "type": "n8n-nodes-base.postgres", "typeVersion": 2.6, "position": [ - 2672, - 352 + 2256, + 576 ], - "id": "8ef1a7e3-819c-46c5-b04c-cc1db01de2d7", + "id": "65f00226-090b-45a2-a401-493fd409474c", "name": "Upsert order status en zoek pakbon", + "notesInFlow": true, "credentials": { "postgres": { "id": "m4DQjg3b1iDYGlBp", "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": { @@ -83,27 +85,28 @@ "type": "n8n-nodes-base.filter", "typeVersion": 2.2, "position": [ - 2960, - 320 + 2704, + 240 ], - "id": "fa659a1a-13b4-4897-b2e6-e637da769216", - "name": "Alleen nieuwe of gewijzigde revisies", - "disabled": true + "id": "fc63eb5b-448a-4d40-a6c4-9b21a516c261", + "name": "Alleen nieuwe of gewijzigde revisies" }, { "parameters": { "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 '&',\n )\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\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)}`;\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}`,\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.delivery_city, 'matchmode=\"4\"'),\n tag('country_id', j.delivery_country, 'matchmode=\"2\"'),\n\n // Transportnummer is ook de losreferentie.\n tag('reference', j.transport_number),\n\n // Vaste Lidl-instructie bij het losadres.\n tag('driverinfo', CFG.deliveryDriverInfo),\n ],\n ' ',\n);\n\nconst sender = block(\n 'sender',\n [\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 ' ',\n);\n\nconst receiver = block(\n 'receiver',\n [\n tag('name', j.delivery_name),\n tag('address1', j.delivery_address1),\n tag('zipcode', j.delivery_zipcode),\n tag('city_id', j.delivery_city, 'matchmode=\"4\"'),\n tag('country_id', j.delivery_country, 'matchmode=\"2\"'),\n ],\n ' ',\n);\n\nconst cargo = block(\n 'cargo',\n [\n tag('colli', j.colli),\n tag(\n 'unitamount',\n j.load_carrier_count ?? j.transport_units,\n ),\n tag(\n 'unit_id',\n j.load_carrier_type,\n `matchmode=\"${CFG.unitMatchmode}\"`,\n ),\n tag('product_id', j.product_group, 'matchmode=\"1\"'),\n tag('productdescription', j.product_group),\n tag('weight', j.gross_weight),\n tag('mintemperature', j.min_temperature),\n tag('maxtemperature', j.max_temperature),\n ],\n ' ',\n);\n\nconst includePakbon =\n !j.is_cancelled &&\n Boolean(pakbonBase64) &&\n Boolean(j.pakbon_file_name);\n\nlet documents = '';\n\nif (includePakbon) {\n documents = [\n ' ',\n ' ',\n ` ${tag(\n 'documenttype_id',\n CFG.documentTypeId,\n `matchmode=\"${CFG.documentTypeMatchmode}\"`,\n )}`,\n ` ${tag('filename', j.pakbon_file_name)}`,\n ` ${tag('filedata', pakbonBase64)}`,\n ` ${tag('reference', j.transport_number)}`,\n ` ${tag('concerns', 'Pakbon')}`,\n ' ',\n ' ',\n ].join('\\n');\n}\n\nconst xmlLines = [\n '',\n '',\n ' ',\n\n ...rootOptional.map(value => ` ${value}`),\n\n ` ${tag(\n 'ediproviderstatus_id',\n j.transpas_action,\n 'matchmode=\"1\"',\n )}`,\n\n ` ${tag('edireference', j.transport_number)}`,\n ` ${tag('reference', j.transport_number)}`,\n ` ${tag(\n 'customer_id',\n CFG.customerId,\n 'matchmode=\"1\"',\n )}`,\n\n ...bookingOptional.map(value => ` ${value}`),\n\n ' ',\n ' ',\n\n ` ${tag(\n 'plangroup_id',\n '17',\n 'matchmode=\"0\"',\n )}`,\n\n ` ${tag(\n 'edireference',\n j.transport_number,\n )}`,\n\n ` ${tag(\n 'reference',\n j.transport_number,\n )}`,\n\n ` ${tag(\n 'cancellation',\n j.is_cancelled ? 'true' : 'false',\n )}`,\n\n ...shipmentOptional.map(value => ` ${value}`),\n\n sender,\n receiver,\n pickup,\n delivery,\n cargo,\n documents,\n\n ' ',\n ' ',\n ' ',\n '',\n].filter(Boolean);\n\nconst xml = xmlLines.join('\\n');\n\nconst stamp = $now\n .setZone('Europe/Amsterdam')\n .toFormat('yyyyLLdd_HHmmss_SSS');\n\nconst xmlFileName =\n `${stamp}_` +\n `${safeFilePart(j.transport_number)}_` +\n `R${j.revision}_` +\n `${j.transpas_action}.xml`;\n\nconst ftpFolder = String(CFG.ftpFolder || '').replace(/\\/+$/, '');\n\nreturn {\n json: {\n order_id: j.order_id,\n transport_number: j.transport_number,\n revision: j.revision,\n payload_hash: j.payload_hash,\n transpas_action: j.transpas_action,\n is_cancelled: j.is_cancelled,\n\n pickup_date: pickupDate,\n delivery_date: deliveryDate,\n delivery_date_till: deliveryDateTill,\n\n pakbon_id: j.pakbon_id ?? null,\n pakbon_file_path: j.pakbon_file_path ?? null,\n document_included: includePakbon,\n\n xml_file_name: xmlFileName,\n ftp_path: `${ftpFolder}/${xmlFileName}`,\n xml,\n },\n};" + "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 '&',\n )\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\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)}`;\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}`,\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.delivery_city, 'matchmode=\"4\"'),\n tag('country_id', j.delivery_country, 'matchmode=\"2\"'),\n\n // Transportnummer is ook de losreferentie.\n tag('reference', j.transport_number),\n\n // Vaste Lidl-instructie bij het losadres.\n tag('driverinfo', CFG.deliveryDriverInfo),\n ],\n ' ',\n);\n\nconst sender = block(\n 'sender',\n [\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 ' ',\n);\n\nconst receiver = block(\n 'receiver',\n [\n tag('name', j.delivery_name),\n tag('address1', j.delivery_address1),\n tag('zipcode', j.delivery_zipcode),\n tag('city_id', j.delivery_city, 'matchmode=\"4\"'),\n tag('country_id', j.delivery_country, 'matchmode=\"2\"'),\n ],\n ' ',\n);\n\nconst cargo = block(\n 'cargo',\n [\n tag('colli', j.colli),\n tag(\n 'unitamount',\n j.load_carrier_count ?? j.transport_units,\n ),\n tag(\n 'unit_id',\n j.load_carrier_type,\n `matchmode=\"${CFG.unitMatchmode}\"`,\n ),\n tag('product_id', j.product_group, 'matchmode=\"1\"'),\n tag('productdescription', j.product_group),\n tag('weight', j.gross_weight),\n tag('mintemperature', j.min_temperature),\n tag('maxtemperature', j.max_temperature),\n ],\n ' ',\n);\n\nconst includePakbon =\n !j.is_cancelled &&\n Boolean(pakbonBase64) &&\n Boolean(j.pakbon_file_name);\n\nlet documents = '';\n\nif (includePakbon) {\n documents = [\n ' ',\n ' ',\n ` ${tag(\n 'documenttype_id',\n CFG.documentTypeId,\n `matchmode=\"${CFG.documentTypeMatchmode}\"`,\n )}`,\n ` ${tag('filename', j.pakbon_file_name)}`,\n ` ${tag('filedata', pakbonBase64)}`,\n ` ${tag('reference', j.transport_number)}`,\n ` ${tag('concerns', 'Pakbon')}`,\n ' ',\n ' ',\n ].join('\\n');\n}\n\nconst xmlLines = [\n '',\n '',\n ' ',\n\n ...rootOptional.map(value => ` ${value}`),\n\n ` ${tag(\n 'ediproviderstatus_id',\n j.transpas_action,\n 'matchmode=\"1\"',\n )}`,\n\n ` ${tag('edireference', j.transport_number)}`,\n ` ${tag('reference', j.transport_number)}`,\n ` ${tag(\n 'customer_id',\n CFG.customerId,\n 'matchmode=\"1\"',\n )}`,\n\n ...bookingOptional.map(value => ` ${value}`),\n\n ' ',\n ' ',\n\n ` ${tag(\n 'plangroup_id',\n '17',\n 'matchmode=\"0\"',\n )}`,\n\n ` ${tag(\n 'edireference',\n j.transport_number,\n )}`,\n\n ` ${tag(\n 'reference',\n j.transport_number,\n )}`,\n\n ` ${tag(\n 'cancellation',\n j.is_cancelled ? 'true' : 'false',\n )}`,\n\n ...shipmentOptional.map(value => ` ${value}`),\n\n sender,\n receiver,\n pickup,\n delivery,\n cargo,\n documents,\n\n ' ',\n ' ',\n ' ',\n '',\n].filter(Boolean);\n\nconst xml = xmlLines.join('\\n');\n\nconst stamp = $now\n .setZone('Europe/Amsterdam')\n .toFormat('yyyyLLdd_HHmmss_SSS');\n\nconst xmlFileName =\n `${stamp}_` +\n `${safeFilePart(j.transport_number)}_` +\n `R${j.revision}_` +\n `${j.transpas_action}.xml`;\n\nconst ftpFolder = String(CFG.ftpFolder || '').replace(/\\/+$/, '');\n\nreturn {\n json: {\n order_id: j.order_id,\n transport_number: j.transport_number,\n revision: j.revision,\n payload_hash: j.payload_hash,\n transpas_action: j.transpas_action,\n is_cancelled: j.is_cancelled,\n\n pickup_date: pickupDate,\n delivery_date: deliveryDate,\n delivery_date_till: deliveryDateTill,\n\n pakbon_id: j.pakbon_id ?? null,\n pakbon_file_path: j.pakbon_file_path ?? null,\n document_included: includePakbon,\n\n xml_file_name: xmlFileName,\n ftp_path: `${ftpFolder}/${xmlFileName}`,\n xml,\n },\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3632, - 320 + 3600, + 240 ], - "id": "13565bdb-edfc-4501-b165-741a6c86ddd6", + "id": "35b1334e-b9fc-47d0-8e0d-d0b15006ac43", "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": { @@ -116,10 +119,10 @@ "type": "n8n-nodes-base.postgres", "typeVersion": 2.6, "position": [ - 3872, - 304 + 3824, + 240 ], - "id": "0ce5c72a-9b85-477d-a1cf-f8814821411d", + "id": "9fe032cb-b9c3-47c1-ba49-50b8f74e7e76", "name": "Log exportpoging", "credentials": { "postgres": { @@ -140,10 +143,10 @@ "type": "n8n-nodes-base.ftp", "typeVersion": 1, "position": [ - 4176, - 288 + 4048, + 240 ], - "id": "cddb120b-27ae-4e65-91f5-63e9a78e045d", + "id": "6c961ced-bbed-4366-b056-25f798d608da", "name": "Upload order naar Transpas FTP", "credentials": { "ftp": { @@ -164,10 +167,10 @@ "type": "n8n-nodes-base.postgres", "typeVersion": 2.6, "position": [ - 4576, - 176 + 4272, + 240 ], - "id": "74d1eabc-f4c6-4bf3-9e54-393da0f7a5d0", + "id": "67539e73-d05a-4ed6-8a95-1f9362c30f63", "name": "Markeer export geslaagd", "credentials": { "postgres": { @@ -185,10 +188,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 4464, - 352 + 4272, + 672 ], - "id": "99f0884c-a60c-4b8d-b142-72176bf12618", + "id": "36e474cd-f4aa-4e10-bce1-803f8bb3cf23", "name": "Maak FTP foutmelding", "onError": "continueErrorOutput" }, @@ -203,10 +206,10 @@ "type": "n8n-nodes-base.postgres", "typeVersion": 2.6, "position": [ - 4752, - 336 + 4496, + 672 ], - "id": "ed112513-d2d0-4f4c-ad41-adee052c4bff", + "id": "7ac9974c-3c70-47c8-a5f1-89b5de72cf1a", "name": "Markeer exportfout", "credentials": { "postgres": { @@ -244,10 +247,10 @@ "type": "n8n-nodes-base.filter", "typeVersion": 2.2, "position": [ - 3184, - 224 + 2928, + 144 ], - "id": "4d787260-1d2c-47d6-b2ac-022636fecd5a", + "id": "a2edfac1-53f7-48db-909f-b1ff0eee2ea8", "name": "Met opgeslagen pakbon" }, { @@ -278,10 +281,10 @@ "type": "n8n-nodes-base.filter", "typeVersion": 2.2, "position": [ - 3312, - 400 + 3376, + 432 ], - "id": "ebc1c06d-376d-4268-ba8b-a4014de05a94", + "id": "7e99ea48-ec49-45ad-b118-ac09f750a2f8", "name": "Zonder pakbonbestand" }, { @@ -296,10 +299,10 @@ "type": "n8n-nodes-base.readWriteFile", "typeVersion": 1.1, "position": [ - 3408, - 224 + 3184, + -32 ], - "id": "b38a126c-6526-4122-909f-e783d9f582af", + "id": "bbcf61d6-f82c-463e-970f-f01eb7ee6612", "name": "Lees pakbon van NAS", "onError": "continueErrorOutput" }, @@ -330,10 +333,10 @@ "type": "n8n-nodes-base.filter", "typeVersion": 2.2, "position": [ - 1408, - 400 + 1360, + 576 ], - "id": "5d1963ea-8bd8-44e8-974b-3e4d96f190f7", + "id": "d723f334-765a-46fd-8832-9b7a9fb3c45f", "name": "Bestand is CSV" }, { @@ -343,10 +346,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1632, - 400 + 1584, + 576 ], - "id": "f8b5ca41-0887-4ded-a493-49a29a783dba", + "id": "7d11ffdd-b21f-4e02-b52f-9b3833ac43fc", "name": "Detecteer CSV scheidingsteken", "notesInFlow": true, "onError": "continueErrorOutput", @@ -366,10 +369,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 1904, - 384 + 1808, + 576 ], - "id": "ca5eeac7-94ec-4299-b785-152f774d6dda", + "id": "26c9ac30-a1c9-483d-b9cf-9df73dbd7f3c", "name": "Lees CSV op kolompositie", "notesInFlow": true, "onError": "continueErrorOutput", @@ -403,10 +406,10 @@ "type": "n8n-nodes-base.filter", "typeVersion": 2.2, "position": [ - 2960, - 512 + 2704, + 432 ], - "id": "84799366-a4ce-4edd-a5b9-67e3233eae48", + "id": "4d3d3c22-20fe-4704-ba01-8ca02895a694", "name": "Alleen ongewijzigde revisies", "notesInFlow": true, "notes": "Ook een mail zonder nieuwe Transpas-export is correct verwerkt en krijgt daarom de groene categorie." @@ -425,9 +428,9 @@ "typeVersion": 1.2, "position": [ 16, - 416 + 576 ], - "id": "685995b6-e0ed-4949-bbd8-d06eddd2f996", + "id": "73618086-2fda-4f64-8f61-6b8bd103b559", "name": "Controleer FTP elke 5 minuten" }, { @@ -448,9 +451,9 @@ "typeVersion": 3.4, "position": [ 240, - 416 + 576 ], - "id": "0267471f-1841-49c5-8fdc-c62f28df5de8", + "id": "ba1e6552-09b5-4892-a405-f4ab64ec8add", "name": "FTP bronmap instellen", "notesInFlow": true, "notes": "Vul hier de FTP-map in waarin Lidl de XLSX-, XLS- of CSV-bestanden plaatst." @@ -465,9 +468,9 @@ "typeVersion": 1, "position": [ 464, - 416 + 576 ], - "id": "178fe9f0-1c61-4d19-b94b-18c784338601", + "id": "16229f15-ce52-42e8-b458-76aff2d5646c", "name": "Toon bestanden in FTP-map", "credentials": { "ftp": { @@ -484,9 +487,9 @@ "typeVersion": 2, "position": [ 688, - 416 + 576 ], - "id": "342f1aa0-48cb-4635-8744-467be0439c1d", + "id": "f08457e0-77bf-44a3-8325-960526ce4c7e", "name": "Selecteer FTP-bronbestanden", "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." @@ -500,9 +503,9 @@ "typeVersion": 1, "position": [ 912, - 416 + 576 ], - "id": "007781e2-68d5-4e3e-b991-86fc501d2f25", + "id": "fb43da1c-4b18-4a6f-9087-a761df10ed72", "name": "Download bestand van FTP", "credentials": { "ftp": { @@ -513,7 +516,7 @@ }, { "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, "width": 500 }, @@ -523,7 +526,7 @@ 0, 0 ], - "id": "99f22c78-9c6f-4853-bb9a-8578f5e32c52", + "id": "150cb6c4-801b-4240-b008-4e2c874cd728", "name": "FTP bron uitleg" }, { @@ -531,15 +534,17 @@ "operation": "rename", "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 }}", - "options": {} + "options": { + "createDirectories": false + } }, "type": "n8n-nodes-base.ftp", "typeVersion": 1.1, "position": [ - 4784, - 80 + 2704, + 624 ], - "id": "3984f2f8-7ca4-4e5a-abe2-025818327ef9", + "id": "4ce36db8-27c5-47de-a94e-d985bb22d376", "name": "Verplaats bronbestand naar verwerkt", "executeOnce": true, "notesInFlow": true, @@ -549,7 +554,39 @@ "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": {}, @@ -613,12 +650,7 @@ "main": [ [ { - "node": "Alleen nieuwe of gewijzigde revisies", - "type": "main", - "index": 0 - }, - { - "node": "Alleen ongewijzigde revisies", + "node": "Verwerk orders één voor één", "type": "main", "index": 0 } @@ -644,6 +676,11 @@ "Met opgeslagen pakbon": { "main": [ [ + { + "node": "Combineer order met gelezen pakbon", + "type": "main", + "index": 0 + }, { "node": "Lees pakbon van NAS", "type": "main", @@ -656,9 +693,9 @@ "main": [ [ { - "node": "Bouw Transpas TPE XML", + "node": "Combineer order met gelezen pakbon", "type": "main", - "index": 0 + "index": 1 } ] ] @@ -781,6 +818,17 @@ ] }, "Markeer export geslaagd": { + "main": [ + [ + { + "node": "Verwerk orders één voor één", + "type": "main", + "index": 0 + } + ] + ] + }, + "Verwerk orders één voor één": { "main": [ [ { @@ -788,6 +836,40 @@ "type": "main", "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", "availableInMCP": false }, - "versionId": "4f45ad38-9fb7-4099-be89-17417dcdf73a", + "versionId": "98e992f4-4db0-441d-87a8-e42df73acb21", "meta": { "instanceId": "bef8d409866a58c0777dfe7cca1b9c2400fd051c056d361501393ab423006b5f" }, "nodeGroups": [], - "id": "LmKzYJsq8CIGXvYJ", + "id": "bA4pz9VBLu3t8tX0", "tags": [] } \ No newline at end of file