@flowselections/logistiek-engine 1.0.175 → 1.0.180

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/db/schema/logistiek-engine.sql +2112 -0
  2. package/dist-lib/components/BeheerPage.d.ts.map +1 -1
  3. package/dist-lib/components/BeheerPage.js +2 -1
  4. package/dist-lib/components/BeladingIndelingCard.d.ts.map +1 -1
  5. package/dist-lib/components/BeladingIndelingCard.js +2 -0
  6. package/dist-lib/components/DocumentkeuzeCard.d.ts +2 -0
  7. package/dist-lib/components/DocumentkeuzeCard.d.ts.map +1 -0
  8. package/dist-lib/components/DocumentkeuzeCard.js +18 -0
  9. package/dist-lib/index.d.ts +1 -0
  10. package/dist-lib/index.d.ts.map +1 -1
  11. package/dist-lib/index.js +1 -0
  12. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +11 -5
  13. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
  14. package/dist-lib/integrations/supabase/previewAuthStorage.js +38 -21
  15. package/dist-lib/integrations/supabase/types.d.ts +376 -15
  16. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  17. package/dist-lib/lib/beladingRegels.d.ts +78 -0
  18. package/dist-lib/lib/beladingRegels.d.ts.map +1 -0
  19. package/dist-lib/lib/beladingRegels.js +143 -0
  20. package/dist-lib/lib/bulkLoadCarrierPlanning.server.js +2 -2
  21. package/dist-lib/lib/controlScan.server.js +1 -1
  22. package/dist-lib/lib/eigenTraysticker.d.ts +17 -0
  23. package/dist-lib/lib/eigenTraysticker.d.ts.map +1 -0
  24. package/dist-lib/lib/eigenTraysticker.js +32 -0
  25. package/dist-lib/lib/layoutPrint.server.d.ts +10 -1
  26. package/dist-lib/lib/layoutPrint.server.d.ts.map +1 -1
  27. package/dist-lib/lib/layoutPrint.server.js +19 -1
  28. package/dist-lib/lib/loadCarrierPlanning.server.d.ts +35 -3
  29. package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
  30. package/dist-lib/lib/loadCarrierPlanning.server.js +251 -89
  31. package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
  32. package/dist-lib/lib/modulePrompts.js +233 -0
  33. package/dist-lib/lib/moduleVersion.d.ts +1 -1
  34. package/dist-lib/lib/moduleVersion.js +1 -1
  35. package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
  36. package/dist-lib/lib/planQueue.server.js +26 -2
  37. package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
  38. package/dist-lib/lib/ruleEngineSchema.js +4 -38
  39. package/dist-lib/lib/ruleEngineWizard.d.ts.map +1 -1
  40. package/dist-lib/lib/ruleEngineWizard.js +0 -2
  41. package/dist-lib/lib/shippingNoteQueue.server.js +1 -1
  42. package/dist-lib/lib/stickerQueue.server.d.ts +25 -0
  43. package/dist-lib/lib/stickerQueue.server.d.ts.map +1 -1
  44. package/dist-lib/lib/stickerQueue.server.js +150 -22
  45. package/dist-lib/lib/stickerRules.d.ts +17 -0
  46. package/dist-lib/lib/stickerRules.d.ts.map +1 -1
  47. package/dist-lib/lib/stickerRules.js +30 -0
  48. package/package.json +1 -1
@@ -28261,3 +28261,2115 @@ ON CONFLICT (key) DO NOTHING;
28261
28261
 
28262
28262
  RESET check_function_bodies;
28263
28263
 
28264
+
28265
+ -- ===== 1.0.176 automatische documentkeuze (idempotent: CREATE OR REPLACE / IF NOT EXISTS) =====
28266
+ -- Logistiek Engine 1.0.176: automatische documentkeuze.
28267
+ -- De gebruiker kiest nergens meer een layout. Eén centrale beslisregel:
28268
+ -- logistics_document_for_order(order, documenttype). Bestaat Layouts'
28269
+ -- layout_voor_document, dan alleen die; anders de automatische standaardlayout.
28270
+ -- Oude werkwijze-keuzes (packing_slip_layout e.d.) blijven staan maar worden genegeerd.
28271
+ -- Geen omgevingswaarden, geen datacorrectie.
28272
+
28273
+ -- 1. Eigen stickers in dezelfde wachtrij als de Floriday-stickers.
28274
+ ALTER TABLE public.logistics_sticker_prints ADD COLUMN IF NOT EXISTS eigen_sticker_id uuid;
28275
+ ALTER TABLE public.logistics_sticker_prints DROP CONSTRAINT IF EXISTS logistics_sticker_prints_soort_check;
28276
+ ALTER TABLE public.logistics_sticker_prints ADD CONSTRAINT logistics_sticker_prints_soort_check
28277
+ CHECK (soort = ANY (ARRAY['sticker'::text, 'traysticker'::text, 'eigen_sticker'::text]));
28278
+
28279
+ -- 2. Automatische keuze zonder werkwijze-overrides.
28280
+ CREATE OR REPLACE FUNCTION public.logistics_document_layout_v2(_document_code text, _is_partial boolean DEFAULT false)
28281
+ RETURNS jsonb
28282
+ LANGUAGE plpgsql
28283
+ STABLE SECURITY DEFINER
28284
+ SET search_path TO 'public'
28285
+ AS $function$
28286
+ DECLARE
28287
+ v_types public.doc_layout_type[];
28288
+ v_layout public.document_layouts%ROWTYPE;
28289
+ BEGIN
28290
+ v_types := public.logistics_document_type_map(lower(btrim(coalesce(_document_code, ''))));
28291
+ IF array_length(v_types, 1) IS NULL THEN RETURN NULL; END IF;
28292
+ SELECT * INTO v_layout
28293
+ FROM public.document_layouts
28294
+ WHERE actief IS TRUE AND documenttype = ANY (v_types)
28295
+ ORDER BY array_position(v_types, documenttype),
28296
+ (coalesce(bron, 'eigen') = 'eigen') DESC,
28297
+ is_standaard DESC NULLS LAST, updated_at DESC
28298
+ LIMIT 1;
28299
+ IF v_layout.id IS NULL THEN RETURN NULL; END IF;
28300
+ RETURN jsonb_build_object(
28301
+ 'layoutId', v_layout.id, 'layoutName', v_layout.naam, 'layoutVersion', v_layout.huidige_versie,
28302
+ 'layoutDocumentType', v_layout.documenttype::text, 'layoutSettingKey', NULL,
28303
+ 'layoutVariant', CASE WHEN COALESCE(_is_partial, false) THEN 'partial_order' ELSE 'full_cart' END,
28304
+ 'layoutFallbackUsed', false,
28305
+ 'layout_id', v_layout.id, 'layout_name', v_layout.naam, 'layout_version', v_layout.huidige_versie,
28306
+ 'layout_document_type', v_layout.documenttype::text, 'layout_setting_key', NULL,
28307
+ 'layout_variant', CASE WHEN COALESCE(_is_partial, false) THEN 'partial_order' ELSE 'full_cart' END,
28308
+ 'layout_fallback_used', false);
28309
+ END;
28310
+ $function$;
28311
+
28312
+ -- 3. Is Layouts al bijgewerkt?
28313
+ CREATE OR REPLACE FUNCTION public.logistics_document_selection_status()
28314
+ RETURNS jsonb
28315
+ LANGUAGE sql
28316
+ STABLE SECURITY DEFINER
28317
+ SET search_path TO 'public'
28318
+ AS $function$
28319
+ SELECT CASE WHEN to_regprocedure('public.layout_voor_document(uuid,text)') IS NOT NULL
28320
+ THEN jsonb_build_object('layouts_bijgewerkt', true, 'melding', 'Het document wordt automatisch bepaald uit de order (Layouts).')
28321
+ ELSE jsonb_build_object('layouts_bijgewerkt', false, 'melding', 'Layouts is nog niet bijgewerkt – automatische keuze gebruikt')
28322
+ END;
28323
+ $function$;
28324
+
28325
+ -- 4. Centrale beslisregel.
28326
+ CREATE OR REPLACE FUNCTION public.logistics_document_for_order(p_order_id uuid, p_documenttype text)
28327
+ RETURNS jsonb
28328
+ LANGUAGE plpgsql
28329
+ STABLE SECURITY DEFINER
28330
+ SET search_path TO 'public'
28331
+ AS $function$
28332
+ DECLARE
28333
+ v_soort text := lower(btrim(coalesce(p_documenttype, '')));
28334
+ v_floriday boolean;
28335
+ v_bron text;
28336
+ v_type text;
28337
+ v_res jsonb;
28338
+ v_n int; v_std int; v_id uuid;
28339
+ v_l public.document_layouts%ROWTYPE;
28340
+ BEGIN
28341
+ IF v_soort = '' THEN
28342
+ RETURN jsonb_build_object('ok', false, 'layout_id', NULL, 'reden', 'Geen documentsoort opgegeven.');
28343
+ END IF;
28344
+
28345
+ IF to_regprocedure('public.layout_voor_document(uuid,text)') IS NOT NULL THEN
28346
+ EXECUTE 'SELECT to_jsonb(public.layout_voor_document($1, $2))' INTO v_res USING p_order_id, v_soort;
28347
+ IF jsonb_typeof(v_res) <> 'object' THEN
28348
+ RETURN jsonb_build_object('ok', false, 'layout_id', NULL, 'keuze', 'layouts',
28349
+ 'reden', 'Layouts gaf geen bruikbaar antwoord voor ' || v_soort || '.');
28350
+ END IF;
28351
+ RETURN v_res || jsonb_build_object('keuze', 'layouts',
28352
+ 'ok', coalesce((v_res->>'ok')::boolean, nullif(v_res->>'layout_id', '') IS NOT NULL));
28353
+ END IF;
28354
+
28355
+ -- Automatische keuze (Layouts nog niet bijgewerkt).
28356
+ SELECT o.order_source = 'floriday' INTO v_floriday FROM public.orders o WHERE o.id = p_order_id;
28357
+ IF v_floriday IS NULL THEN
28358
+ RETURN jsonb_build_object('ok', false, 'layout_id', NULL, 'keuze', 'automatisch', 'reden', 'Order niet gevonden.');
28359
+ END IF;
28360
+
28361
+ v_type := CASE
28362
+ WHEN v_soort IN ('eigen_sticker', 'sticker', 'etiket') THEN 'sticker'
28363
+ WHEN v_soort IN ('verzenddocument', 'verzendbrief', 'afleverbrief', 'afleverbon') THEN 'afleverbrief'
28364
+ WHEN v_soort IN ('veilingbrief', 'veilingbon') THEN 'veilingbrief'
28365
+ WHEN v_soort LIKE '%pakbon%' THEN 'pakbon'
28366
+ WHEN v_soort LIKE '%productie%' THEN 'productiebon'
28367
+ ELSE v_soort END;
28368
+ v_bron := CASE
28369
+ WHEN v_floriday AND v_soort <> 'eigen_sticker' AND v_type IN ('sticker', 'traysticker', 'afleverbrief', 'veilingbrief') THEN 'floriday'
28370
+ ELSE 'eigen' END;
28371
+ IF v_bron = 'eigen' AND v_type = 'veilingbrief' THEN
28372
+ RETURN jsonb_build_object('ok', false, 'layout_id', NULL, 'keuze', 'automatisch',
28373
+ 'reden', 'Een veilingbrief hoort alleen bij een Floriday-order; voor een eigen order wordt de afleverbrief gebruikt.');
28374
+ END IF;
28375
+
28376
+ SELECT count(*), count(*) FILTER (WHERE is_standaard IS TRUE) INTO v_n, v_std
28377
+ FROM public.document_layouts
28378
+ WHERE actief IS TRUE AND coalesce(bron, 'eigen') = v_bron AND documenttype::text = v_type;
28379
+ IF v_n = 1 THEN
28380
+ SELECT * INTO v_l FROM public.document_layouts
28381
+ WHERE actief IS TRUE AND coalesce(bron, 'eigen') = v_bron AND documenttype::text = v_type;
28382
+ ELSIF v_n > 1 THEN
28383
+ SELECT * INTO v_l FROM public.document_layouts
28384
+ WHERE actief IS TRUE AND coalesce(bron, 'eigen') = v_bron AND documenttype::text = v_type
28385
+ ORDER BY is_standaard DESC NULLS LAST, updated_at DESC LIMIT 1;
28386
+ END IF;
28387
+ IF v_l.id IS NULL THEN
28388
+ RETURN jsonb_build_object('ok', false, 'layout_id', NULL, 'keuze', 'automatisch', 'bron', v_bron, 'documenttype', v_type,
28389
+ 'reden', CASE WHEN v_bron = 'floriday'
28390
+ THEN 'Geen actieve ' || v_type || '-layout met bron Floriday. Activeer de Floriday-systeemlayout in Layouts en printers.'
28391
+ ELSE 'Maak of activeer een ' || v_type || 'layout in Layouts en printers.' END);
28392
+ END IF;
28393
+ RETURN jsonb_build_object('ok', true, 'layout_id', v_l.id, 'keuze', 'automatisch', 'bron', v_bron,
28394
+ 'documenttype', v_type, 'breedte_mm', v_l.breedte_mm, 'hoogte_mm', v_l.hoogte_mm, 'reden', NULL);
28395
+ END;
28396
+ $function$;
28397
+
28398
+ -- 5. Eigen stickers (HO/MO/AO): één per klant+product, aantal = planten van regels met needs_sticker.
28399
+ CREATE OR REPLACE FUNCTION public.logistics_own_sticker_count(p_unit text, p_quantity numeric, p_ppc_regel numeric, p_ppc_product numeric)
28400
+ RETURNS integer LANGUAGE sql IMMUTABLE SET search_path = public AS $cnt$
28401
+ -- stuks/planten = aantal planten; fust = aantal x planten per fust (regel, anders product); onbekend = NULL (tegenhouden).
28402
+ SELECT CASE
28403
+ WHEN p_quantity IS NULL THEN NULL
28404
+ WHEN lower(coalesce(p_unit, '')) = 'fust' THEN
28405
+ CASE WHEN coalesce(nullif(p_ppc_regel, 0), nullif(p_ppc_product, 0)) IS NULL THEN NULL
28406
+ ELSE (p_quantity * coalesce(nullif(p_ppc_regel, 0), nullif(p_ppc_product, 0)))::int END
28407
+ ELSE p_quantity::int
28408
+ END
28409
+ $cnt$;
28410
+
28411
+ CREATE OR REPLACE FUNCTION public.logistics_own_sticker_sync(p_limit integer DEFAULT 200)
28412
+ RETURNS jsonb
28413
+ LANGUAGE plpgsql
28414
+ SECURITY DEFINER
28415
+ SET search_path TO 'public'
28416
+ AS $function$
28417
+ DECLARE
28418
+ r record;
28419
+ v_bron text;
28420
+ v_n int;
28421
+ v_sticker uuid;
28422
+ v_status text;
28423
+ v_reden text;
28424
+ v_bijgewerkt int := 0;
28425
+ v_vrij int := 0;
28426
+ v_uit int := 0;
28427
+ BEGIN
28428
+ FOR r IN
28429
+ SELECT o.id AS order_id, o.order_number, o.customer_id, oi.product_id,
28430
+ sum(public.logistics_own_sticker_count(oi.unit, oi.quantity, oi.plants_per_crate, prd.plants_per_crate))::int AS planten,
28431
+ bool_or(public.logistics_own_sticker_count(oi.unit, oi.quantity, oi.plants_per_crate, prd.plants_per_crate) IS NULL) AS onbekend
28432
+ FROM public.orders o
28433
+ JOIN public.order_items oi ON oi.order_id = o.id
28434
+ LEFT JOIN public.products prd ON prd.id = oi.product_id
28435
+ WHERE oi.needs_sticker IS TRUE
28436
+ AND oi.quarantined_at IS NULL
28437
+ AND oi.product_id IS NOT NULL
28438
+ AND coalesce(o.order_source, '') <> 'floriday'
28439
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
28440
+ AND coalesce(o.status, '') <> 'geannuleerd'
28441
+ AND o.delivery_date >= current_date - 1
28442
+ GROUP BY o.id, o.order_number, o.customer_id, oi.product_id
28443
+ LIMIT greatest(1, p_limit)
28444
+ LOOP
28445
+ v_bron := r.order_id::text || ':' || r.product_id::text;
28446
+ SELECT count(*), (array_agg(s.id))[1] INTO v_n, v_sticker
28447
+ FROM public.customer_product_stickers s
28448
+ JOIN public.customers c ON c.id = r.customer_id
28449
+ WHERE s.trade_item_key = r.product_id::text
28450
+ AND s.customer_key IN (c.id::text, 'tf:' || coalesce(c.twinfield_code, '-'), 'cn:' || coalesce(c.customer_number, '-'));
28451
+ v_status := 'wacht_op_moment'; v_reden := NULL;
28452
+ IF r.onbekend THEN
28453
+ v_status := 'geblokkeerd'; v_reden := 'Aantal stickers onbekend: regel in fust zonder planten per fust (niet op de regel en niet bij het product). Vul planten per fust in.';
28454
+ ELSIF coalesce(r.planten, 0) < 1 THEN
28455
+ v_status := 'geblokkeerd'; v_reden := 'Aantal planten is 0; geen sticker te printen.';
28456
+ ELSIF v_n = 0 THEN
28457
+ v_status := 'geblokkeerd'; v_reden := 'Geen eigen sticker gevonden voor deze klant en dit product. Voeg hem toe bij de klant.';
28458
+ ELSIF v_n > 1 THEN
28459
+ v_status := 'geblokkeerd'; v_reden := 'Meer dan één eigen sticker voor deze klant en dit product; er mag er maar één zijn.';
28460
+ END IF;
28461
+
28462
+ UPDATE public.logistics_sticker_prints
28463
+ SET kopieen = greatest(coalesce(r.planten, 0), 1),
28464
+ eigen_sticker_id = CASE WHEN v_n = 1 THEN v_sticker ELSE NULL END,
28465
+ status = CASE WHEN status = 'geblokkeerd' AND v_status = 'wacht_op_moment' THEN 'wacht_op_moment'
28466
+ WHEN status = 'niet_printen' THEN v_status
28467
+ WHEN status = 'wacht_op_moment' THEN v_status ELSE status END,
28468
+ reden = CASE WHEN status IN ('wacht_op_moment', 'geblokkeerd', 'niet_printen') THEN v_reden ELSE reden END,
28469
+ updated_at = now()
28470
+ WHERE soort = 'eigen_sticker' AND bron_id = v_bron
28471
+ AND status IN ('wacht_op_moment', 'geblokkeerd', 'niet_printen');
28472
+ IF NOT FOUND AND NOT EXISTS (SELECT 1 FROM public.logistics_sticker_prints WHERE soort = 'eigen_sticker' AND bron_id = v_bron) THEN
28473
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, kopieen, status, reden, eigen_sticker_id)
28474
+ VALUES ('eigen_sticker', v_bron, r.order_id, r.order_number, greatest(coalesce(r.planten, 0), 1), v_status, v_reden,
28475
+ CASE WHEN v_n = 1 THEN v_sticker ELSE NULL END);
28476
+ END IF;
28477
+ v_bijgewerkt := v_bijgewerkt + 1;
28478
+ END LOOP;
28479
+
28480
+ -- Vinkje uitgezet (of order vervallen) vóór het printen: niet printen.
28481
+ UPDATE public.logistics_sticker_prints p
28482
+ SET status = 'niet_printen', reden = 'Sticker uitgezet op de orderregel of order vervallen.', updated_at = now()
28483
+ WHERE p.soort = 'eigen_sticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
28484
+ AND NOT EXISTS (
28485
+ SELECT 1 FROM public.order_items oi JOIN public.orders o ON o.id = oi.order_id
28486
+ WHERE oi.order_id = p.order_id AND p.bron_id = oi.order_id::text || ':' || oi.product_id::text
28487
+ AND oi.needs_sticker IS TRUE AND oi.quarantined_at IS NULL
28488
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.status, '') <> 'geannuleerd');
28489
+ GET DIAGNOSTICS v_uit = ROW_COUNT;
28490
+
28491
+ -- Vrijgeven met de pakbon: kar en printplek van de geprinte pakbonrij, formaat uit sticker of layout.
28492
+ WITH vrij AS (
28493
+ SELECT p.id,
28494
+ (SELECT public.logistics_try_uuid(q.payload->>'cartId')
28495
+ FROM public.pick_queue q
28496
+ WHERE q.cancelled_at IS NULL AND q.print_job_id IS NOT NULL
28497
+ AND (q.payload->>'orderId' = p.order_id::text OR q.payload->'orderIds' ? p.order_id::text)
28498
+ ORDER BY q.created_at LIMIT 1) AS kar,
28499
+ (SELECT k.print_plek_id::text
28500
+ FROM public.pick_queue q
28501
+ JOIN public.print_plek_koppelingen k ON k.bron IN ('pickstation', 'pick_station') AND k.bron_id = q.station_id::text
28502
+ WHERE q.cancelled_at IS NULL AND q.print_job_id IS NOT NULL
28503
+ AND (q.payload->>'orderId' = p.order_id::text OR q.payload->'orderIds' ? p.order_id::text)
28504
+ ORDER BY q.created_at, k.created_at LIMIT 1) AS plek,
28505
+ s.width_mm, s.height_mm,
28506
+ public.logistics_document_for_order(p.order_id, 'eigen_sticker') AS lay
28507
+ FROM public.logistics_sticker_prints p
28508
+ LEFT JOIN public.customer_product_stickers s ON s.id = p.eigen_sticker_id
28509
+ WHERE p.soort = 'eigen_sticker' AND p.status = 'wacht_op_moment'
28510
+ AND EXISTS (SELECT 1 FROM public.pick_queue q
28511
+ WHERE q.cancelled_at IS NULL AND q.print_job_id IS NOT NULL
28512
+ AND (q.payload->>'orderId' = p.order_id::text OR q.payload->'orderIds' ? p.order_id::text))
28513
+ )
28514
+ UPDATE public.logistics_sticker_prints p
28515
+ SET status = 'klaar', cart_id = vrij.kar, print_plek_id = vrij.plek,
28516
+ breedte_mm = coalesce(vrij.width_mm, (vrij.lay->>'breedte_mm')::numeric),
28517
+ hoogte_mm = coalesce(vrij.height_mm, (vrij.lay->>'hoogte_mm')::numeric),
28518
+ updated_at = now()
28519
+ FROM vrij
28520
+ WHERE p.id = vrij.id;
28521
+ GET DIAGNOSTICS v_vrij = ROW_COUNT;
28522
+
28523
+ RETURN jsonb_build_object('regels', v_bijgewerkt, 'vrijgegeven', v_vrij, 'niet_printen', v_uit);
28524
+ END;
28525
+ $function$;
28526
+
28527
+ -- 6. Pakbon van het pickstation: alleen de centrale beslisregel (geen layout uit payload of werkwijze).
28528
+ CREATE OR REPLACE FUNCTION public.pick_enqueue_print_job(_queue_item_id uuid, _printer_id uuid, _session_id text, _is_herprint boolean, _cart_id text)
28529
+ RETURNS uuid
28530
+ LANGUAGE plpgsql
28531
+ SECURITY DEFINER
28532
+ SET search_path TO 'public'
28533
+ AS $function$
28534
+ DECLARE
28535
+ item public.pick_queue;
28536
+ dt public.pick_document_types;
28537
+ doc_code text;
28538
+ target_printer uuid;
28539
+ target_layout uuid;
28540
+ target_plek uuid;
28541
+ layout_meta jsonb;
28542
+ layout_reden text;
28543
+ v_types public.doc_layout_type[];
28544
+ doc_type public.doc_layout_type := 'vrij';
28545
+ layout_type public.doc_layout_type;
28546
+ pdf_url text;
28547
+ pdf_b64 text;
28548
+ herkomst text;
28549
+ job_id uuid;
28550
+ cart_plan jsonb := NULL;
28551
+ selected_cart jsonb := NULL;
28552
+ v_cart_uuid uuid;
28553
+ v_barcode text;
28554
+ v_barcode_on boolean := public.logistics_pick_barcode_enabled();
28555
+ v_is_partial boolean := false;
28556
+ v_sleutel text;
28557
+ v_order uuid;
28558
+ BEGIN
28559
+ SELECT * INTO item FROM public.pick_queue WHERE id = _queue_item_id;
28560
+ IF NOT FOUND THEN RAISE EXCEPTION 'Dit document bestaat niet meer.'; END IF;
28561
+
28562
+ SELECT * INTO dt FROM public.pick_document_types WHERE id = item.document_type_id;
28563
+
28564
+ doc_code := lower(btrim(coalesce(
28565
+ nullif(item.payload->>'documentType', ''),
28566
+ nullif(dt.code, ''),
28567
+ ''
28568
+ )));
28569
+
28570
+ v_types := public.logistics_document_type_map(doc_code);
28571
+
28572
+ IF _cart_id IS NOT NULL THEN
28573
+ cart_plan := public.pick_queue_cart_plan(_queue_item_id);
28574
+ SELECT c INTO selected_cart
28575
+ FROM jsonb_array_elements(COALESCE(cart_plan->'carts', '[]'::jsonb)) AS c
28576
+ WHERE c->>'cartId' = _cart_id
28577
+ LIMIT 1;
28578
+
28579
+ IF selected_cart IS NULL THEN
28580
+ RAISE EXCEPTION 'De gekozen kar bestaat niet meer in het actuele karplan.';
28581
+ END IF;
28582
+
28583
+ v_is_partial := NOT COALESCE((selected_cart->>'isFullCart')::boolean, false);
28584
+
28585
+ IF v_barcode_on THEN
28586
+ BEGIN
28587
+ v_cart_uuid := nullif(_cart_id, '')::uuid;
28588
+ EXCEPTION WHEN invalid_text_representation THEN
28589
+ v_cart_uuid := NULL;
28590
+ END;
28591
+ IF v_cart_uuid IS NOT NULL THEN
28592
+ v_barcode := public.logistics_ensure_cart_barcode(v_cart_uuid);
28593
+ selected_cart := selected_cart || jsonb_build_object('barcode', v_barcode);
28594
+ END IF;
28595
+ END IF;
28596
+ END IF;
28597
+
28598
+ -- 1.0.176: het document wordt automatisch bepaald uit de order.
28599
+ v_order := public.logistics_try_uuid(COALESCE(selected_cart->>'orderId', item.payload->>'orderId', item.payload->>'order_id'));
28600
+ IF doc_code <> '' AND v_order IS NOT NULL THEN
28601
+ layout_meta := public.logistics_document_for_order(v_order, doc_code);
28602
+ target_layout := public.logistics_try_uuid(layout_meta->>'layout_id');
28603
+ layout_reden := nullif(layout_meta->>'reden', '');
28604
+ ELSIF doc_code <> '' THEN
28605
+ layout_meta := public.logistics_document_layout_v2(doc_code, v_is_partial);
28606
+ target_layout := public.logistics_try_uuid(layout_meta->>'layout_id');
28607
+ END IF;
28608
+
28609
+ SELECT l.documenttype INTO layout_type
28610
+ FROM public.document_layouts l
28611
+ WHERE l.id = target_layout
28612
+ AND l.actief IS TRUE;
28613
+
28614
+ IF layout_type IS NULL THEN
28615
+ target_layout := NULL;
28616
+ END IF;
28617
+
28618
+ doc_type := COALESCE(layout_type, v_types[1], 'vrij');
28619
+
28620
+ IF item.station_id IS NOT NULL THEN
28621
+ SELECT k.print_plek_id INTO target_plek
28622
+ FROM public.print_plek_koppelingen k
28623
+ WHERE k.bron IN ('pickstation', 'pick_station')
28624
+ AND k.bron_id = item.station_id::text
28625
+ ORDER BY k.created_at
28626
+ LIMIT 1;
28627
+ END IF;
28628
+
28629
+ SELECT p.id INTO target_printer
28630
+ FROM public.printers p
28631
+ WHERE p.id = _printer_id
28632
+ AND p.actief
28633
+ AND nullif(p.extern_printer_id, '') IS NOT NULL;
28634
+
28635
+ pdf_url := nullif(item.payload->>'pdf_url', '');
28636
+ pdf_b64 := nullif(item.payload->>'pdf_base64', '');
28637
+ herkomst := nullif(item.payload->>'herkomst', '');
28638
+
28639
+ IF NOT COALESCE(_is_herprint, false) AND _cart_id IS NOT NULL THEN
28640
+ v_sleutel := 'pakbon:' || item.id::text || ':' || _cart_id || ':'
28641
+ || coalesce(target_layout::text, '-') || ':' || coalesce(target_printer::text, '-');
28642
+ END IF;
28643
+
28644
+ INSERT INTO public.print_jobs(
28645
+ printer_id, layout_id, documenttype, documentnaam, status, foutmelding,
28646
+ payload, gebruiker_naam, pdf_url, pdf_base64, herkomst, is_herprint, idempotentie_sleutel
28647
+ )
28648
+ VALUES (
28649
+ target_printer,
28650
+ target_layout,
28651
+ doc_type,
28652
+ COALESCE(item.document_number, 'Pickdocument'),
28653
+ 'wachtend',
28654
+ CASE
28655
+ WHEN target_layout IS NULL AND pdf_url IS NULL AND pdf_b64 IS NULL
28656
+ THEN COALESCE(layout_reden, 'Maak of activeer een ' || COALESCE(nullif(doc_code, ''), 'document') || 'layout in Layouts en printers.')
28657
+ ELSE NULL
28658
+ END,
28659
+ jsonb_build_object(
28660
+ 'source', 'pickstation',
28661
+ 'queue_item_id', item.id,
28662
+ 'station_id', item.station_id,
28663
+ 'print_plek_id', target_plek,
28664
+ 'session_id', _session_id,
28665
+ 'is_herprint', COALESCE(_is_herprint, false),
28666
+ 'document_number', item.document_number,
28667
+ 'document_type_code', COALESCE(nullif(doc_code, ''), dt.code),
28668
+ 'report_key', dt.report_key,
28669
+ 'print_template', dt.print_template,
28670
+ 'order_number', COALESCE(selected_cart->>'orderNumber', item.order_number),
28671
+ 'customer_name', item.customer_name,
28672
+ 'reference', item.reference,
28673
+ 'layout_id', target_layout,
28674
+ 'layout_keuze', layout_meta->>'keuze',
28675
+ 'layout_variant', CASE WHEN v_is_partial THEN 'partial_order' ELSE 'full_cart' END,
28676
+ 'barcode', CASE WHEN v_barcode_on THEN COALESCE(v_barcode, item.barcode) ELSE item.barcode END,
28677
+ 'country_code', item.country_code,
28678
+ 'cart_id', _cart_id,
28679
+ 'cart', selected_cart,
28680
+ 'data', COALESCE(item.payload, '{}'::jsonb) || jsonb_build_object(
28681
+ 'print_plek_id', target_plek,
28682
+ 'cart_id', _cart_id,
28683
+ 'cart', selected_cart,
28684
+ 'orderId', COALESCE(selected_cart->>'orderId', item.payload->>'orderId'),
28685
+ 'orderNumber', COALESCE(selected_cart->>'orderNumber', item.order_number)
28686
+ )
28687
+ ),
28688
+ 'Pickstation',
28689
+ pdf_url,
28690
+ pdf_b64,
28691
+ herkomst,
28692
+ COALESCE(_is_herprint, false),
28693
+ v_sleutel
28694
+ )
28695
+ RETURNING id INTO job_id;
28696
+
28697
+ PERFORM public.logistics_enqueue_sticker_with_slip(_queue_item_id, _printer_id, _session_id, COALESCE(_is_herprint,false), _cart_id, job_id);
28698
+ UPDATE public.pick_queue SET print_job_id = job_id WHERE id = _queue_item_id;
28699
+
28700
+ RETURN job_id;
28701
+ END;
28702
+ $function$;
28703
+
28704
+ REVOKE EXECUTE ON FUNCTION public.logistics_own_sticker_sync(integer) FROM PUBLIC, anon, authenticated;
28705
+ GRANT EXECUTE ON FUNCTION public.logistics_own_sticker_sync(integer) TO service_role;
28706
+ REVOKE EXECUTE ON FUNCTION public.logistics_document_for_order(uuid, text) FROM PUBLIC, anon;
28707
+ GRANT EXECUTE ON FUNCTION public.logistics_document_for_order(uuid, text) TO authenticated, service_role;
28708
+ REVOKE EXECUTE ON FUNCTION public.logistics_document_selection_status() FROM PUBLIC, anon;
28709
+ GRANT EXECUTE ON FUNCTION public.logistics_document_selection_status() TO authenticated, service_role;
28710
+
28711
+ -- Veiligheid: draaien met de rechten van de aanroeper.
28712
+ ALTER FUNCTION public.logistics_document_selection_status() SECURITY INVOKER;
28713
+ ALTER FUNCTION public.logistics_document_for_order(uuid, text) SECURITY INVOKER;
28714
+
28715
+ -- ===== 1.0.176 reserveringen FO + pakbon centrale keuze =====
28716
+ -- 1.0.176: reserveringen (Voorraad 1.0.93) en pakbon via centrale documentkeuze (Pickschermen 1.0.56).
28717
+ CREATE OR REPLACE FUNCTION public.logistics_recompute_reserved_targeted(p_product_id uuid)
28718
+ RETURNS jsonb
28719
+ LANGUAGE plpgsql
28720
+ SECURITY DEFINER
28721
+ SET search_path TO 'public'
28722
+ AS $function$
28723
+ DECLARE
28724
+ v_t0 timestamptz := clock_timestamp();
28725
+ v_max_lines constant integer := 500;
28726
+ v_max_keys constant integer := 200;
28727
+ v_trace_ms constant numeric := 500;
28728
+ v_nkeys integer; v_nkeys_prev integer := -1; v_nlines integer := 0; v_changed integer := 0; v_ms numeric;
28729
+ oi_rec record; part record; member_rec record;
28730
+ v_key text; v_last_key text; v_remaining numeric; v_available numeric; v_take numeric; v_err text;
28731
+ v_skipped jsonb := '[]'::jsonb; v_skipped_qty numeric := 0;
28732
+ v_struct jsonb := '[]'::jsonb; v_struct_qty numeric := 0;
28733
+ v_freeze boolean := public.logistics_plan_setting('verdeling_bevriezen_actief', 0) = 1;
28734
+ v_frozen_ids uuid[]; v_line_ids uuid[];
28735
+ BEGIN
28736
+ IF p_product_id IS NULL THEN RETURN jsonb_build_object('status', 'geen_product'); END IF;
28737
+ IF current_setting('app.recomputing_reserved', true) = 'on' THEN RETURN jsonb_build_object('status', 'al_bezig'); END IF;
28738
+ PERFORM set_config('app.recomputing_reserved', 'on', true);
28739
+
28740
+ DROP TABLE IF EXISTS _rt_open;
28741
+ DROP TABLE IF EXISTS _rt_pexp;
28742
+ DROP TABLE IF EXISTS _rt_gm;
28743
+ DROP TABLE IF EXISTS _rt_raw;
28744
+ DROP TABLE IF EXISTS _rt_pkeys;
28745
+ DROP TABLE IF EXISTS _rt_keys;
28746
+ DROP TABLE IF EXISTS _rt_lines;
28747
+ DROP TABLE IF EXISTS _rt_demand;
28748
+ DROP TABLE IF EXISTS _rt_alloc;
28749
+ DROP TABLE IF EXISTS _rt_frozen;
28750
+ CREATE TEMP TABLE _rt_open (order_item_id uuid PRIMARY KEY, product_id uuid, qty numeric, created_at timestamptz, order_id uuid) ON COMMIT DROP;
28751
+ CREATE TEMP TABLE _rt_pexp (product_id uuid, ord bigint, kind text, ref text, qty numeric, note text) ON COMMIT DROP;
28752
+ CREATE TEMP TABLE _rt_gm (group_id uuid, raw_key text, canon_key text, ord bigint) ON COMMIT DROP;
28753
+ CREATE TEMP TABLE _rt_raw (raw_key text PRIMARY KEY, canon_key text) ON COMMIT DROP;
28754
+ CREATE TEMP TABLE _rt_pkeys (product_id uuid, canon_key text) ON COMMIT DROP;
28755
+ CREATE TEMP TABLE _rt_keys (canon_key text PRIMARY KEY) ON COMMIT DROP;
28756
+ CREATE TEMP TABLE _rt_lines (order_item_id uuid PRIMARY KEY) ON COMMIT DROP;
28757
+ CREATE TEMP TABLE _rt_demand (trade_item_key text PRIMARY KEY, qty numeric NOT NULL DEFAULT 0) ON COMMIT DROP;
28758
+ CREATE TEMP TABLE _rt_alloc (order_item_id uuid, trade_item_key text, qty numeric, source text, group_id uuid) ON COMMIT DROP;
28759
+ CREATE TEMP TABLE _rt_frozen (order_item_id uuid PRIMARY KEY) ON COMMIT DROP;
28760
+
28761
+ INSERT INTO _rt_open
28762
+ SELECT oi.id, oi.product_id, COALESCE(oi.quantity, 0)::numeric, o.created_at, oi.order_id
28763
+ FROM logistics_active.order_items oi
28764
+ JOIN public.orders o ON o.id = oi.order_id
28765
+ WHERE o.status = 'Nog te picken' AND NOT public.logistics_reserved_order_excluded(o.id)
28766
+ AND COALESCE(oi.quantity, 0) > 0
28767
+ AND oi.product_id IS NOT NULL
28768
+ AND NOT EXISTS (SELECT 1 FROM public.stock_mutation_log sml WHERE sml.order_item_id = oi.id AND sml.mutation_type = 'consumption');
28769
+ -- Groepsregel naast eigen ledenregels: nooit reserveren.
28770
+ DELETE FROM _rt_open o WHERE o.order_item_id IN (SELECT d.order_item_id FROM public.logistics_double_group_lines(NULL) d);
28771
+
28772
+ INSERT INTO _rt_pexp (product_id, ord, kind, ref, qty, note)
28773
+ SELECT d.pid, e.ord, e.kind, e.ref, e.qty, e.note
28774
+ FROM (SELECT DISTINCT product_id AS pid FROM _rt_open UNION SELECT p_product_id) d,
28775
+ LATERAL public.logistics_expand_line(d.pid, 1) WITH ORDINALITY AS e(kind, ref, qty, note, ord);
28776
+
28777
+ INSERT INTO _rt_gm (group_id, raw_key, ord)
28778
+ SELECT g.gid, m.raw_key, m.ord
28779
+ FROM (SELECT DISTINCT ref::uuid AS gid FROM _rt_pexp WHERE kind = 'group') g,
28780
+ LATERAL (
28781
+ SELECT public.resolve_group_member_stock_key(pgi.trade_item_id) AS raw_key,
28782
+ row_number() OVER (ORDER BY pgi.sort_order ASC NULLS LAST, pgi.created_at ASC, pgi.id ASC) AS ord
28783
+ FROM public.product_group_items pgi
28784
+ WHERE pgi.product_group_id = g.gid) m
28785
+ WHERE m.raw_key IS NOT NULL
28786
+ AND m.raw_key NOT LIKE 'group:%';
28787
+
28788
+ INSERT INTO _rt_raw (raw_key, canon_key)
28789
+ SELECT x.k, public.logistics_reserved_key(x.k)
28790
+ FROM (SELECT ref AS k FROM _rt_pexp WHERE kind = 'item' UNION SELECT raw_key FROM _rt_gm) x;
28791
+ UPDATE _rt_gm g SET canon_key = r.canon_key FROM _rt_raw r WHERE r.raw_key = g.raw_key;
28792
+
28793
+ INSERT INTO _rt_pkeys (product_id, canon_key)
28794
+ SELECT DISTINCT e.product_id, r.canon_key
28795
+ FROM _rt_pexp e JOIN _rt_raw r ON r.raw_key = e.ref
28796
+ WHERE e.kind = 'item' AND r.canon_key IS NOT NULL
28797
+ UNION
28798
+ SELECT DISTINCT e.product_id, g.canon_key
28799
+ FROM _rt_pexp e JOIN _rt_gm g ON g.group_id = e.ref::uuid
28800
+ WHERE e.kind = 'group' AND g.canon_key IS NOT NULL;
28801
+
28802
+ INSERT INTO _rt_keys SELECT DISTINCT canon_key FROM _rt_pkeys WHERE product_id = p_product_id ON CONFLICT DO NOTHING;
28803
+ LOOP
28804
+ SELECT count(*) INTO v_nkeys FROM _rt_keys;
28805
+ EXIT WHEN v_nkeys = v_nkeys_prev;
28806
+ v_nkeys_prev := v_nkeys;
28807
+ INSERT INTO _rt_lines
28808
+ SELECT o.order_item_id FROM _rt_open o
28809
+ WHERE EXISTS (SELECT 1 FROM _rt_pkeys pk JOIN _rt_keys k ON k.canon_key = pk.canon_key
28810
+ WHERE pk.product_id = o.product_id)
28811
+ ON CONFLICT DO NOTHING;
28812
+ INSERT INTO _rt_keys
28813
+ SELECT DISTINCT pk.canon_key FROM _rt_pkeys pk
28814
+ JOIN _rt_open o ON o.product_id = pk.product_id
28815
+ JOIN _rt_lines l ON l.order_item_id = o.order_item_id
28816
+ ON CONFLICT DO NOTHING;
28817
+ SELECT count(*) INTO v_nlines FROM _rt_lines;
28818
+ IF v_nlines > v_max_lines OR (SELECT count(*) FROM _rt_keys) > v_max_keys THEN
28819
+ PERFORM set_config('app.recomputing_reserved', 'off', true);
28820
+ PERFORM public.recompute_all_reserved_stock();
28821
+ v_ms := round(extract(epoch FROM clock_timestamp() - v_t0) * 1000, 1);
28822
+ PERFORM public.logistics_trace(jsonb_build_object(
28823
+ 'phase', 'reservering', 'step', 'reservering herberekenen', 'status', 'waarschuwing',
28824
+ 'actor', 'logistics_recompute_reserved_targeted', 'errorClass', 'omvang',
28825
+ 'reason', 'Gerichte herberekening te groot; volledige herberekening uitgevoerd.',
28826
+ 'values', jsonb_build_object('product_id', p_product_id, 'regels', v_nlines,
28827
+ 'sleutels', (SELECT count(*) FROM _rt_keys), 'duur_ms', v_ms)));
28828
+ RETURN jsonb_build_object('status', 'volledig', 'regels', v_nlines, 'duur_ms', v_ms);
28829
+ END IF;
28830
+ END LOOP;
28831
+
28832
+ -- Bevroren regels eerst, naar rato van het huidige aantal.
28833
+ IF v_freeze THEN
28834
+ INSERT INTO _rt_frozen
28835
+ SELECT o.order_item_id FROM _rt_open o JOIN _rt_lines l ON l.order_item_id = o.order_item_id
28836
+ WHERE EXISTS (SELECT 1 FROM public.logistics_reservation_allocations a WHERE a.order_item_id = o.order_item_id AND a.source <> 'error')
28837
+ AND public.logistics_allocation_frozen(o.order_id)
28838
+ ON CONFLICT DO NOTHING;
28839
+ INSERT INTO _rt_alloc
28840
+ SELECT f.order_item_id, s.trade_item_key, s.qty, s.source, s.group_id
28841
+ FROM _rt_frozen f
28842
+ JOIN _rt_open o ON o.order_item_id = f.order_item_id
28843
+ CROSS JOIN LATERAL public.logistics_frozen_allocation_scaled(f.order_item_id, o.qty) s;
28844
+ INSERT INTO _rt_demand AS d
28845
+ SELECT trade_item_key, SUM(qty) FROM _rt_alloc GROUP BY trade_item_key
28846
+ ON CONFLICT (trade_item_key) DO UPDATE SET qty = d.qty + EXCLUDED.qty;
28847
+ INSERT INTO _rt_keys SELECT DISTINCT trade_item_key FROM _rt_alloc ON CONFLICT DO NOTHING;
28848
+ END IF;
28849
+
28850
+ FOR oi_rec IN
28851
+ SELECT o.order_item_id AS id, o.product_id, o.qty
28852
+ FROM _rt_open o JOIN _rt_lines l ON l.order_item_id = o.order_item_id
28853
+ WHERE NOT EXISTS (SELECT 1 FROM _rt_frozen f WHERE f.order_item_id = o.order_item_id)
28854
+ ORDER BY o.created_at, o.order_item_id
28855
+ LOOP
28856
+ SELECT string_agg(e.note, '; ') INTO v_err FROM _rt_pexp e WHERE e.product_id = oi_rec.product_id AND e.kind = 'error';
28857
+ IF v_err IS NOT NULL THEN
28858
+ v_struct := v_struct || jsonb_build_object('order_item_id', oi_rec.id, 'product_id', oi_rec.product_id, 'reden', v_err);
28859
+ v_struct_qty := v_struct_qty + oi_rec.qty;
28860
+ CONTINUE;
28861
+ END IF;
28862
+
28863
+ FOR part IN
28864
+ SELECT e.kind, e.ref, e.qty * oi_rec.qty AS qty
28865
+ FROM _rt_pexp e WHERE e.product_id = oi_rec.product_id ORDER BY e.ord
28866
+ LOOP
28867
+ IF part.kind = 'item' THEN
28868
+ SELECT canon_key INTO v_key FROM _rt_raw WHERE raw_key = part.ref;
28869
+ IF v_key IS NULL THEN
28870
+ v_skipped := v_skipped || to_jsonb(part.ref);
28871
+ v_skipped_qty := v_skipped_qty + part.qty;
28872
+ ELSE
28873
+ INSERT INTO _rt_demand AS d VALUES (v_key, part.qty)
28874
+ ON CONFLICT (trade_item_key) DO UPDATE SET qty = d.qty + EXCLUDED.qty;
28875
+ INSERT INTO _rt_alloc VALUES (oi_rec.id, v_key, part.qty, 'item', NULL);
28876
+ END IF;
28877
+ ELSE
28878
+ v_remaining := part.qty;
28879
+ v_last_key := NULL;
28880
+ FOR member_rec IN
28881
+ SELECT g.canon_key FROM _rt_gm g WHERE g.group_id = part.ref::uuid ORDER BY g.ord
28882
+ LOOP
28883
+ v_key := member_rec.canon_key;
28884
+ IF v_key IS NULL THEN CONTINUE; END IF;
28885
+ v_last_key := v_key;
28886
+ IF v_remaining <= 0 THEN CONTINUE; END IF;
28887
+
28888
+ SELECT GREATEST(COALESCE(MAX(s.physical_stock), 0)
28889
+ - COALESCE((SELECT d.qty FROM _rt_demand d WHERE d.trade_item_key = v_key), 0), 0)
28890
+ INTO v_available
28891
+ FROM public.trade_item_stock s
28892
+ WHERE s.trade_item_key = v_key;
28893
+
28894
+ IF COALESCE(v_available, 0) <= 0 THEN CONTINUE; END IF;
28895
+ v_take := LEAST(v_available, v_remaining);
28896
+ INSERT INTO _rt_demand AS d VALUES (v_key, v_take)
28897
+ ON CONFLICT (trade_item_key) DO UPDATE SET qty = d.qty + EXCLUDED.qty;
28898
+ INSERT INTO _rt_alloc VALUES (oi_rec.id, v_key, v_take, 'group', part.ref::uuid);
28899
+ v_remaining := v_remaining - v_take;
28900
+ END LOOP;
28901
+
28902
+ IF v_remaining > 0 THEN
28903
+ IF v_last_key IS NULL THEN
28904
+ v_skipped := v_skipped || to_jsonb('group:' || part.ref);
28905
+ v_skipped_qty := v_skipped_qty + v_remaining;
28906
+ ELSE
28907
+ INSERT INTO _rt_demand AS d VALUES (v_last_key, v_remaining)
28908
+ ON CONFLICT (trade_item_key) DO UPDATE SET qty = d.qty + EXCLUDED.qty;
28909
+ INSERT INTO _rt_alloc VALUES (oi_rec.id, v_last_key, v_remaining, 'group', part.ref::uuid);
28910
+ END IF;
28911
+ END IF;
28912
+ END IF;
28913
+ END LOOP;
28914
+ END LOOP;
28915
+
28916
+ DELETE FROM public.logistics_reservation_allocations a
28917
+ WHERE a.order_item_id IN (SELECT l.order_item_id FROM _rt_lines l);
28918
+ INSERT INTO public.logistics_reservation_allocations (order_item_id, trade_item_key, qty, source, group_id, line_qty)
28919
+ SELECT r.order_item_id, r.trade_item_key, SUM(r.qty), r.source, r.group_id, MAX(o.qty)
28920
+ FROM _rt_alloc r
28921
+ LEFT JOIN _rt_open o ON o.order_item_id = r.order_item_id
28922
+ WHERE r.qty > 0
28923
+ GROUP BY r.order_item_id, r.trade_item_key, r.source, r.group_id;
28924
+
28925
+ -- 1.0.176 (Voorraad 1.0.93): Floriday-orders op Floriday-leidende sleutels tellen niet mee
28926
+ -- in reserved_stock; de toewijzingen zelf blijven bestaan. Oudere Voorraad: gedrag zoals nu + melding.
28927
+ IF to_regprocedure('public.voorraad_floriday_leads_key(text)') IS NOT NULL THEN
28928
+ UPDATE _rt_demand d
28929
+ SET qty = COALESCE((
28930
+ SELECT SUM(r.qty)
28931
+ FROM _rt_alloc r
28932
+ JOIN _rt_open op ON op.order_item_id = r.order_item_id
28933
+ JOIN public.orders ord ON ord.id = op.order_id
28934
+ WHERE r.trade_item_key = d.trade_item_key AND r.qty > 0
28935
+ AND NOT (public.normalize_order_source(ord.order_source) = 'FO'
28936
+ AND COALESCE(public.voorraad_floriday_leads_key(d.trade_item_key), false))), 0);
28937
+ ELSE
28938
+ PERFORM public.logistics_trace(jsonb_build_object(
28939
+ 'phase', 'reservering', 'step', 'reservering herberekenen', 'status', 'waarschuwing',
28940
+ 'actor', 'logistics_recompute_reserved_targeted', 'errorClass', 'afhankelijkheid',
28941
+ 'reason', 'voorraad_floriday_leads_key ontbreekt (oudere Voorraad); Floriday-orders tellen nog mee in de reservering.',
28942
+ 'values', jsonb_build_object('product_id', p_product_id)));
28943
+ END IF;
28944
+
28945
+ PERFORM set_config('stock.change_source', 'RESERVATION_RECOMPUTE', true);
28946
+ UPDATE public.trade_item_stock s
28947
+ SET reserved_stock = CEIL(COALESCE(d.qty, 0))::integer,
28948
+ change_source = 'RESERVATION_RECOMPUTE',
28949
+ updated_at = now()
28950
+ FROM _rt_keys k
28951
+ LEFT JOIN _rt_demand d ON d.trade_item_key = k.canon_key
28952
+ WHERE s.trade_item_key = k.canon_key
28953
+ AND s.reserved_stock IS DISTINCT FROM CEIL(COALESCE(d.qty, 0))::integer;
28954
+ GET DIAGNOSTICS v_changed = ROW_COUNT;
28955
+
28956
+ v_ms := round(extract(epoch FROM clock_timestamp() - v_t0) * 1000, 1);
28957
+
28958
+ IF jsonb_array_length(v_skipped) > 0 THEN
28959
+ PERFORM public.logistics_trace(jsonb_build_object(
28960
+ 'phase', 'reservering', 'step', 'reservering herberekenen', 'status', 'waarschuwing',
28961
+ 'actor', 'logistics_recompute_reserved_targeted', 'errorClass', 'ontbrekende_gegevens',
28962
+ 'reason', 'Geen voorraadrij en geen actieve Floriday-koppeling om die aan te maken; reservering voor deze artikelen niet berekend.',
28963
+ 'values', jsonb_build_object('product_id', p_product_id,
28964
+ 'sleutels', (SELECT jsonb_agg(DISTINCT x) FROM jsonb_array_elements_text(v_skipped) x),
28965
+ 'gemiste_vraag_stuks', v_skipped_qty)));
28966
+ END IF;
28967
+
28968
+ IF jsonb_array_length(v_struct) > 0 THEN
28969
+ PERFORM public.logistics_trace(jsonb_build_object(
28970
+ 'phase', 'reservering', 'step', 'reservering herberekenen', 'status', 'waarschuwing',
28971
+ 'actor', 'logistics_recompute_reserved_targeted', 'errorClass', 'structuur',
28972
+ 'reason', 'Samenstelling met een lus of te diepe nesting; voor deze bestelregels is niets gereserveerd.',
28973
+ 'values', jsonb_build_object('regels', v_struct, 'gemiste_vraag_stuks', v_struct_qty)));
28974
+ END IF;
28975
+
28976
+ IF v_ms > v_trace_ms THEN
28977
+ PERFORM public.logistics_trace(jsonb_build_object(
28978
+ 'phase', 'reservering', 'step', 'reservering herberekenen', 'status', 'waarschuwing',
28979
+ 'actor', 'logistics_recompute_reserved_targeted', 'errorClass', 'traag',
28980
+ 'reason', 'Gerichte herberekening duurde langer dan verwacht.',
28981
+ 'values', jsonb_build_object('product_id', p_product_id, 'regels', v_nlines,
28982
+ 'sleutels', (SELECT count(*) FROM _rt_keys), 'duur_ms', v_ms)));
28983
+ END IF;
28984
+
28985
+ IF v_freeze THEN
28986
+ SELECT array_agg(order_item_id) INTO v_frozen_ids FROM _rt_frozen;
28987
+ SELECT array_agg(order_item_id) INTO v_line_ids FROM _rt_lines;
28988
+ PERFORM public.logistics_report_frozen_shortage(v_frozen_ids);
28989
+ PERFORM public.logistics_allocation_check_changes(v_line_ids);
28990
+ END IF;
28991
+
28992
+ PERFORM set_config('app.recomputing_reserved', 'off', true);
28993
+ RETURN jsonb_build_object('status', 'gericht', 'regels', v_nlines,
28994
+ 'sleutels', (SELECT count(*) FROM _rt_keys), 'gewijzigd', v_changed, 'duur_ms', v_ms);
28995
+ END
28996
+ $function$;
28997
+
28998
+ CREATE OR REPLACE FUNCTION public.logistics_resolve_pick_document(_queue_item_id uuid, _voorbeeld boolean, _session_id text, _is_herprint boolean)
28999
+ RETURNS jsonb
29000
+ LANGUAGE plpgsql
29001
+ STABLE SECURITY DEFINER
29002
+ SET search_path TO 'public'
29003
+ AS $function$
29004
+ DECLARE
29005
+ item public.pick_queue;
29006
+ dt public.pick_document_types;
29007
+ doc_code text;
29008
+ target_layout uuid;
29009
+ target_plek uuid;
29010
+ layout_meta jsonb;
29011
+ v_types public.doc_layout_type[];
29012
+ doc_type public.doc_layout_type := 'vrij';
29013
+ layout_type public.doc_layout_type;
29014
+ pdf_url text;
29015
+ pdf_b64 text;
29016
+ herkomst text;
29017
+ v_variant text;
29018
+ v_is_partial boolean := false;
29019
+ v_cart_id uuid;
29020
+ v_cart jsonb;
29021
+ v_order_id uuid;
29022
+ v_barcode text;
29023
+ v_barcode_on boolean := public.logistics_pick_barcode_enabled();
29024
+ v_ok boolean := true;
29025
+ v_reden text;
29026
+ BEGIN
29027
+ SELECT * INTO item FROM public.pick_queue WHERE id = _queue_item_id;
29028
+ IF NOT FOUND THEN
29029
+ RAISE EXCEPTION 'Dit document bestaat niet meer.' USING ERRCODE = 'P0002';
29030
+ END IF;
29031
+
29032
+ SELECT * INTO dt FROM public.pick_document_types WHERE id = item.document_type_id;
29033
+
29034
+ doc_code := lower(btrim(coalesce(
29035
+ nullif(item.payload->>'documentType', ''),
29036
+ nullif(dt.code, ''),
29037
+ ''
29038
+ )));
29039
+
29040
+ v_types := public.logistics_document_type_map(doc_code);
29041
+
29042
+ BEGIN
29043
+ v_cart_id := nullif(coalesce(
29044
+ item.payload->>'cartId', item.payload->>'cart_id',
29045
+ item.payload->'data'->>'cartId', item.payload->'data'->>'cart_id'
29046
+ ), '')::uuid;
29047
+ EXCEPTION WHEN invalid_text_representation THEN
29048
+ v_cart_id := NULL;
29049
+ END;
29050
+
29051
+ IF v_cart_id IS NOT NULL THEN
29052
+ SELECT COALESCE(c.order_id, (
29053
+ SELECT oi.order_id FROM logistics_active.order_cart_items ci
29054
+ JOIN logistics_active.order_items oi ON oi.id = ci.order_item_id
29055
+ WHERE ci.cart_id = c.id LIMIT 1))
29056
+ INTO v_order_id
29057
+ FROM logistics_active.order_carts c WHERE c.id = v_cart_id;
29058
+
29059
+ IF v_order_id IS NOT NULL THEN
29060
+ SELECT c INTO v_cart
29061
+ FROM jsonb_array_elements(public.logistics_cart_slip_plan(v_order_id)->'carts') AS c
29062
+ WHERE (c->>'cartId')::uuid = v_cart_id;
29063
+ END IF;
29064
+
29065
+ IF v_barcode_on THEN
29066
+ v_barcode := COALESCE(
29067
+ (SELECT b.barcode FROM public.logistics_cart_barcode b WHERE b.cart_id = v_cart_id),
29068
+ public.logistics_cart_barcode_value(v_cart_id)
29069
+ );
29070
+ IF v_cart IS NOT NULL THEN
29071
+ v_cart := v_cart || jsonb_build_object('barcode', v_barcode);
29072
+ END IF;
29073
+ END IF;
29074
+ END IF;
29075
+
29076
+ IF v_cart IS NOT NULL THEN
29077
+ v_is_partial := NOT COALESCE((v_cart->>'isFullCart')::boolean, false);
29078
+ ELSE
29079
+ v_variant := nullif(coalesce(
29080
+ item.payload->>'slipVariant',
29081
+ item.payload->'data'->>'slipVariant'
29082
+ ), '');
29083
+ IF v_variant IS NOT NULL THEN
29084
+ v_is_partial := (v_variant = 'partial_order');
29085
+ ELSIF nullif(coalesce(item.payload->>'pickUnit', item.payload->'data'->>'pickUnit'), '') IS NOT NULL THEN
29086
+ v_is_partial := (coalesce(item.payload->>'pickUnit', item.payload->'data'->>'pickUnit') = 'order');
29087
+ ELSE
29088
+ v_is_partial := (item.cart_count IS NOT NULL AND item.cart_count <= 1);
29089
+ END IF;
29090
+ END IF;
29091
+
29092
+ -- 1.0.176 (Pickschermen 1.0.56): layout alleen via de centrale documentkeuze; payload.layout_id wordt genegeerd.
29093
+ v_order_id := COALESCE(v_order_id, public.logistics_try_uuid(COALESCE(
29094
+ v_cart->>'orderId', item.payload->>'orderId', item.payload->>'order_id', item.payload->'data'->>'orderId')));
29095
+ IF doc_code <> '' AND v_order_id IS NOT NULL THEN
29096
+ layout_meta := public.logistics_document_for_order(v_order_id, doc_code);
29097
+ target_layout := public.logistics_try_uuid(layout_meta->>'layout_id');
29098
+ v_ok := COALESCE(layout_meta->>'ok', 'true') <> 'false';
29099
+ v_reden := nullif(layout_meta->>'reden', '');
29100
+ ELSIF doc_code <> '' THEN
29101
+ layout_meta := public.logistics_document_layout_v2(doc_code, v_is_partial);
29102
+ target_layout := public.logistics_try_uuid(layout_meta->>'layout_id');
29103
+ END IF;
29104
+
29105
+ SELECT l.documenttype INTO layout_type
29106
+ FROM public.document_layouts l
29107
+ WHERE l.id = target_layout AND l.actief IS TRUE;
29108
+
29109
+ IF layout_type IS NULL THEN target_layout := NULL; END IF;
29110
+
29111
+ doc_type := COALESCE(layout_type, v_types[1], 'vrij');
29112
+
29113
+ IF item.station_id IS NOT NULL THEN
29114
+ SELECT k.print_plek_id INTO target_plek
29115
+ FROM public.print_plek_koppelingen k
29116
+ WHERE k.bron IN ('pickstation', 'pick_station')
29117
+ AND k.bron_id = item.station_id::text
29118
+ ORDER BY k.created_at
29119
+ LIMIT 1;
29120
+ END IF;
29121
+
29122
+ pdf_url := nullif(item.payload->>'pdf_url', '');
29123
+ pdf_b64 := nullif(item.payload->>'pdf_base64', '');
29124
+ herkomst := nullif(item.payload->>'herkomst', '');
29125
+
29126
+ IF target_layout IS NULL AND v_reden IS NULL AND pdf_url IS NULL AND pdf_b64 IS NULL THEN
29127
+ v_reden := 'Maak of activeer een ' || COALESCE(nullif(doc_code, ''), 'document') || 'layout in Layouts en printers.';
29128
+ END IF;
29129
+ IF NOT v_ok AND (pdf_url IS NOT NULL OR pdf_b64 IS NOT NULL) THEN v_ok := true; END IF;
29130
+
29131
+ RETURN jsonb_build_object(
29132
+ 'ok', v_ok,
29133
+ 'reden', CASE WHEN v_ok THEN NULL ELSE v_reden END,
29134
+ 'layout_keuze', layout_meta->>'keuze',
29135
+ 'queue_item_id', item.id,
29136
+ 'station_id', item.station_id,
29137
+ 'doc_code', doc_code,
29138
+ 'layout_id', target_layout,
29139
+ 'layout_variant', CASE WHEN v_is_partial THEN 'partial_order' ELSE 'full_cart' END,
29140
+ 'cart_id', v_cart_id,
29141
+ 'cart', v_cart,
29142
+ 'barcode', v_barcode,
29143
+ 'documenttype', doc_type,
29144
+ 'documentnaam', COALESCE(item.document_number, 'Pickdocument'),
29145
+ 'pdf_url', pdf_url,
29146
+ 'pdf_base64', pdf_b64,
29147
+ 'herkomst', herkomst,
29148
+ 'render_html', nullif(item.payload->>'render_html', ''),
29149
+ 'print_plek_id', target_plek,
29150
+ 'foutmelding',
29151
+ CASE WHEN target_layout IS NULL AND pdf_url IS NULL AND pdf_b64 IS NULL
29152
+ THEN v_reden
29153
+ ELSE NULL END,
29154
+ 'payload', jsonb_build_object(
29155
+ 'source', 'pickstation',
29156
+ 'voorbeeld', COALESCE(_voorbeeld, false),
29157
+ 'queue_item_id', item.id,
29158
+ 'station_id', item.station_id,
29159
+ 'print_plek_id', target_plek,
29160
+ 'session_id', _session_id,
29161
+ 'is_herprint', COALESCE(_is_herprint, false),
29162
+ 'document_number', item.document_number,
29163
+ 'document_type_code', COALESCE(nullif(doc_code, ''), dt.code),
29164
+ 'layout_variant', CASE WHEN v_is_partial THEN 'partial_order' ELSE 'full_cart' END,
29165
+ 'layout_id', target_layout,
29166
+ 'report_key', dt.report_key,
29167
+ 'print_template', dt.print_template,
29168
+ 'order_number', item.order_number,
29169
+ 'customer_name', item.customer_name,
29170
+ 'reference', item.reference,
29171
+ 'barcode', CASE WHEN v_barcode_on THEN COALESCE(v_barcode, item.barcode) ELSE item.barcode END,
29172
+ 'cart_id', v_cart_id,
29173
+ 'cart_index', v_cart->>'cartIndex',
29174
+ 'cart_total', v_cart->>'cartTotal',
29175
+ 'cart_label', v_cart->>'cartLabel',
29176
+ 'cart_items', COALESCE(v_cart->'items', '[]'::jsonb),
29177
+ 'country_code', item.country_code,
29178
+ 'data', (COALESCE(item.payload, '{}'::jsonb) - 'layout_id' - 'layout')
29179
+ || jsonb_build_object('print_plek_id', target_plek)
29180
+ || COALESCE(jsonb_build_object('cart', v_cart), '{}'::jsonb)
29181
+ )
29182
+ );
29183
+ END;
29184
+ $function$;
29185
+
29186
+ CREATE OR REPLACE FUNCTION public.pick_enqueue_print_job(_queue_item_id uuid, _printer_id uuid DEFAULT NULL::uuid, _session_id text DEFAULT NULL::text, _is_herprint boolean DEFAULT false)
29187
+ RETURNS uuid
29188
+ LANGUAGE plpgsql
29189
+ SECURITY DEFINER
29190
+ SET search_path TO 'public'
29191
+ AS $function$
29192
+ declare
29193
+ doc jsonb; target_printer uuid; job_id uuid; v_cart_id uuid;
29194
+ v_doc_code text; v_plan jsonb; v_cart jsonb; v_last uuid; v_row_cart text;
29195
+ begin
29196
+ select lower(btrim(coalesce(nullif(q.payload->>'documentType',''), nullif(t.code,''), ''))),
29197
+ nullif(q.payload->>'cartId','')
29198
+ into v_doc_code, v_row_cart
29199
+ from public.pick_queue q
29200
+ left join public.pick_document_types t on t.id = q.document_type_id
29201
+ where q.id = _queue_item_id;
29202
+
29203
+ if v_doc_code = 'pakbon' then
29204
+ -- Karregel: precies die ene kar. Orderregel: per kar één opdracht.
29205
+ if v_row_cart is not null then
29206
+ return public.pick_enqueue_print_job(_queue_item_id, _printer_id, _session_id,
29207
+ coalesce(_is_herprint,false), v_row_cart);
29208
+ end if;
29209
+
29210
+ begin
29211
+ v_plan := public.pick_queue_cart_plan(_queue_item_id);
29212
+ exception when others then v_plan := null; end;
29213
+
29214
+ if v_plan is null or jsonb_array_length(coalesce(v_plan->'carts','[]'::jsonb)) = 0 then
29215
+ raise exception 'Het karplan voor dit document is nog niet klaar; er wordt geen pakbon zonder barcode geprint.' using errcode='check_violation';
29216
+ end if;
29217
+
29218
+ for v_cart in select c from jsonb_array_elements(v_plan->'carts') as c loop
29219
+ v_last := public.pick_enqueue_print_job(_queue_item_id, _printer_id, _session_id,
29220
+ coalesce(_is_herprint,false), v_cart->>'cartId');
29221
+ end loop;
29222
+ return v_last;
29223
+ end if;
29224
+
29225
+ doc := public.logistics_resolve_pick_document(_queue_item_id, false, _session_id, coalesce(_is_herprint,false));
29226
+ -- 1.0.176: centrale documentkeuze zegt nee -> dit document niet maken, reden teruggeven.
29227
+ if doc->>'ok' = 'false' then
29228
+ raise exception '%', coalesce(nullif(doc->>'reden',''), 'Geen layout voor dit document.') using errcode='check_violation';
29229
+ end if;
29230
+
29231
+ if nullif(doc->>'barcode','') is not null then
29232
+ begin v_cart_id := nullif(doc->>'cart_id','')::uuid;
29233
+ exception when invalid_text_representation then v_cart_id := null; end;
29234
+ if v_cart_id is not null then perform public.logistics_ensure_cart_barcode(v_cart_id); end if;
29235
+ end if;
29236
+
29237
+ select p.id into target_printer from public.printers p
29238
+ where p.id = _printer_id and p.actief and nullif(p.extern_printer_id,'') is not null;
29239
+
29240
+ insert into public.print_jobs(
29241
+ printer_id, layout_id, documenttype, documentnaam, status, foutmelding,
29242
+ payload, gebruiker_naam, pdf_url, pdf_base64, herkomst, is_herprint)
29243
+ values (target_printer, nullif(doc->>'layout_id','')::uuid,
29244
+ (doc->>'documenttype')::public.doc_layout_type, doc->>'documentnaam', 'wachtend',
29245
+ nullif(doc->>'foutmelding',''), doc->'payload', 'Pickstation',
29246
+ nullif(doc->>'pdf_url',''), nullif(doc->>'pdf_base64',''), nullif(doc->>'herkomst',''),
29247
+ coalesce(_is_herprint,false))
29248
+ returning id into job_id;
29249
+
29250
+ perform public.logistics_enqueue_sticker_with_slip(_queue_item_id, _printer_id, _session_id,
29251
+ coalesce(_is_herprint,false), nullif(doc->>'cart_id',''), job_id);
29252
+ update public.pick_queue set print_job_id = job_id where id = _queue_item_id;
29253
+
29254
+ return job_id;
29255
+ end; $function$;
29256
+
29257
+ CREATE OR REPLACE FUNCTION public.pick_enqueue_print_job(_queue_item_id uuid, _printer_id uuid, _session_id text, _is_herprint boolean, _cart_id text)
29258
+ RETURNS uuid
29259
+ LANGUAGE plpgsql
29260
+ SECURITY DEFINER
29261
+ SET search_path TO 'public'
29262
+ AS $function$
29263
+ DECLARE
29264
+ item public.pick_queue;
29265
+ dt public.pick_document_types;
29266
+ doc_code text;
29267
+ target_printer uuid;
29268
+ target_layout uuid;
29269
+ target_plek uuid;
29270
+ layout_meta jsonb;
29271
+ layout_reden text;
29272
+ v_types public.doc_layout_type[];
29273
+ doc_type public.doc_layout_type := 'vrij';
29274
+ layout_type public.doc_layout_type;
29275
+ pdf_url text;
29276
+ pdf_b64 text;
29277
+ herkomst text;
29278
+ job_id uuid;
29279
+ cart_plan jsonb := NULL;
29280
+ selected_cart jsonb := NULL;
29281
+ v_cart_uuid uuid;
29282
+ v_barcode text;
29283
+ v_barcode_on boolean := public.logistics_pick_barcode_enabled();
29284
+ v_is_partial boolean := false;
29285
+ v_sleutel text;
29286
+ v_order uuid;
29287
+ BEGIN
29288
+ SELECT * INTO item FROM public.pick_queue WHERE id = _queue_item_id;
29289
+ IF NOT FOUND THEN RAISE EXCEPTION 'Dit document bestaat niet meer.'; END IF;
29290
+ SELECT * INTO dt FROM public.pick_document_types WHERE id = item.document_type_id;
29291
+ doc_code := lower(btrim(coalesce(nullif(item.payload->>'documentType', ''), nullif(dt.code, ''), '')));
29292
+ v_types := public.logistics_document_type_map(doc_code);
29293
+
29294
+ IF _cart_id IS NOT NULL THEN
29295
+ cart_plan := public.pick_queue_cart_plan(_queue_item_id);
29296
+ SELECT c INTO selected_cart FROM jsonb_array_elements(COALESCE(cart_plan->'carts', '[]'::jsonb)) AS c
29297
+ WHERE c->>'cartId' = _cart_id LIMIT 1;
29298
+ IF selected_cart IS NULL THEN
29299
+ RAISE EXCEPTION 'De gekozen kar bestaat niet meer in het actuele karplan.';
29300
+ END IF;
29301
+ v_is_partial := NOT COALESCE((selected_cart->>'isFullCart')::boolean, false);
29302
+ IF v_barcode_on THEN
29303
+ BEGIN
29304
+ v_cart_uuid := nullif(_cart_id, '')::uuid;
29305
+ EXCEPTION WHEN invalid_text_representation THEN
29306
+ v_cart_uuid := NULL;
29307
+ END;
29308
+ IF v_cart_uuid IS NOT NULL THEN
29309
+ v_barcode := public.logistics_ensure_cart_barcode(v_cart_uuid);
29310
+ selected_cart := selected_cart || jsonb_build_object('barcode', v_barcode);
29311
+ END IF;
29312
+ END IF;
29313
+ END IF;
29314
+
29315
+ -- 1.0.176: het document wordt automatisch bepaald uit de order.
29316
+ v_order := public.logistics_try_uuid(COALESCE(selected_cart->>'orderId', item.payload->>'orderId', item.payload->>'order_id'));
29317
+ IF doc_code <> '' AND v_order IS NOT NULL THEN
29318
+ layout_meta := public.logistics_document_for_order(v_order, doc_code);
29319
+ target_layout := public.logistics_try_uuid(layout_meta->>'layout_id');
29320
+ layout_reden := nullif(layout_meta->>'reden', '');
29321
+ IF layout_meta->>'ok' = 'false' AND nullif(item.payload->>'pdf_url', '') IS NULL AND nullif(item.payload->>'pdf_base64', '') IS NULL THEN
29322
+ RAISE EXCEPTION '%', COALESCE(layout_reden, 'Geen layout voor dit document.') USING ERRCODE = 'check_violation';
29323
+ END IF;
29324
+ ELSIF doc_code <> '' THEN
29325
+ layout_meta := public.logistics_document_layout_v2(doc_code, v_is_partial);
29326
+ target_layout := public.logistics_try_uuid(layout_meta->>'layout_id');
29327
+ END IF;
29328
+
29329
+ SELECT l.documenttype INTO layout_type FROM public.document_layouts l WHERE l.id = target_layout AND l.actief IS TRUE;
29330
+ IF layout_type IS NULL THEN target_layout := NULL; END IF;
29331
+ doc_type := COALESCE(layout_type, v_types[1], 'vrij');
29332
+
29333
+ IF item.station_id IS NOT NULL THEN
29334
+ SELECT k.print_plek_id INTO target_plek FROM public.print_plek_koppelingen k
29335
+ WHERE k.bron IN ('pickstation', 'pick_station') AND k.bron_id = item.station_id::text
29336
+ ORDER BY k.created_at LIMIT 1;
29337
+ END IF;
29338
+
29339
+ SELECT p.id INTO target_printer FROM public.printers p
29340
+ WHERE p.id = _printer_id AND p.actief AND nullif(p.extern_printer_id, '') IS NOT NULL;
29341
+
29342
+ pdf_url := nullif(item.payload->>'pdf_url', '');
29343
+ pdf_b64 := nullif(item.payload->>'pdf_base64', '');
29344
+ herkomst := nullif(item.payload->>'herkomst', '');
29345
+
29346
+ IF NOT COALESCE(_is_herprint, false) AND _cart_id IS NOT NULL THEN
29347
+ v_sleutel := 'pakbon:' || item.id::text || ':' || _cart_id || ':'
29348
+ || coalesce(target_layout::text, '-') || ':' || coalesce(target_printer::text, '-');
29349
+ END IF;
29350
+
29351
+ INSERT INTO public.print_jobs(
29352
+ printer_id, layout_id, documenttype, documentnaam, status, foutmelding,
29353
+ payload, gebruiker_naam, pdf_url, pdf_base64, herkomst, is_herprint, idempotentie_sleutel
29354
+ )
29355
+ VALUES (
29356
+ target_printer, target_layout, doc_type, COALESCE(item.document_number, 'Pickdocument'), 'wachtend',
29357
+ CASE WHEN target_layout IS NULL AND pdf_url IS NULL AND pdf_b64 IS NULL
29358
+ THEN COALESCE(layout_reden, 'Maak of activeer een ' || COALESCE(nullif(doc_code, ''), 'document') || 'layout in Layouts en printers.')
29359
+ ELSE NULL END,
29360
+ jsonb_build_object(
29361
+ 'source', 'pickstation', 'queue_item_id', item.id, 'station_id', item.station_id,
29362
+ 'print_plek_id', target_plek, 'session_id', _session_id, 'is_herprint', COALESCE(_is_herprint, false),
29363
+ 'document_number', item.document_number, 'document_type_code', COALESCE(nullif(doc_code, ''), dt.code),
29364
+ 'report_key', dt.report_key, 'print_template', dt.print_template,
29365
+ 'order_number', COALESCE(selected_cart->>'orderNumber', item.order_number),
29366
+ 'customer_name', item.customer_name, 'reference', item.reference,
29367
+ 'layout_id', target_layout, 'layout_keuze', layout_meta->>'keuze',
29368
+ 'layout_variant', CASE WHEN v_is_partial THEN 'partial_order' ELSE 'full_cart' END,
29369
+ 'barcode', CASE WHEN v_barcode_on THEN COALESCE(v_barcode, item.barcode) ELSE item.barcode END,
29370
+ 'country_code', item.country_code, 'cart_id', _cart_id, 'cart', selected_cart,
29371
+ 'data', (COALESCE(item.payload, '{}'::jsonb) - 'layout_id' - 'layout') || jsonb_build_object(
29372
+ 'print_plek_id', target_plek, 'cart_id', _cart_id, 'cart', selected_cart,
29373
+ 'orderId', COALESCE(selected_cart->>'orderId', item.payload->>'orderId'),
29374
+ 'orderNumber', COALESCE(selected_cart->>'orderNumber', item.order_number))
29375
+ ),
29376
+ 'Pickstation', pdf_url, pdf_b64, herkomst, COALESCE(_is_herprint, false), v_sleutel
29377
+ )
29378
+ RETURNING id INTO job_id;
29379
+
29380
+ PERFORM public.logistics_enqueue_sticker_with_slip(_queue_item_id, _printer_id, _session_id, COALESCE(_is_herprint,false), _cart_id, job_id);
29381
+ UPDATE public.pick_queue SET print_job_id = job_id WHERE id = _queue_item_id;
29382
+ RETURN job_id;
29383
+ END;
29384
+ $function$;
29385
+
29386
+ -- ===== 1.0.176 verzenddocument Floriday bepalen =====
29387
+ -- 1.0.176: welk verzenddocument levert Floriday voor deze order (afleverbrief of veilingbrief).
29388
+ -- Bron: logistics_slip_shipping_note.fulfillment_order_id -> floriday_fulfillment_orders_cache.floriday_id,
29389
+ -- velden data->>'type', data->>'isReAuctioning', data->'deliveryNoteCodes'. Fail closed: niet gokken.
29390
+ CREATE OR REPLACE FUNCTION public.logistics_floriday_shipping_document(p_order_id uuid)
29391
+ RETURNS jsonb
29392
+ LANGUAGE plpgsql
29393
+ STABLE
29394
+ SET search_path TO 'public'
29395
+ AS $function$
29396
+ DECLARE
29397
+ v_fo text;
29398
+ v_data jsonb;
29399
+ v_type text;
29400
+ BEGIN
29401
+ IF to_regclass('public.logistics_slip_shipping_note') IS NULL OR to_regclass('public.floriday_fulfillment_orders_cache') IS NULL THEN
29402
+ RETURN jsonb_build_object('ok', false, 'documenttype', NULL, 'fulfillment_order_id', NULL,
29403
+ 'reden', 'De Floriday-zendinggegevens ontbreken in deze omgeving; het verzenddocument kan niet worden bepaald.');
29404
+ END IF;
29405
+ SELECT s.fulfillment_order_id::text INTO v_fo
29406
+ FROM public.logistics_slip_shipping_note s
29407
+ WHERE s.order_id = p_order_id AND s.fulfillment_order_id IS NOT NULL
29408
+ ORDER BY s.updated_at DESC NULLS LAST, s.created_at DESC LIMIT 1;
29409
+ IF v_fo IS NULL THEN
29410
+ RETURN jsonb_build_object('ok', false, 'documenttype', NULL, 'fulfillment_order_id', NULL,
29411
+ 'reden', 'Nog geen Floriday-zending bekend voor deze order; het verzenddocument volgt zodra Floriday die heeft vastgelegd.');
29412
+ END IF;
29413
+ SELECT c.data INTO v_data FROM public.floriday_fulfillment_orders_cache c
29414
+ WHERE c.floriday_id::text = v_fo AND c.is_deleted IS NOT TRUE ORDER BY c.fetched_at DESC NULLS LAST LIMIT 1;
29415
+ IF v_data IS NULL THEN
29416
+ RETURN jsonb_build_object('ok', false, 'documenttype', NULL, 'fulfillment_order_id', v_fo,
29417
+ 'reden', 'De Floriday-zending van deze order is nog niet opgehaald; het verzenddocument volgt na de volgende Floriday-synchronisatie.');
29418
+ END IF;
29419
+ v_type := upper(coalesce(v_data->>'type', ''));
29420
+ IF lower(coalesce(v_data->>'isReAuctioning', '')) = 'true' OR v_type LIKE '%CLOCK%' OR v_type LIKE '%AUCTION%' THEN
29421
+ RETURN jsonb_build_object('ok', true, 'documenttype', 'veilingbrief', 'fulfillment_order_id', v_fo, 'reden', NULL);
29422
+ END IF;
29423
+ IF v_type = 'DIRECT_SALES' AND jsonb_typeof(v_data->'deliveryNoteCodes') = 'array'
29424
+ AND jsonb_array_length(v_data->'deliveryNoteCodes') > 0 THEN
29425
+ RETURN jsonb_build_object('ok', true, 'documenttype', 'afleverbrief', 'fulfillment_order_id', v_fo, 'reden', NULL);
29426
+ END IF;
29427
+ RETURN jsonb_build_object('ok', false, 'documenttype', NULL, 'fulfillment_order_id', v_fo,
29428
+ 'reden', 'Onbekend Floriday-zendingtype "' || coalesce(nullif(v_type, ''), 'leeg')
29429
+ || '" of nog geen verzendbriefcode; het verzenddocument wordt niet gegokt.');
29430
+ END;
29431
+ $function$;
29432
+ REVOKE EXECUTE ON FUNCTION public.logistics_floriday_shipping_document(uuid) FROM PUBLIC, anon;
29433
+ GRANT EXECUTE ON FUNCTION public.logistics_floriday_shipping_document(uuid) TO authenticated, service_role;
29434
+
29435
+ CREATE OR REPLACE FUNCTION public.logistics_document_for_order(p_order_id uuid, p_documenttype text)
29436
+ RETURNS jsonb
29437
+ LANGUAGE plpgsql
29438
+ STABLE
29439
+ SET search_path TO 'public'
29440
+ AS $function$
29441
+ DECLARE
29442
+ v_soort text := lower(btrim(coalesce(p_documenttype, '')));
29443
+ v_floriday boolean;
29444
+ v_bron text;
29445
+ v_type text;
29446
+ v_res jsonb;
29447
+ v_n int; v_std int;
29448
+ v_l public.document_layouts%ROWTYPE;
29449
+ v_ship jsonb;
29450
+ v_extra jsonb := '{}'::jsonb;
29451
+ BEGIN
29452
+ IF v_soort = '' THEN
29453
+ RETURN jsonb_build_object('ok', false, 'layout_id', NULL, 'reden', 'Geen documentsoort opgegeven.');
29454
+ END IF;
29455
+
29456
+ -- 1.0.176: 'verzenddocument' wordt hier vertaald naar het echte document van de order.
29457
+ IF v_soort IN ('verzenddocument', 'verzendbrief') THEN
29458
+ SELECT o.order_source = 'floriday' INTO v_floriday FROM public.orders o WHERE o.id = p_order_id;
29459
+ IF NOT FOUND THEN
29460
+ RETURN jsonb_build_object('ok', false, 'layout_id', NULL, 'reden', 'Order niet gevonden.');
29461
+ END IF;
29462
+ IF coalesce(v_floriday, false) THEN
29463
+ v_ship := public.logistics_floriday_shipping_document(p_order_id);
29464
+ IF NOT coalesce((v_ship->>'ok')::boolean, false) THEN
29465
+ RETURN jsonb_build_object('ok', false, 'layout_id', NULL, 'keuze', 'engine',
29466
+ 'fulfillment_order_id', v_ship->>'fulfillment_order_id', 'reden', v_ship->>'reden');
29467
+ END IF;
29468
+ v_soort := v_ship->>'documenttype';
29469
+ v_extra := jsonb_build_object('verzenddocument_bepaald', v_soort,
29470
+ 'fulfillment_order_id', v_ship->>'fulfillment_order_id');
29471
+ ELSE
29472
+ v_soort := 'afleverbrief';
29473
+ v_extra := jsonb_build_object('verzenddocument_bepaald', 'afleverbrief');
29474
+ END IF;
29475
+ END IF;
29476
+
29477
+ IF to_regprocedure('public.layout_voor_document(uuid,text)') IS NOT NULL THEN
29478
+ EXECUTE 'SELECT to_jsonb(public.layout_voor_document($1, $2))' INTO v_res USING p_order_id, v_soort;
29479
+ IF jsonb_typeof(v_res) IS DISTINCT FROM 'object' THEN
29480
+ RETURN jsonb_build_object('ok', false, 'layout_id', NULL, 'keuze', 'layouts',
29481
+ 'reden', 'Layouts gaf geen bruikbaar antwoord voor ' || v_soort || '.') || v_extra;
29482
+ END IF;
29483
+ RETURN v_res || jsonb_build_object('keuze', 'layouts',
29484
+ 'ok', coalesce((v_res->>'ok')::boolean, nullif(v_res->>'layout_id', '') IS NOT NULL)) || v_extra;
29485
+ END IF;
29486
+ SELECT o.order_source = 'floriday' INTO v_floriday FROM public.orders o WHERE o.id = p_order_id;
29487
+ IF v_floriday IS NULL THEN
29488
+ SELECT EXISTS (SELECT 1 FROM public.orders WHERE id = p_order_id) INTO v_floriday;
29489
+ IF NOT v_floriday THEN
29490
+ RETURN jsonb_build_object('ok', false, 'layout_id', NULL, 'keuze', 'automatisch', 'reden', 'Order niet gevonden.');
29491
+ END IF;
29492
+ v_floriday := false;
29493
+ END IF;
29494
+ v_type := CASE
29495
+ WHEN v_soort IN ('eigen_sticker', 'sticker', 'etiket') THEN 'sticker'
29496
+ WHEN v_soort IN ('verzenddocument', 'verzendbrief', 'afleverbrief', 'afleverbon') THEN 'afleverbrief'
29497
+ WHEN v_soort IN ('veilingbrief', 'veilingbon') THEN 'veilingbrief'
29498
+ WHEN v_soort LIKE '%pakbon%' THEN 'pakbon'
29499
+ WHEN v_soort LIKE '%productie%' THEN 'productiebon'
29500
+ ELSE v_soort END;
29501
+ v_bron := CASE
29502
+ WHEN v_floriday AND v_soort <> 'eigen_sticker' AND v_type IN ('sticker', 'traysticker', 'afleverbrief', 'veilingbrief') THEN 'floriday'
29503
+ ELSE 'eigen' END;
29504
+ IF v_bron = 'eigen' AND v_type = 'veilingbrief' THEN
29505
+ RETURN jsonb_build_object('ok', false, 'layout_id', NULL, 'keuze', 'automatisch',
29506
+ 'reden', 'Een veilingbrief hoort alleen bij een Floriday-order; voor een eigen order wordt de afleverbrief gebruikt.');
29507
+ END IF;
29508
+ SELECT count(*), count(*) FILTER (WHERE is_standaard IS TRUE) INTO v_n, v_std
29509
+ FROM public.document_layouts
29510
+ WHERE actief IS TRUE AND coalesce(bron, 'eigen') = v_bron AND documenttype::text = v_type;
29511
+ IF v_n >= 1 THEN
29512
+ SELECT * INTO v_l FROM public.document_layouts
29513
+ WHERE actief IS TRUE AND coalesce(bron, 'eigen') = v_bron AND documenttype::text = v_type
29514
+ ORDER BY is_standaard DESC NULLS LAST, updated_at DESC LIMIT 1;
29515
+ END IF;
29516
+ IF v_l.id IS NULL THEN
29517
+ RETURN jsonb_build_object('ok', false, 'layout_id', NULL, 'keuze', 'automatisch', 'bron', v_bron, 'documenttype', v_type,
29518
+ 'reden', CASE WHEN v_bron = 'floriday'
29519
+ THEN 'Geen actieve ' || v_type || '-layout met bron Floriday. Activeer de Floriday-systeemlayout in Layouts en printers.'
29520
+ ELSE 'Maak of activeer een ' || v_type || 'layout in Layouts en printers.' END) || v_extra;
29521
+ END IF;
29522
+ RETURN jsonb_build_object('ok', true, 'layout_id', v_l.id, 'keuze', 'automatisch', 'bron', v_bron,
29523
+ 'documenttype', v_type, 'breedte_mm', v_l.breedte_mm, 'hoogte_mm', v_l.hoogte_mm, 'reden', NULL) || v_extra;
29524
+ END;
29525
+ $function$;
29526
+
29527
+
29528
+ -- ===== 1.0.178: fusten en land per kar, eigen traystickers =====
29529
+ -- 1.0.178 A: fusten en land per kar in control_get_todays_carts (achteraan toegevoegd)
29530
+ DO $m$ BEGIN
29531
+ IF pg_get_function_result('public.control_get_todays_carts()'::regprocedure) NOT LIKE '%crates_on_cart%' THEN
29532
+ DROP FUNCTION public.control_get_todays_carts();
29533
+ END IF;
29534
+ END $m$;
29535
+
29536
+ CREATE OR REPLACE FUNCTION public.control_get_todays_carts()
29537
+ RETURNS TABLE(cart_number_id uuid, cart_number text, order_source text, order_id text, cart_index integer, order_number text, pick_date date, status text, printed_at timestamp with time zone, printed_today boolean, crates_on_cart integer, country_code text)
29538
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
29539
+ AS $function$
29540
+ WITH vandaag AS (
29541
+ SELECT (now() AT TIME ZONE public.logistics_tz())::date AS d
29542
+ ),
29543
+ idx AS (
29544
+ SELECT DISTINCT ON (i.cart_id) i.*
29545
+ FROM public.logistics_cart_index((SELECT d FROM vandaag), (SELECT d FROM vandaag)) i
29546
+ ORDER BY i.cart_id, i.order_number
29547
+ ),
29548
+ gecontroleerd AS (
29549
+ SELECT cci.cart_number_id, cci.order_source, cci.order_id, cci.cart_index
29550
+ FROM public.cart_control_items cci
29551
+ JOIN public.cart_control_sessions ccs ON ccs.id = cci.session_id
29552
+ WHERE ccs.control_date = (SELECT d FROM vandaag)
29553
+ AND cci.action = 'checked'
29554
+ )
29555
+ SELECT
29556
+ i.cart_id,
29557
+ COALESCE(cn.cart_number, i.cart_label),
29558
+ i.order_source,
29559
+ i.order_uuid::text,
29560
+ i.cart_index,
29561
+ i.order_number,
29562
+ i.pick_date,
29563
+ COALESCE(cn.status, 'pending'),
29564
+ i.printed_at,
29565
+ (i.printed_at >= (SELECT d FROM vandaag)::timestamptz),
29566
+ (SELECT sum(ci.crates)::int FROM public.order_cart_items ci WHERE ci.cart_id = i.cart_id),
29567
+ (SELECT CASE WHEN upper(btrim(l.country)) ~ '^[A-Z]{2}$' THEN upper(btrim(l.country)) END
29568
+ FROM public.orders o
29569
+ JOIN public.customer_delivery_locations l
29570
+ ON l.id = public.logistics_try_uuid(o.custom_fields->>'delivery_location')
29571
+ WHERE o.id = i.order_uuid
29572
+ LIMIT 1)
29573
+ FROM idx i
29574
+ LEFT JOIN public.cart_numbers cn
29575
+ ON cn.order_source = i.order_source
29576
+ AND cn.order_id = i.order_uuid::text
29577
+ AND cn.cart_index = i.cart_index
29578
+ LEFT JOIN gecontroleerd g1 ON g1.cart_number_id = i.cart_id
29579
+ LEFT JOIN gecontroleerd g2
29580
+ ON g2.order_source = i.order_source
29581
+ AND g2.order_id = i.order_uuid::text
29582
+ AND g2.cart_index = i.cart_index
29583
+ WHERE COALESCE(cn.status, 'pending') <> 'delivered'
29584
+ AND g1.cart_number_id IS NULL
29585
+ AND g2.order_id IS NULL
29586
+ ORDER BY i.order_number, i.cart_index;
29587
+ $function$;
29588
+ REVOKE ALL ON FUNCTION public.control_get_todays_carts() FROM PUBLIC, anon;
29589
+ GRANT EXECUTE ON FUNCTION public.control_get_todays_carts() TO authenticated, service_role;
29590
+
29591
+ -- 1.0.178 B: eigen traystickers
29592
+ ALTER TABLE public.logistics_sticker_prints DROP CONSTRAINT IF EXISTS logistics_sticker_prints_soort_check;
29593
+ ALTER TABLE public.logistics_sticker_prints ADD CONSTRAINT logistics_sticker_prints_soort_check
29594
+ CHECK (soort = ANY (ARRAY['sticker','traysticker','eigen_sticker','eigen_traysticker']));
29595
+
29596
+ CREATE OR REPLACE FUNCTION public.logistics_own_traysticker_sync(p_limit integer DEFAULT 200)
29597
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
29598
+ AS $function$
29599
+ DECLARE
29600
+ s jsonb; v_tray text; v_rel text; v_station text;
29601
+ r record; v_bron text; v_keep text[] := '{}'; n int := 0; n_geblokkeerd int := 0; n_vrij int := 0; n_uit int := 0;
29602
+ BEGIN
29603
+ SELECT settings INTO s FROM public.logistics_rule_configs
29604
+ ORDER BY is_active DESC, (status = 'actief') DESC, updated_at DESC LIMIT 1;
29605
+ v_tray := public.logistics_rule_text(s, 'pickstations', 'traysticker_release', 'uit');
29606
+ IF v_tray = 'uit' THEN
29607
+ RETURN jsonb_build_object('trayRelease', v_tray, 'regels', 0);
29608
+ END IF;
29609
+ v_rel := public.logistics_rule_text(s, 'pickstations', 'sticker_release', 'with_pick_release');
29610
+ v_station := CASE WHEN v_rel = 'with_packing_slip'
29611
+ THEN public.logistics_rule_text(s, 'pickstations', 'packing_slip_station', '')
29612
+ ELSE public.logistics_rule_text(s, 'pickstations', 'sticker_station', '') END;
29613
+
29614
+ FOR r IN
29615
+ SELECT o.id AS order_id, o.order_number, ci.cart_id,
29616
+ bool_or(ci.layer_index IS NULL) AS zonder_indeling,
29617
+ array_agg(DISTINCT ci.layer_index) FILTER (WHERE ci.layer_index IS NOT NULL) AS lagen
29618
+ FROM public.orders o
29619
+ JOIN public.order_items oi ON oi.order_id = o.id
29620
+ JOIN public.order_cart_items ci ON ci.order_item_id = oi.id
29621
+ WHERE coalesce(o.order_source, '') <> 'floriday'
29622
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
29623
+ AND coalesce(o.status, '') <> 'geannuleerd'
29624
+ AND oi.quarantined_at IS NULL
29625
+ AND o.delivery_date >= current_date - 1
29626
+ GROUP BY o.id, o.order_number, ci.cart_id
29627
+ LIMIT greatest(1, p_limit)
29628
+ LOOP
29629
+ IF r.zonder_indeling OR r.lagen IS NULL THEN
29630
+ v_bron := r.order_id::text || ':' || r.cart_id::text || ':geen-indeling';
29631
+ v_keep := v_keep || v_bron;
29632
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, status, reden)
29633
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, r.cart_id, 1, 'geblokkeerd',
29634
+ 'Trayindeling ontbreekt voor deze kar: niet elke orderregel heeft een tray (laag). Deel de kar opnieuw in; de rest van de order gaat door.')
29635
+ ON CONFLICT DO NOTHING;
29636
+ n_geblokkeerd := n_geblokkeerd + 1;
29637
+ CONTINUE;
29638
+ END IF;
29639
+ FOR i IN 1 .. array_length(r.lagen, 1) LOOP
29640
+ v_bron := r.order_id::text || ':' || r.cart_id::text || ':' || r.lagen[i]::text;
29641
+ v_keep := v_keep || v_bron;
29642
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, print_plek_id, status, reden)
29643
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, r.cart_id, 1,
29644
+ public.logistics_sticker_cart_plek(r.cart_id, v_station), 'wacht_op_moment', 'Wacht op de pakbon van deze kar.')
29645
+ ON CONFLICT DO NOTHING;
29646
+ n := n + 1;
29647
+ END LOOP;
29648
+ END LOOP;
29649
+
29650
+ -- Indeling hersteld of tray vervallen: niet meer printen (alleen nog niet geprinte rijen).
29651
+ UPDATE public.logistics_sticker_prints p
29652
+ SET status = 'niet_printen', reden = 'Tray hoort niet meer bij deze kar of order vervallen.', updated_at = now()
29653
+ WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
29654
+ AND NOT (p.bron_id = ANY (v_keep))
29655
+ AND p.order_id IN (SELECT o.id FROM public.orders o WHERE o.delivery_date >= current_date - 1);
29656
+ GET DIAGNOSTICS n_uit = ROW_COUNT;
29657
+
29658
+ -- Vrijgave: met de pakbon van de kar (zoals eigen stickers).
29659
+ UPDATE public.logistics_sticker_prints p
29660
+ SET status = 'klaar', reden = NULL,
29661
+ print_plek_id = coalesce(p.print_plek_id, public.logistics_sticker_cart_plek(p.cart_id, v_station)),
29662
+ updated_at = now()
29663
+ WHERE p.soort = 'eigen_traysticker' AND p.status = 'wacht_op_moment'
29664
+ AND p.cart_id IS NOT NULL AND public.logistics_cart_slip_queued(p.cart_id);
29665
+ GET DIAGNOSTICS n_vrij = ROW_COUNT;
29666
+
29667
+ RETURN jsonb_build_object('trayRelease', v_tray, 'trays', n, 'geblokkeerd', n_geblokkeerd, 'vrijgegeven', n_vrij, 'niet_printen', n_uit);
29668
+ END; $function$;
29669
+ REVOKE ALL ON FUNCTION public.logistics_own_traysticker_sync(integer) FROM PUBLIC, anon, authenticated;
29670
+ GRANT EXECUTE ON FUNCTION public.logistics_own_traysticker_sync(integer) TO service_role;
29671
+
29672
+ CREATE OR REPLACE FUNCTION public.logistics_own_traysticker_payload(p_id uuid)
29673
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
29674
+ AS $function$
29675
+ DECLARE p record; o record; v_laag int; v_nr int; v_tot int; v_loc text; v_prod jsonb;
29676
+ BEGIN
29677
+ SELECT * INTO p FROM public.logistics_sticker_prints WHERE id = p_id AND soort = 'eigen_traysticker';
29678
+ IF p.id IS NULL THEN RETURN jsonb_build_object('ok', false, 'reden', 'Traysticker niet gevonden.'); END IF;
29679
+ v_laag := nullif(split_part(p.bron_id, ':', 3), '')::int;
29680
+ SELECT * INTO o FROM public.orders WHERE id = p.order_id;
29681
+ SELECT count(DISTINCT ci.layer_index),
29682
+ count(DISTINCT ci.layer_index) FILTER (WHERE ci.layer_index <= v_laag)
29683
+ INTO v_tot, v_nr
29684
+ FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
29685
+ WHERE oi.order_id = p.order_id AND ci.cart_id = p.cart_id AND ci.layer_index IS NOT NULL;
29686
+ SELECT coalesce(jsonb_agg(jsonb_build_object(
29687
+ 'artikelnummer', prd.article_number,
29688
+ 'naam', coalesce(ci.product_name, oi.product_name),
29689
+ 'aantal', coalesce(ci.crates_in_layer, ci.crates)) ORDER BY coalesce(ci.product_name, oi.product_name)), '[]'::jsonb)
29690
+ INTO v_prod
29691
+ FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
29692
+ LEFT JOIN public.products prd ON prd.id = coalesce(ci.product_id, oi.product_id)
29693
+ WHERE oi.order_id = p.order_id AND ci.cart_id = p.cart_id AND ci.layer_index = v_laag;
29694
+ SELECT nullif(btrim(concat_ws(', ', nullif(btrim(l.name), ''), nullif(btrim(l.city), ''), nullif(btrim(l.country), ''))), '')
29695
+ INTO v_loc
29696
+ FROM public.customer_delivery_locations l
29697
+ WHERE l.id = public.logistics_try_uuid(o.custom_fields->>'delivery_location');
29698
+ IF v_tot IS NULL OR v_tot = 0 OR jsonb_array_length(v_prod) = 0 THEN
29699
+ RETURN jsonb_build_object('ok', false, 'reden', 'Trayindeling ontbreekt voor deze tray. Deel de kar opnieuw in.');
29700
+ END IF;
29701
+ RETURN jsonb_build_object('ok', true, 'payload', jsonb_build_object(
29702
+ 'order_id', o.id, 'order_number', o.order_number, 'ordernummer', o.order_number,
29703
+ 'klantnaam', o.customer_name, 'leverdatum', o.delivery_date, 'afleverlocatie', v_loc,
29704
+ 'cart_id', p.cart_id, 'tray_nummer', v_nr, 'tray_totaal', v_tot, 'tray_producten', v_prod));
29705
+ END; $function$;
29706
+ REVOKE ALL ON FUNCTION public.logistics_own_traysticker_payload(uuid) FROM PUBLIC, anon, authenticated;
29707
+ GRANT EXECUTE ON FUNCTION public.logistics_own_traysticker_payload(uuid) TO service_role;
29708
+ -- ===== 1.0.178 correctie: eigen traysticker per fust =====
29709
+ -- 1.0.178 correctie: eigen traysticker per FUST (niet per laag).
29710
+ -- Sleutel: order:kar:karregel:fustnummer. Crates onbekend: één geblokkeerde rij per karregel.
29711
+ -- Planten op fust x van y binnen een orderregel: ppc, laatste fust = de rest (nooit <= 0).
29712
+ CREATE OR REPLACE FUNCTION public.logistics_tray_plants(p_ppc integer, p_regel_totaal integer, p_fust integer, p_fusten integer)
29713
+ RETURNS integer LANGUAGE sql IMMUTABLE SET search_path = public AS $tp$
29714
+ SELECT CASE
29715
+ WHEN p_ppc IS NULL OR p_ppc <= 0 THEN NULL
29716
+ WHEN p_fust < p_fusten OR p_regel_totaal IS NULL THEN p_ppc
29717
+ WHEN p_regel_totaal - p_ppc * (p_fusten - 1) > 0 THEN p_regel_totaal - p_ppc * (p_fusten - 1)
29718
+ ELSE p_ppc
29719
+ END
29720
+ $tp$;
29721
+
29722
+ CREATE OR REPLACE FUNCTION public.logistics_own_traysticker_sync(p_limit integer DEFAULT 200)
29723
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
29724
+ AS $function$
29725
+ DECLARE
29726
+ s jsonb; v_tray text; v_rel text; v_station text;
29727
+ r record; v_bron text; v_keep text[] := '{}'; n int := 0; n_geblokkeerd int := 0; n_vrij int := 0; n_uit int := 0;
29728
+ BEGIN
29729
+ SELECT settings INTO s FROM public.logistics_rule_configs
29730
+ ORDER BY is_active DESC, (status = 'actief') DESC, updated_at DESC LIMIT 1;
29731
+ v_tray := public.logistics_rule_text(s, 'pickstations', 'traysticker_release', 'uit');
29732
+ IF v_tray = 'uit' THEN
29733
+ RETURN jsonb_build_object('trayRelease', v_tray, 'regels', 0);
29734
+ END IF;
29735
+ v_rel := public.logistics_rule_text(s, 'pickstations', 'sticker_release', 'with_pick_release');
29736
+ v_station := CASE WHEN v_rel = 'with_packing_slip'
29737
+ THEN public.logistics_rule_text(s, 'pickstations', 'packing_slip_station', '')
29738
+ ELSE public.logistics_rule_text(s, 'pickstations', 'sticker_station', '') END;
29739
+
29740
+ FOR r IN
29741
+ SELECT o.id AS order_id, o.order_number, ci.cart_id, ci.id AS ci_id, ci.crates
29742
+ FROM public.orders o
29743
+ JOIN public.order_items oi ON oi.order_id = o.id
29744
+ JOIN public.order_cart_items ci ON ci.order_item_id = oi.id
29745
+ WHERE coalesce(o.order_source, '') <> 'floriday'
29746
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
29747
+ AND coalesce(o.status, '') <> 'geannuleerd'
29748
+ AND oi.quarantined_at IS NULL
29749
+ AND o.delivery_date >= current_date - 1
29750
+ ORDER BY o.order_number, ci.cart_id, ci.created_at, ci.id
29751
+ LIMIT greatest(1, p_limit)
29752
+ LOOP
29753
+ IF r.crates IS NULL OR r.crates <= 0 THEN
29754
+ v_bron := r.order_id::text || ':' || r.cart_id::text || ':' || r.ci_id::text || ':onbekend';
29755
+ v_keep := v_keep || v_bron;
29756
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, status, reden)
29757
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, r.cart_id, 1, 'geblokkeerd',
29758
+ 'Aantal fusten onbekend voor deze regel op de kar. De rest van de order gaat door.')
29759
+ ON CONFLICT DO NOTHING;
29760
+ n_geblokkeerd := n_geblokkeerd + 1;
29761
+ CONTINUE;
29762
+ END IF;
29763
+ FOR k IN 1 .. r.crates LOOP
29764
+ v_bron := r.order_id::text || ':' || r.cart_id::text || ':' || r.ci_id::text || ':' || k::text;
29765
+ v_keep := v_keep || v_bron;
29766
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, print_plek_id, status, reden)
29767
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, r.cart_id, 1,
29768
+ public.logistics_sticker_cart_plek(r.cart_id, v_station), 'wacht_op_moment', 'Wacht op de pakbon van deze kar.')
29769
+ ON CONFLICT DO NOTHING;
29770
+ n := n + 1;
29771
+ END LOOP;
29772
+ END LOOP;
29773
+
29774
+ -- Oude laagsleutels, vervallen fusten of orders: niet meer printen (alleen nog niet geprinte rijen, geen DELETE).
29775
+ UPDATE public.logistics_sticker_prints p
29776
+ SET status = 'niet_printen', reden = 'Fust hoort niet meer bij deze kar of order vervallen.', updated_at = now()
29777
+ WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
29778
+ AND NOT (p.bron_id = ANY (v_keep))
29779
+ AND p.order_id IN (SELECT o.id FROM public.orders o WHERE o.delivery_date >= current_date - 1);
29780
+ GET DIAGNOSTICS n_uit = ROW_COUNT;
29781
+
29782
+ UPDATE public.logistics_sticker_prints p
29783
+ SET status = 'klaar', reden = NULL,
29784
+ print_plek_id = coalesce(p.print_plek_id, public.logistics_sticker_cart_plek(p.cart_id, v_station)),
29785
+ updated_at = now()
29786
+ WHERE p.soort = 'eigen_traysticker' AND p.status = 'wacht_op_moment'
29787
+ AND p.cart_id IS NOT NULL AND public.logistics_cart_slip_queued(p.cart_id);
29788
+ GET DIAGNOSTICS n_vrij = ROW_COUNT;
29789
+
29790
+ RETURN jsonb_build_object('trayRelease', v_tray, 'fusten', n, 'geblokkeerd', n_geblokkeerd, 'vrijgegeven', n_vrij, 'niet_printen', n_uit);
29791
+ END; $function$;
29792
+ REVOKE ALL ON FUNCTION public.logistics_own_traysticker_sync(integer) FROM PUBLIC, anon, authenticated;
29793
+ GRANT EXECUTE ON FUNCTION public.logistics_own_traysticker_sync(integer) TO service_role;
29794
+
29795
+ CREATE OR REPLACE FUNCTION public.logistics_own_traysticker_payload(p_id uuid)
29796
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
29797
+ AS $function$
29798
+ DECLARE
29799
+ p record; o record; c record; v_ci uuid; v_k int; v_nr int; v_tot int; v_loc text;
29800
+ v_ppc int; v_regel_tot int; v_item_voor int; v_item_fusten int; v_aantal int; v_prod jsonb;
29801
+ BEGIN
29802
+ SELECT * INTO p FROM public.logistics_sticker_prints WHERE id = p_id AND soort = 'eigen_traysticker';
29803
+ IF p.id IS NULL THEN RETURN jsonb_build_object('ok', false, 'reden', 'Traysticker niet gevonden.'); END IF;
29804
+ v_ci := public.logistics_try_uuid(split_part(p.bron_id, ':', 3));
29805
+ v_k := CASE WHEN split_part(p.bron_id, ':', 4) ~ '^[0-9]+$' THEN split_part(p.bron_id, ':', 4)::int END;
29806
+ IF v_ci IS NULL OR v_k IS NULL THEN
29807
+ RETURN jsonb_build_object('ok', false, 'reden', 'Aantal fusten onbekend');
29808
+ END IF;
29809
+ SELECT ci.*, oi.unit, oi.quantity, oi.plants_per_crate AS ppc_regel, oi.product_name AS oi_naam,
29810
+ prd.article_number, prd.plants_per_crate AS ppc_product, ci.order_item_id AS item_id
29811
+ INTO c
29812
+ FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
29813
+ LEFT JOIN public.products prd ON prd.id = coalesce(ci.product_id, oi.product_id)
29814
+ WHERE ci.id = v_ci;
29815
+ IF c.id IS NULL OR c.crates IS NULL OR c.crates < v_k THEN
29816
+ RETURN jsonb_build_object('ok', false, 'reden', 'Aantal fusten onbekend');
29817
+ END IF;
29818
+ SELECT * INTO o FROM public.orders WHERE id = p.order_id;
29819
+
29820
+ -- Fustnummer x van y binnen de order (alle karren), volgorde kar, regel, fust.
29821
+ WITH alle AS (
29822
+ SELECT ci.id, ci.crates, row_number() OVER (ORDER BY ci.cart_id, ci.created_at, ci.id) AS rn
29823
+ FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
29824
+ WHERE oi.order_id = p.order_id AND oi.quarantined_at IS NULL AND coalesce(ci.crates, 0) > 0
29825
+ )
29826
+ SELECT sum(crates)::int,
29827
+ coalesce(sum(crates) FILTER (WHERE rn < (SELECT rn FROM alle WHERE id = v_ci)), 0)::int + v_k
29828
+ INTO v_tot, v_nr FROM alle;
29829
+
29830
+ -- Planten per fust: karregel, dan orderregel, dan product. Laatste fust van de regel: de rest.
29831
+ v_ppc := coalesce(nullif(c.plants_per_crate, 0), nullif(c.ppc_regel, 0), nullif(c.ppc_product, 0))::int;
29832
+ IF v_ppc IS NULL THEN
29833
+ RETURN jsonb_build_object('ok', false, 'reden', 'Planten per fust onbekend (niet op de regel en niet bij het product). Vul planten per fust in.');
29834
+ END IF;
29835
+ v_regel_tot := public.logistics_own_sticker_count(c.unit, c.quantity, c.ppc_regel, c.ppc_product);
29836
+ SELECT coalesce(sum(ci.crates) FILTER (WHERE (ci.cart_id, ci.created_at, ci.id) < (c.cart_id, c.created_at, c.id)), 0)::int,
29837
+ coalesce(sum(ci.crates), 0)::int
29838
+ INTO v_item_voor, v_item_fusten
29839
+ FROM public.order_cart_items ci WHERE ci.order_item_id = c.item_id AND coalesce(ci.crates, 0) > 0;
29840
+ v_aantal := public.logistics_tray_plants(v_ppc, v_regel_tot, v_item_voor + v_k, v_item_fusten);
29841
+
29842
+ v_prod := jsonb_build_array(jsonb_strip_nulls(jsonb_build_object(
29843
+ 'artikelnummer', c.article_number,
29844
+ 'naam', coalesce(c.product_name, c.oi_naam),
29845
+ 'aantal', v_aantal,
29846
+ 'onderdelen', CASE WHEN jsonb_typeof(to_jsonb(c.components)) = 'array' AND jsonb_array_length(to_jsonb(c.components)) > 0
29847
+ THEN to_jsonb(c.components) END)));
29848
+
29849
+ SELECT nullif(btrim(concat_ws(', ', nullif(btrim(l.name), ''), nullif(btrim(l.city), ''), nullif(btrim(l.country), ''))), '')
29850
+ INTO v_loc
29851
+ FROM public.customer_delivery_locations l
29852
+ WHERE l.id = public.logistics_try_uuid(o.custom_fields->>'delivery_location');
29853
+
29854
+ RETURN jsonb_build_object('ok', true, 'payload', jsonb_build_object(
29855
+ 'order_id', o.id, 'order_number', o.order_number, 'ordernummer', o.order_number,
29856
+ 'klantnaam', o.customer_name, 'leverdatum', o.delivery_date, 'afleverlocatie', v_loc,
29857
+ 'cart_id', p.cart_id, 'tray_nummer', v_nr, 'tray_totaal', v_tot, 'tray_producten', v_prod));
29858
+ END; $function$;
29859
+ REVOKE ALL ON FUNCTION public.logistics_own_traysticker_payload(uuid) FROM PUBLIC, anon, authenticated;
29860
+ GRANT EXECUTE ON FUNCTION public.logistics_own_traysticker_payload(uuid) TO service_role;
29861
+
29862
+ -- ===== 1.0.179: noodreparaties karindeling =====
29863
+ -- 1.0.179 fase 1: noodreparaties karindeling (alle ordersoorten FO/HO/MO/AO).
29864
+ -- Alleen eigen Engine-objecten; idempotent; geen omgevingswaarden; geen datacorrectie.
29865
+
29866
+ -- 3. Bevestigingen van een onwaarschijnlijke belading: één per order en verpakking.
29867
+ CREATE TABLE IF NOT EXISTS public.logistics_belading_bevestigingen (
29868
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
29869
+ order_id uuid NOT NULL,
29870
+ verpakking_sleutel text NOT NULL,
29871
+ redenen text[] NOT NULL DEFAULT '{}',
29872
+ bevestigd_door uuid,
29873
+ bevestigd_op timestamptz NOT NULL DEFAULT now(),
29874
+ created_at timestamptz NOT NULL DEFAULT now(),
29875
+ UNIQUE (order_id, verpakking_sleutel)
29876
+ );
29877
+ GRANT SELECT ON public.logistics_belading_bevestigingen TO authenticated;
29878
+ GRANT ALL ON public.logistics_belading_bevestigingen TO service_role;
29879
+ ALTER TABLE public.logistics_belading_bevestigingen ENABLE ROW LEVEL SECURITY;
29880
+ DO $p$ BEGIN
29881
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'logistics_belading_bevestigingen' AND policyname = 'beheer leest bevestigingen') THEN
29882
+ CREATE POLICY "beheer leest bevestigingen" ON public.logistics_belading_bevestigingen
29883
+ FOR SELECT TO authenticated USING (public.logistics_is_beheer());
29884
+ END IF;
29885
+ END $p$;
29886
+
29887
+ CREATE OR REPLACE FUNCTION public.logistics_belading_bevestig(p_order_id uuid, p_sleutel text, p_redenen text[] DEFAULT '{}')
29888
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
29889
+ AS $function$
29890
+ BEGIN
29891
+ IF NOT public.logistics_is_beheer() THEN
29892
+ RAISE EXCEPTION 'Alleen beheer mag een belading bevestigen.';
29893
+ END IF;
29894
+ IF p_order_id IS NULL OR coalesce(btrim(p_sleutel), '') = '' THEN
29895
+ RAISE EXCEPTION 'Order en verpakking zijn verplicht.';
29896
+ END IF;
29897
+ INSERT INTO public.logistics_belading_bevestigingen (order_id, verpakking_sleutel, redenen, bevestigd_door)
29898
+ VALUES (p_order_id, p_sleutel, coalesce(p_redenen, '{}'), auth.uid())
29899
+ ON CONFLICT (order_id, verpakking_sleutel) DO NOTHING;
29900
+ PERFORM public.logistics_plan_enqueue(p_order_id, 'belading bevestigd');
29901
+ RETURN jsonb_build_object('ok', true);
29902
+ END; $function$;
29903
+ REVOKE ALL ON FUNCTION public.logistics_belading_bevestig(uuid, text, text[]) FROM PUBLIC, anon;
29904
+ GRANT EXECUTE ON FUNCTION public.logistics_belading_bevestig(uuid, text, text[]) TO authenticated, service_role;
29905
+
29906
+ -- 5. Lease bezet: opdracht blijft open en komt later terug (nooit 'done' zonder indeling).
29907
+ CREATE OR REPLACE FUNCTION public.logistics_plan_retry_later(p_id uuid, p_seconds integer DEFAULT 30)
29908
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
29909
+ AS $function$
29910
+ BEGIN
29911
+ UPDATE public.logistics_plan_queue
29912
+ SET status = 'pending', claimed_at = NULL,
29913
+ next_attempt_at = now() + make_interval(secs => greatest(5, least(coalesce(p_seconds, 30), 600))),
29914
+ error = 'Indeling van deze buffer was bezet; later opnieuw.', updated_at = now()
29915
+ WHERE id = p_id AND status IN ('running', 'pending');
29916
+ END; $function$;
29917
+ REVOKE ALL ON FUNCTION public.logistics_plan_retry_later(uuid, integer) FROM PUBLIC, anon, authenticated;
29918
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_retry_later(uuid, integer) TO service_role;
29919
+
29920
+ -- 6. Oude SQL-indeler onbruikbaar: geen EXECUTE voor PUBLIC, anon of authenticated.
29921
+ DO $r$ BEGIN
29922
+ IF to_regprocedure('public.logistics_plan_all(uuid)') IS NOT NULL THEN
29923
+ REVOKE ALL ON FUNCTION public.logistics_plan_all(uuid) FROM PUBLIC, anon, authenticated;
29924
+ END IF;
29925
+ IF to_regprocedure('public.logistics_plan_buffer_sql(uuid)') IS NOT NULL THEN
29926
+ REVOKE ALL ON FUNCTION public.logistics_plan_buffer_sql(uuid) FROM PUBLIC, anon, authenticated;
29927
+ END IF;
29928
+ END $r$;
29929
+
29930
+ -- 7. Karnummers aan het kar-id (stabiel na herindeling). Kolom en index alleen toevoegen.
29931
+ DO $c$ BEGIN
29932
+ IF to_regclass('public.cart_numbers') IS NULL THEN
29933
+ RAISE NOTICE 'logistiek-engine: cart_numbers ontbreekt (andere module), kar-id-koppeling overgeslagen';
29934
+ ELSE
29935
+ ALTER TABLE public.cart_numbers ADD COLUMN IF NOT EXISTS cart_id uuid;
29936
+ IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'cart_numbers_pick_date_cart_id_uniq') THEN
29937
+ IF EXISTS (SELECT 1 FROM public.cart_numbers WHERE cart_id IS NOT NULL GROUP BY pick_date, cart_id HAVING count(*) > 1) THEN
29938
+ RAISE NOTICE 'logistiek-engine: dubbele kar-id per dag in cart_numbers; unieke index niet aangemaakt';
29939
+ ELSE
29940
+ CREATE UNIQUE INDEX cart_numbers_pick_date_cart_id_uniq ON public.cart_numbers (pick_date, cart_id) WHERE cart_id IS NOT NULL;
29941
+ END IF;
29942
+ END IF;
29943
+ END IF;
29944
+ END $c$;
29945
+
29946
+ -- Dubbele kar-indexen per order (twee karren met hetzelfde volgnummer): alleen-lezen lijst.
29947
+ CREATE OR REPLACE FUNCTION public.logistics_cart_seq_dubbel(p_order_uuid uuid DEFAULT NULL)
29948
+ RETURNS TABLE(order_uuid uuid, cart_index integer, cart_ids uuid[])
29949
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
29950
+ AS $function$
29951
+ WITH ruw AS (
29952
+ SELECT oc.id AS cart_id, oc.cart_number AS cart_index, oc.order_id AS order_uuid
29953
+ FROM logistics_active.order_carts oc
29954
+ WHERE oc.order_id IS NOT NULL AND (p_order_uuid IS NULL OR oc.order_id = p_order_uuid)
29955
+ UNION
29956
+ SELECT oc.id, oc.cart_number, oi.order_id
29957
+ FROM logistics_active.order_carts oc
29958
+ JOIN public.order_cart_items oci ON oci.cart_id = oc.id
29959
+ JOIN logistics_active.order_items oi ON oi.id = oci.order_item_id
29960
+ WHERE oc.order_id IS NULL AND oi.order_id IS NOT NULL AND (p_order_uuid IS NULL OR oi.order_id = p_order_uuid)
29961
+ )
29962
+ SELECT order_uuid, cart_index, array_agg(DISTINCT cart_id ORDER BY cart_id)
29963
+ FROM ruw GROUP BY order_uuid, cart_index HAVING count(DISTINCT cart_id) > 1;
29964
+ $function$;
29965
+ REVOKE ALL ON FUNCTION public.logistics_cart_seq_dubbel(uuid) FROM PUBLIC, anon, authenticated;
29966
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_seq_dubbel(uuid) TO service_role;
29967
+
29968
+ -- logistics_cart_seq: nooit stil een kar laten vallen. Bij een dubbel volgnummer: fout.
29969
+ -- Karnummer eerst via kar-id, anders via order + index (bestaande nummers).
29970
+ CREATE OR REPLACE FUNCTION public.logistics_cart_seq(p_order_uuid uuid DEFAULT NULL::uuid)
29971
+ RETURNS TABLE(order_uuid uuid, cart_id uuid, cart_index integer, cart_seq integer, cart_number text, cart_total integer)
29972
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
29973
+ AS $function$
29974
+ DECLARE d record;
29975
+ BEGIN
29976
+ SELECT * INTO d FROM public.logistics_cart_seq_dubbel(p_order_uuid) LIMIT 1;
29977
+ IF d.order_uuid IS NOT NULL THEN
29978
+ RAISE EXCEPTION 'Karnummering ongeldig: order % heeft % karren met volgnummer %. Deel de order opnieuw in; er wordt geen kar weggelaten.',
29979
+ d.order_uuid, array_length(d.cart_ids, 1), d.cart_index;
29980
+ END IF;
29981
+ RETURN QUERY
29982
+ WITH deelname AS (
29983
+ SELECT oc.id AS cart_id, oc.cart_number AS cart_index, oc.order_id AS order_uuid
29984
+ FROM public.order_carts oc
29985
+ WHERE oc.order_id IS NOT NULL AND (p_order_uuid IS NULL OR oc.order_id = p_order_uuid)
29986
+ UNION
29987
+ SELECT oc.id, oc.cart_number, oi.order_id
29988
+ FROM public.order_carts oc
29989
+ JOIN public.order_cart_items oci ON oci.cart_id = oc.id
29990
+ JOIN logistics_active.order_items oi ON oi.id = oci.order_item_id
29991
+ WHERE oc.order_id IS NULL AND oi.order_id IS NOT NULL AND (p_order_uuid IS NULL OR oi.order_id = p_order_uuid)
29992
+ ),
29993
+ genummerd AS (
29994
+ SELECT dd.order_uuid, dd.cart_id, dd.cart_index,
29995
+ dense_rank() OVER (PARTITION BY dd.order_uuid ORDER BY dd.cart_index)::int AS cart_seq,
29996
+ count(*) OVER (PARTITION BY dd.order_uuid)::int AS cart_total
29997
+ FROM deelname dd
29998
+ )
29999
+ SELECT g.order_uuid, g.cart_id, g.cart_index, g.cart_seq,
30000
+ COALESCE(
30001
+ (SELECT cn.cart_number FROM public.cart_numbers cn WHERE cn.cart_id = g.cart_id ORDER BY cn.assigned_at DESC LIMIT 1),
30002
+ (SELECT cn.cart_number FROM public.cart_numbers cn WHERE cn.order_id = g.order_uuid::text AND cn.cart_index = g.cart_index LIMIT 1),
30003
+ g.cart_seq::text),
30004
+ g.cart_total
30005
+ FROM genummerd g;
30006
+ END; $function$;
30007
+ REVOKE ALL ON FUNCTION public.logistics_cart_seq(uuid) FROM PUBLIC, anon;
30008
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_seq(uuid) TO authenticated, service_role;
30009
+
30010
+ -- 8. Kar wisselen: eerst vergrendelen en pickup_locked controleren, dan pas verwijderen; geen stille 1.
30011
+ CREATE OR REPLACE FUNCTION public.logistics_apply_cart_swap(p_delete_ids uuid[], p_new_carts jsonb)
30012
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
30013
+ AS $function$
30014
+ DECLARE
30015
+ v_del uuid[] := COALESCE(p_delete_ids, ARRAY[]::uuid[]);
30016
+ v_cart jsonb; v_row jsonb; v_id uuid; v_map jsonb := '[]'::jsonb;
30017
+ v_jobs int := 0; v_carts int := 0; v_items int := 0; v_locked int;
30018
+ BEGIN
30019
+ IF jsonb_typeof(COALESCE(p_new_carts, '[]'::jsonb)) <> 'array' THEN
30020
+ RAISE EXCEPTION 'Nieuwe karren ontbreken of zijn ongeldig.';
30021
+ END IF;
30022
+ IF EXISTS (
30023
+ SELECT 1 FROM jsonb_array_elements(COALESCE(p_new_carts, '[]'::jsonb)) c,
30024
+ jsonb_array_elements(COALESCE(c.value->'items', '[]'::jsonb)) i
30025
+ WHERE coalesce((i.value->>'plants_per_crate')::numeric, 0) <= 0
30026
+ OR coalesce((i.value->>'crates')::numeric, 0) < 0) THEN
30027
+ RAISE EXCEPTION 'Karwissel geweigerd: een karregel mist planten per fust (belading_onvolledig).';
30028
+ END IF;
30029
+
30030
+ IF array_length(v_del, 1) IS NOT NULL THEN
30031
+ PERFORM 1 FROM public.order_carts WHERE id = ANY(v_del) ORDER BY id FOR UPDATE;
30032
+ SELECT count(*) INTO v_locked FROM public.order_carts WHERE id = ANY(v_del) AND COALESCE(pickup_locked, false);
30033
+ IF v_locked > 0 THEN
30034
+ RAISE EXCEPTION 'Karwissel geweigerd: % kar(ren) zijn al vastgezet (opgehaald). Er is niets gewijzigd.', v_locked;
30035
+ END IF;
30036
+ UPDATE public.print_jobs
30037
+ SET status = 'geannuleerd'::print_job_status,
30038
+ foutmelding = 'Vervallen: kar verwijderd bij herindeling',
30039
+ updated_at = now()
30040
+ WHERE status IN ('wachtend'::print_job_status, 'bezig'::print_job_status)
30041
+ AND (payload->>'cart_id') = ANY(SELECT id::text FROM unnest(v_del) AS id);
30042
+ GET DIAGNOSTICS v_jobs = ROW_COUNT;
30043
+ DELETE FROM public.pakbon_barcode_inhoud WHERE cart_id = ANY(SELECT id::text FROM unnest(v_del) AS id);
30044
+ DELETE FROM public.order_cart_layer_positions WHERE cart_id = ANY(v_del);
30045
+ DELETE FROM public.order_cart_items WHERE cart_id = ANY(v_del);
30046
+ DELETE FROM public.order_carts WHERE id = ANY(v_del);
30047
+ GET DIAGNOSTICS v_carts = ROW_COUNT;
30048
+ END IF;
30049
+
30050
+ FOR v_cart IN SELECT value FROM jsonb_array_elements(COALESCE(p_new_carts, '[]'::jsonb)) LOOP
30051
+ v_row := v_cart->'row';
30052
+ v_id := gen_random_uuid();
30053
+ INSERT INTO public.order_carts (id, order_id, buffer_id, cart_number, total_carts, is_auto, is_complete, source,
30054
+ occupancy_pct, total_layers_used, carrier_type, carrier_type_id, max_layers, weight_kg, algorithm_version, layout_method)
30055
+ SELECT v_id, r.order_id, r.buffer_id, r.cart_number, r.total_carts, COALESCE(r.is_auto, true), COALESCE(r.is_complete, false),
30056
+ COALESCE(r.source, 'auto'), COALESCE(r.occupancy_pct, 0), COALESCE(r.total_layers_used, 0),
30057
+ COALESCE(r.carrier_type, 'trolley'), r.carrier_type_id, r.max_layers, r.weight_kg, COALESCE(r.algorithm_version, 1), r.layout_method
30058
+ FROM jsonb_to_record(v_row) AS r(order_id uuid, buffer_id uuid, cart_number integer, total_carts integer, is_auto boolean,
30059
+ is_complete boolean, source text, occupancy_pct numeric, total_layers_used numeric, carrier_type text, carrier_type_id uuid,
30060
+ max_layers integer, weight_kg numeric, algorithm_version integer, layout_method text);
30061
+
30062
+ INSERT INTO public.order_cart_items (cart_id, order_item_id, product_id, product_name, crates, boxes, weight_kg,
30063
+ plants_per_crate, crates_per_layer, layers_per_cart, layer_fraction, layer_share_pct, layer_index, crates_in_layer, components)
30064
+ SELECT v_id, i.order_item_id, i.product_id, i.product_name, i.crates, i.boxes, i.weight_kg,
30065
+ i.plants_per_crate, COALESCE(i.crates_per_layer, 1), COALESCE(i.layers_per_cart, 1),
30066
+ COALESCE(i.layer_fraction, 0), COALESCE(i.layer_share_pct, 0), i.layer_index, i.crates_in_layer,
30067
+ CASE WHEN jsonb_typeof(i.components) = 'array' THEN i.components END
30068
+ FROM jsonb_to_recordset(COALESCE(v_cart->'items', '[]'::jsonb)) AS i(order_item_id uuid, product_id uuid, product_name text,
30069
+ crates integer, boxes integer, weight_kg numeric, plants_per_crate integer, crates_per_layer integer, layers_per_cart integer,
30070
+ layer_fraction numeric, layer_share_pct numeric, layer_index integer, crates_in_layer integer, components jsonb);
30071
+ v_items := v_items + jsonb_array_length(COALESCE(v_cart->'items', '[]'::jsonb));
30072
+ v_map := v_map || jsonb_build_object('planIndex', (v_cart->>'planIndex')::integer, 'id', v_id);
30073
+ END LOOP;
30074
+
30075
+ RETURN jsonb_build_object('karren', v_map, 'verwijderd', v_carts, 'printopdrachtenGeannuleerd', v_jobs, 'regels', v_items);
30076
+ END;
30077
+ $function$;
30078
+ REVOKE ALL ON FUNCTION public.logistics_apply_cart_swap(uuid[], jsonb) FROM PUBLIC, anon, authenticated;
30079
+ GRANT EXECUTE ON FUNCTION public.logistics_apply_cart_swap(uuid[], jsonb) TO service_role;
30080
+
30081
+ -- 1. Autofix schrijft alleen een volledige, positieve Floriday-verpakking weg (0 = onbekend).
30082
+ CREATE OR REPLACE FUNCTION public.logistics_packing_autofix(p_limit integer DEFAULT 100)
30083
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
30084
+ AS $function$
30085
+ DECLARE r record; v_reden text; v_aantal integer := 0; v_ppc int; v_cpl int; v_lpc int;
30086
+ BEGIN
30087
+ FOR r IN
30088
+ SELECT d.* FROM public.logistics_packing_deviations d
30089
+ WHERE d.resolved_at IS NULL AND d.scope = 'order_line' AND d.needs_human = false
30090
+ AND d.order_id IS NOT NULL AND d.order_item_id IS NOT NULL
30091
+ ORDER BY d.first_seen_at
30092
+ LIMIT GREATEST(1, LEAST(p_limit, 500))
30093
+ LOOP
30094
+ v_reden := public.logistics_packing_block_reason(r.order_id, r.order_item_id);
30095
+ IF v_reden IS NOT NULL THEN
30096
+ UPDATE public.logistics_packing_deviations SET needs_human = true, human_reason = v_reden WHERE id = r.id;
30097
+ CONTINUE;
30098
+ END IF;
30099
+ v_ppc := nullif(greatest(coalesce((r.floriday_packing->>'pieces_per_package')::numeric, 0), 0), 0)::int;
30100
+ v_cpl := nullif(greatest(coalesce((r.floriday_packing->>'packages_per_layer')::numeric, 0), 0), 0)::int;
30101
+ v_lpc := nullif(greatest(coalesce((r.floriday_packing->>'layers_per_load_carrier')::numeric, 0), 0), 0)::int;
30102
+ IF v_ppc IS NULL OR v_cpl IS NULL OR v_lpc IS NULL THEN
30103
+ UPDATE public.logistics_packing_deviations
30104
+ SET needs_human = true,
30105
+ human_reason = 'Floriday-verpakking onvolledig (0 of leeg = onbekend); niet automatisch overgenomen.'
30106
+ WHERE id = r.id;
30107
+ CONTINUE;
30108
+ END IF;
30109
+ UPDATE public.order_items oi
30110
+ SET plants_per_crate = v_ppc, crates_per_layer = v_cpl, layers_per_cart = v_lpc
30111
+ WHERE oi.id = r.order_item_id AND oi.packing_locked_at IS NULL;
30112
+ IF FOUND THEN
30113
+ v_aantal := v_aantal + 1;
30114
+ UPDATE public.logistics_packing_deviations
30115
+ SET auto_fixed_at = now(), resolved_at = now(), human_reason = 'automatisch bijgewerkt naar de Floriday-stand'
30116
+ WHERE id = r.id;
30117
+ PERFORM public.logistics_trace_event(r.order_id, 'stap:belading_synchronisatie', 'ok', 'belading bijgewerkt naar de Floriday-stand');
30118
+ END IF;
30119
+ END LOOP;
30120
+ RETURN v_aantal;
30121
+ END;
30122
+ $function$;
30123
+
30124
+ -- 9. Reconcile alleen bij een echte wijziging sinds de laatste afgeronde of geblokkeerde opdracht; kleinere porties.
30125
+ CREATE OR REPLACE FUNCTION public.logistics_reconcile_missing_plan_queue()
30126
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
30127
+ AS $function$
30128
+ DECLARE r record; n integer := 0;
30129
+ BEGIN
30130
+ FOR r IN
30131
+ SELECT o.id FROM public.orders o
30132
+ WHERE o.deleted_at IS NULL
30133
+ AND COALESCE(o.is_reservation,false) = false
30134
+ AND o.plan_locked_at IS NULL
30135
+ AND o.order_number IS NOT NULL
30136
+ AND (o.created_at >= now() - interval '14 days' OR o.updated_at >= now() - interval '14 days')
30137
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_tasks t WHERE t.order_ref = o.order_number)
30138
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_plan_queue q
30139
+ WHERE q.order_id = o.id AND q.status IN ('pending','running'))
30140
+ AND NOT EXISTS (
30141
+ SELECT 1 FROM public.logistics_plan_queue q
30142
+ WHERE q.order_id = o.id AND q.status IN ('geblokkeerd', 'done', 'opgelost')
30143
+ AND q.updated_at >= COALESCE(o.updated_at, o.created_at)
30144
+ AND NOT EXISTS (SELECT 1 FROM logistics_active.order_items oi WHERE oi.order_id = o.id AND oi.created_at > q.updated_at)
30145
+ AND NOT EXISTS (SELECT 1 FROM logistics_active.order_items oi JOIN public.products p ON p.id = oi.product_id
30146
+ WHERE oi.order_id = o.id AND p.updated_at > q.updated_at))
30147
+ ORDER BY o.created_at ASC
30148
+ LIMIT 100
30149
+ LOOP
30150
+ PERFORM public.logistics_plan_enqueue(r.id, 'reconcile');
30151
+ n := n + 1;
30152
+ END LOOP;
30153
+ RETURN n;
30154
+ END;
30155
+ $function$;
30156
+
30157
+ -- 9. Eén open opdracht per order (index alleen als er geen dubbele open opdrachten zijn).
30158
+ DO $q$ BEGIN
30159
+ IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'logistics_plan_queue_open_uniq') THEN
30160
+ IF EXISTS (SELECT 1 FROM public.logistics_plan_queue WHERE status IN ('pending','running','geblokkeerd')
30161
+ GROUP BY order_id HAVING count(*) > 1) THEN
30162
+ RAISE NOTICE 'logistiek-engine: dubbele open planopdrachten; unieke index wacht op samenvoegen (akkoord nodig)';
30163
+ ELSE
30164
+ CREATE UNIQUE INDEX logistics_plan_queue_open_uniq ON public.logistics_plan_queue (order_id)
30165
+ WHERE status IN ('pending','running','geblokkeerd');
30166
+ END IF;
30167
+ END IF;
30168
+ END $q$;
30169
+
30170
+ -- Alleen-lezen lijst: orders met meer dan één open planopdracht.
30171
+ CREATE OR REPLACE FUNCTION public.logistics_plan_queue_dubbel()
30172
+ RETURNS TABLE(order_id uuid, open_opdrachten bigint, oudste timestamptz, nieuwste timestamptz)
30173
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
30174
+ AS $function$
30175
+ SELECT q.order_id, count(*), min(q.enqueued_at), max(q.enqueued_at)
30176
+ FROM public.logistics_plan_queue q
30177
+ WHERE q.status IN ('pending','running','geblokkeerd')
30178
+ GROUP BY q.order_id HAVING count(*) > 1;
30179
+ $function$;
30180
+ REVOKE ALL ON FUNCTION public.logistics_plan_queue_dubbel() FROM PUBLIC, anon, authenticated;
30181
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_queue_dubbel() TO service_role;
30182
+
30183
+ -- 10. Buffer uit: elke order krijgt een eigen buffer (sleutel solo:<order-id>) en wordt dus per order ingedeeld.
30184
+ CREATE OR REPLACE FUNCTION public.logistics_apply_buffer_decision(p_order_id uuid)
30185
+ RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
30186
+ AS $function$
30187
+ DECLARE
30188
+ o public.orders%ROWTYPE;
30189
+ cf jsonb; d jsonb; v_ref text; v_close timestamptz; v_key text;
30190
+ v_buffer uuid; v_closed boolean; v_try int := 0; v_on_cart boolean; v_solo boolean := false;
30191
+ BEGIN
30192
+ SELECT * INTO o FROM public.orders WHERE id = p_order_id;
30193
+ IF NOT FOUND OR o.deleted_at IS NOT NULL THEN RETURN NULL; END IF;
30194
+ cf := COALESCE(o.custom_fields, '{}'::jsonb);
30195
+
30196
+ IF public.logistics_order_buffer_excluded(o.id) THEN
30197
+ IF o.buffer_id IS NOT NULL THEN
30198
+ SELECT EXISTS (SELECT 1 FROM public.order_items oi
30199
+ JOIN logistics_active.order_cart_items ci ON ci.order_item_id = oi.id
30200
+ JOIN logistics_active.order_carts oc ON oc.id = ci.cart_id
30201
+ WHERE oi.order_id = o.id AND oc.buffer_id = o.buffer_id)
30202
+ INTO v_on_cart;
30203
+ IF NOT v_on_cart THEN
30204
+ PERFORM set_config('logistics.buffer_writer', 'engine', true);
30205
+ UPDATE public.orders SET buffer_id = NULL WHERE id = o.id;
30206
+ BEGIN
30207
+ INSERT INTO public.logistics_planner_trace(order_id, message)
30208
+ VALUES (o.id, 'afgemeld; uit buffer gehaald');
30209
+ EXCEPTION WHEN others THEN NULL; END;
30210
+ END IF;
30211
+ END IF;
30212
+ RETURN NULL;
30213
+ END IF;
30214
+
30215
+ d := public.logistics_buffer_decision(public.logistics_buffer_payload(o.id), NULL);
30216
+
30217
+ IF (d ->> 'reason') = 'buffering_disabled' THEN
30218
+ v_solo := true;
30219
+ ELSIF COALESCE((d ->> 'buffer')::boolean, false) IS NOT TRUE OR (d ->> 'bufferRef') IS NULL THEN
30220
+ IF o.buffer_id IS NOT NULL THEN
30221
+ UPDATE public.order_carts SET buffer_id = NULL WHERE order_id = o.id;
30222
+ PERFORM set_config('logistics.buffer_writer', 'engine', true);
30223
+ UPDATE public.orders SET buffer_id = NULL WHERE id = o.id;
30224
+ END IF;
30225
+ DELETE FROM public.order_buffers b
30226
+ WHERE NOT EXISTS (SELECT 1 FROM public.orders x WHERE x.buffer_id = b.id)
30227
+ AND NOT EXISTS (SELECT 1 FROM public.order_carts oc WHERE oc.buffer_id = b.id);
30228
+ RETURN NULL;
30229
+ END IF;
30230
+
30231
+ IF v_solo THEN
30232
+ v_ref := 'solo:' || o.id::text;
30233
+ v_close := NULL;
30234
+ ELSE
30235
+ v_ref := d ->> 'bufferRef';
30236
+ BEGIN v_close := NULLIF(d ->> 'bufferCloseAt', '')::timestamptz;
30237
+ EXCEPTION WHEN others THEN v_close := NULL; END;
30238
+ END IF;
30239
+
30240
+ LOOP
30241
+ v_key := CASE WHEN v_try = 0 THEN v_ref ELSE v_ref || '#' || v_try END;
30242
+ SELECT id INTO v_buffer FROM public.order_buffers WHERE buffer_key = v_key;
30243
+ IF v_buffer IS NULL THEN
30244
+ INSERT INTO public.order_buffers (buffer_key, grouping_mode, pickup_date, pickup_location, customer_id, close_at)
30245
+ VALUES (v_key, CASE WHEN v_solo THEN 'solo' ELSE 'engine' END,
30246
+ COALESCE(cf ->> 'pickup_moment', cf ->> 'pickup_date', o.delivery_date::text),
30247
+ COALESCE(cf ->> 'delivery_location_gln', cf ->> 'delivery_location_id', cf ->> 'delivery_location'),
30248
+ o.customer_id, v_close)
30249
+ ON CONFLICT (buffer_key) DO UPDATE SET grouping_mode = EXCLUDED.grouping_mode
30250
+ RETURNING id INTO v_buffer;
30251
+ EXIT;
30252
+ END IF;
30253
+ IF o.buffer_id = v_buffer OR v_solo THEN EXIT; END IF;
30254
+
30255
+ v_closed := public.logistics_buffer_closed_for_join(v_buffer);
30256
+ IF NOT COALESCE(v_closed, false) THEN
30257
+ UPDATE public.order_buffers
30258
+ SET close_at = LEAST(COALESCE(close_at, v_close), COALESCE(v_close, close_at)), updated_at = now()
30259
+ WHERE id = v_buffer;
30260
+ EXIT;
30261
+ END IF;
30262
+ UPDATE public.order_buffers SET closed_at = COALESCE(closed_at, now()), updated_at = now() WHERE id = v_buffer;
30263
+ v_try := v_try + 1; v_buffer := NULL;
30264
+ IF v_try > 50 THEN RAISE EXCEPTION 'Kan geen open orderbuffer vinden voor sleutel %', v_ref; END IF;
30265
+ END LOOP;
30266
+
30267
+ IF o.buffer_id IS DISTINCT FROM v_buffer THEN
30268
+ PERFORM set_config('logistics.buffer_writer', 'engine', true);
30269
+ UPDATE public.orders SET buffer_id = v_buffer WHERE id = o.id;
30270
+ DELETE FROM public.order_buffers b
30271
+ WHERE NOT EXISTS (SELECT 1 FROM public.orders x WHERE x.buffer_id = b.id)
30272
+ AND NOT EXISTS (SELECT 1 FROM public.order_carts oc WHERE oc.buffer_id = b.id);
30273
+ END IF;
30274
+ RETURN v_buffer;
30275
+ END;
30276
+ $function$;
30277
+
30278
+ -- 13. Eigen traystickers: zolang bij één regel van de order het fustaantal onbekend is, wachten ALLE
30279
+ -- eigen traystickers van die order ("Fustaantal van regel X onbekend"), zodat x van y klopt.
30280
+ CREATE OR REPLACE FUNCTION public.logistics_own_traysticker_sync(p_limit integer DEFAULT 200)
30281
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
30282
+ AS $function$
30283
+ DECLARE
30284
+ s jsonb; v_tray text; v_rel text; v_station text;
30285
+ r record; v_bron text; v_keep text[] := '{}'; v_wacht_orders uuid[] := '{}';
30286
+ n int := 0; n_geblokkeerd int := 0; n_vrij int := 0; n_uit int := 0;
30287
+ BEGIN
30288
+ SELECT settings INTO s FROM public.logistics_rule_configs
30289
+ ORDER BY is_active DESC, (status = 'actief') DESC, updated_at DESC LIMIT 1;
30290
+ v_tray := public.logistics_rule_text(s, 'pickstations', 'traysticker_release', 'uit');
30291
+ IF v_tray = 'uit' THEN
30292
+ RETURN jsonb_build_object('trayRelease', v_tray, 'regels', 0);
30293
+ END IF;
30294
+ v_rel := public.logistics_rule_text(s, 'pickstations', 'sticker_release', 'with_pick_release');
30295
+ v_station := CASE WHEN v_rel = 'with_packing_slip'
30296
+ THEN public.logistics_rule_text(s, 'pickstations', 'packing_slip_station', '')
30297
+ ELSE public.logistics_rule_text(s, 'pickstations', 'sticker_station', '') END;
30298
+
30299
+ FOR r IN
30300
+ WITH regels AS (
30301
+ SELECT o.id AS order_id, o.order_number, ci.cart_id, ci.id AS ci_id, ci.crates, ci.created_at,
30302
+ coalesce(ci.product_name, oi.product_name, 'onbekend') AS regelnaam
30303
+ FROM public.orders o
30304
+ JOIN public.order_items oi ON oi.order_id = o.id
30305
+ JOIN public.order_cart_items ci ON ci.order_item_id = oi.id
30306
+ WHERE coalesce(o.order_source, '') <> 'floriday'
30307
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
30308
+ AND coalesce(o.status, '') <> 'geannuleerd'
30309
+ AND oi.quarantined_at IS NULL
30310
+ AND o.delivery_date >= current_date - 1
30311
+ )
30312
+ SELECT g.*,
30313
+ (SELECT min(x.regelnaam) FROM regels x WHERE x.order_id = g.order_id AND coalesce(x.crates, 0) <= 0) AS onbekende_regel
30314
+ FROM regels g
30315
+ ORDER BY g.order_number, g.cart_id, g.created_at, g.ci_id
30316
+ LIMIT greatest(1, p_limit)
30317
+ LOOP
30318
+ IF r.onbekende_regel IS NOT NULL THEN
30319
+ v_wacht_orders := v_wacht_orders || r.order_id;
30320
+ v_bron := r.order_id::text || ':fustaantal-onbekend';
30321
+ v_keep := v_keep || v_bron;
30322
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, status, reden)
30323
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, NULL, 1, 'geblokkeerd',
30324
+ 'Fustaantal van regel ' || r.onbekende_regel || ' onbekend. Alle traystickers van deze order wachten, zodat de nummering klopt.')
30325
+ ON CONFLICT DO NOTHING;
30326
+ n_geblokkeerd := n_geblokkeerd + 1;
30327
+ CONTINUE;
30328
+ END IF;
30329
+ FOR k IN 1 .. r.crates LOOP
30330
+ v_bron := r.order_id::text || ':' || r.cart_id::text || ':' || r.ci_id::text || ':' || k::text;
30331
+ v_keep := v_keep || v_bron;
30332
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, print_plek_id, status, reden)
30333
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, r.cart_id, 1,
30334
+ public.logistics_sticker_cart_plek(r.cart_id, v_station), 'wacht_op_moment', 'Wacht op de pakbon van deze kar.')
30335
+ ON CONFLICT DO NOTHING;
30336
+ -- Was hij eerder tegengehouden door een onbekend fustaantal: weer laten wachten op het moment.
30337
+ UPDATE public.logistics_sticker_prints
30338
+ SET status = 'wacht_op_moment', reden = 'Wacht op de pakbon van deze kar.', updated_at = now()
30339
+ WHERE soort = 'eigen_traysticker' AND bron_id = v_bron AND status = 'geblokkeerd'
30340
+ AND reden LIKE 'Fustaantal van regel %';
30341
+ n := n + 1;
30342
+ END LOOP;
30343
+ END LOOP;
30344
+
30345
+ -- Order met onbekend fustaantal: alle nog niet geprinte stickers wachten (niet vervallen).
30346
+ UPDATE public.logistics_sticker_prints p
30347
+ SET status = 'geblokkeerd',
30348
+ reden = (SELECT b.reden FROM public.logistics_sticker_prints b
30349
+ WHERE b.soort = 'eigen_traysticker' AND b.bron_id = p.order_id::text || ':fustaantal-onbekend' LIMIT 1),
30350
+ updated_at = now()
30351
+ WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'klaar')
30352
+ AND p.order_id = ANY (v_wacht_orders);
30353
+ v_keep := v_keep || ARRAY(SELECT p.bron_id FROM public.logistics_sticker_prints p
30354
+ WHERE p.soort = 'eigen_traysticker' AND p.order_id = ANY (v_wacht_orders)
30355
+ AND p.reden LIKE 'Fustaantal van regel %');
30356
+
30357
+ UPDATE public.logistics_sticker_prints p
30358
+ SET status = 'niet_printen', reden = 'Fust hoort niet meer bij deze kar of order vervallen.', updated_at = now()
30359
+ WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
30360
+ AND NOT (p.bron_id = ANY (v_keep))
30361
+ AND p.order_id IN (SELECT o.id FROM public.orders o WHERE o.delivery_date >= current_date - 1);
30362
+ GET DIAGNOSTICS n_uit = ROW_COUNT;
30363
+
30364
+ UPDATE public.logistics_sticker_prints p
30365
+ SET status = 'klaar', reden = NULL,
30366
+ print_plek_id = coalesce(p.print_plek_id, public.logistics_sticker_cart_plek(p.cart_id, v_station)),
30367
+ updated_at = now()
30368
+ WHERE p.soort = 'eigen_traysticker' AND p.status = 'wacht_op_moment'
30369
+ AND p.cart_id IS NOT NULL AND public.logistics_cart_slip_queued(p.cart_id);
30370
+ GET DIAGNOSTICS n_vrij = ROW_COUNT;
30371
+
30372
+ RETURN jsonb_build_object('trayRelease', v_tray, 'fusten', n, 'geblokkeerd', n_geblokkeerd, 'vrijgegeven', n_vrij, 'niet_printen', n_uit);
30373
+ END; $function$;
30374
+ REVOKE ALL ON FUNCTION public.logistics_own_traysticker_sync(integer) FROM PUBLIC, anon, authenticated;
30375
+ GRANT EXECUTE ON FUNCTION public.logistics_own_traysticker_sync(integer) TO service_role;