@flowselections/logistiek-engine 1.0.193 → 1.0.199

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 (59) hide show
  1. package/db/schema/logistiek-engine.sql +2782 -13
  2. package/dist-lib/components/BeheerPage.d.ts.map +1 -1
  3. package/dist-lib/components/BeheerPage.js +3 -1
  4. package/dist-lib/components/DataGereedheidCard.d.ts +47 -0
  5. package/dist-lib/components/DataGereedheidCard.d.ts.map +1 -0
  6. package/dist-lib/components/DataGereedheidCard.js +133 -0
  7. package/dist-lib/components/GeblokkeerdeOrdersCard.d.ts.map +1 -1
  8. package/dist-lib/components/GeblokkeerdeOrdersCard.js +3 -1
  9. package/dist-lib/components/GeblokkeerdeStickersCard.d.ts.map +1 -1
  10. package/dist-lib/components/GeblokkeerdeStickersCard.js +11 -1
  11. package/dist-lib/components/LadingdragerVoorstellenCard.d.ts +4 -0
  12. package/dist-lib/components/LadingdragerVoorstellenCard.d.ts.map +1 -0
  13. package/dist-lib/components/LadingdragerVoorstellenCard.js +44 -0
  14. package/dist-lib/components/LadingdragersPage.d.ts.map +1 -1
  15. package/dist-lib/components/LadingdragersPage.js +2 -1
  16. package/dist-lib/components/StartlijstCard.d.ts +2 -0
  17. package/dist-lib/components/StartlijstCard.d.ts.map +1 -0
  18. package/dist-lib/components/StartlijstCard.js +54 -0
  19. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts.map +1 -1
  20. package/dist-lib/hooks/useGeblokkeerdeOrders.js +2 -0
  21. package/dist-lib/index.d.ts +3 -0
  22. package/dist-lib/index.d.ts.map +1 -1
  23. package/dist-lib/index.js +3 -0
  24. package/dist-lib/integrations/supabase/types.d.ts +681 -121
  25. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  26. package/dist-lib/integrations/supabase/types.js +1 -0
  27. package/dist-lib/lib/beladingRegels.d.ts +22 -0
  28. package/dist-lib/lib/beladingRegels.d.ts.map +1 -1
  29. package/dist-lib/lib/beladingRegels.js +46 -4
  30. package/dist-lib/lib/dimensionData.server.js +15 -12
  31. package/dist-lib/lib/dimensionPlanner.d.ts +11 -0
  32. package/dist-lib/lib/dimensionPlanner.d.ts.map +1 -1
  33. package/dist-lib/lib/dimensionPlanner.js +30 -2
  34. package/dist-lib/lib/handlers/index.d.ts +1 -0
  35. package/dist-lib/lib/handlers/index.d.ts.map +1 -1
  36. package/dist-lib/lib/handlers/index.js +1 -0
  37. package/dist-lib/lib/handlers/logistics-plan-now.server.d.ts +18 -0
  38. package/dist-lib/lib/handlers/logistics-plan-now.server.d.ts.map +1 -0
  39. package/dist-lib/lib/handlers/logistics-plan-now.server.js +113 -0
  40. package/dist-lib/lib/handlers/logistics-tick.server.d.ts +5 -0
  41. package/dist-lib/lib/handlers/logistics-tick.server.d.ts.map +1 -1
  42. package/dist-lib/lib/handlers/logistics-tick.server.js +65 -26
  43. package/dist-lib/lib/invoerTeksten.d.ts +4 -0
  44. package/dist-lib/lib/invoerTeksten.d.ts.map +1 -0
  45. package/dist-lib/lib/invoerTeksten.js +19 -0
  46. package/dist-lib/lib/layoutPrint.server.d.ts +8 -0
  47. package/dist-lib/lib/layoutPrint.server.d.ts.map +1 -1
  48. package/dist-lib/lib/layoutPrint.server.js +32 -0
  49. package/dist-lib/lib/loadCarrierPlanning.server.d.ts +4 -0
  50. package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
  51. package/dist-lib/lib/loadCarrierPlanning.server.js +209 -12
  52. package/dist-lib/lib/moduleVersion.d.ts +1 -1
  53. package/dist-lib/lib/moduleVersion.js +1 -1
  54. package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
  55. package/dist-lib/lib/ruleEngineSchema.js +14 -0
  56. package/dist-lib/lib/stickerQueue.server.d.ts +18 -0
  57. package/dist-lib/lib/stickerQueue.server.d.ts.map +1 -1
  58. package/dist-lib/lib/stickerQueue.server.js +63 -20
  59. package/package.json +11 -1
@@ -13,6 +13,25 @@
13
13
  -- Gegenereerd uit de database; niet met de hand bewerken.
14
14
  -- ============================================================================
15
15
 
16
+ -- 1.0.196: vroege definitie van de toegangscontrole, zodat SQL-functies verderop
17
+ -- bij een eerste installatie (lege database) al kunnen verwijzen. Identiek aan de
18
+ -- definitie verderop; die blijft leidend.
19
+ CREATE OR REPLACE FUNCTION public.logistics_acc_guard(p_level text, p_fn text)
20
+ RETURNS void LANGUAGE plpgsql STABLE SET search_path TO 'public'
21
+ AS $function$
22
+ BEGIN
23
+ IF public.logistics_acc_is_server() THEN RETURN; END IF;
24
+ IF pg_trigger_depth() > 0 THEN RETURN; END IF;
25
+ IF NOT public.logistics_acc_direct(p_fn) THEN RETURN; END IF;
26
+ IF p_level = 'ingelogd' AND auth.uid() IS NOT NULL THEN RETURN; END IF;
27
+ IF p_level = 'beheer' AND auth.uid() IS NOT NULL AND public.logistics_is_beheer() THEN RETURN; END IF;
28
+ RAISE EXCEPTION '%', CASE p_level
29
+ WHEN 'server' THEN 'Geen toegang: ' || p_fn || ' is alleen voor de server.'
30
+ WHEN 'beheer' THEN 'Geen toegang: ' || p_fn || ' is alleen voor beheer.'
31
+ ELSE 'Geen toegang: log in om ' || p_fn || ' te gebruiken.' END
32
+ USING ERRCODE = '42501';
33
+ END $function$;
34
+
16
35
  SET check_function_bodies = off;
17
36
 
18
37
  DO $fs$ BEGIN
@@ -26902,7 +26921,7 @@ BEGIN
26902
26921
  AND coalesce(o.order_source, '') <> 'floriday'
26903
26922
  AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
26904
26923
  AND coalesce(o.status, '') <> 'geannuleerd'
26905
- AND o.delivery_date >= current_date - 1
26924
+ AND public.logistics_order_date(o.delivery_date) >= current_date - 1
26906
26925
  GROUP BY o.id, o.order_number, o.customer_id, oi.product_id
26907
26926
  LIMIT greatest(1, p_limit)
26908
26927
  LOOP
@@ -26989,14 +27008,12 @@ $function$;
26989
27008
  -- 6. Pakbon van het pickstation: alleen de centrale beslisregel (geen layout uit payload of werkwijze).
26990
27009
  REVOKE EXECUTE ON FUNCTION public.logistics_own_sticker_sync(integer) FROM PUBLIC, anon, authenticated;
26991
27010
  GRANT EXECUTE ON FUNCTION public.logistics_own_sticker_sync(integer) TO service_role;
26992
- REVOKE EXECUTE ON FUNCTION public.logistics_document_for_order(uuid, text) FROM PUBLIC, anon;
26993
- GRANT EXECUTE ON FUNCTION public.logistics_document_for_order(uuid, text) TO authenticated, service_role;
27011
+ -- 1.0.197: rechten van logistics_document_for_order staan direct na de definitie (verderop).
26994
27012
  REVOKE EXECUTE ON FUNCTION public.logistics_document_selection_status() FROM PUBLIC, anon;
26995
27013
  GRANT EXECUTE ON FUNCTION public.logistics_document_selection_status() TO authenticated, service_role;
26996
27014
 
26997
27015
  -- Veiligheid: draaien met de rechten van de aanroeper.
26998
27016
  ALTER FUNCTION public.logistics_document_selection_status() SECURITY INVOKER;
26999
- ALTER FUNCTION public.logistics_document_for_order(uuid, text) SECURITY INVOKER;
27000
27017
 
27001
27018
  -- ===== 1.0.176 reserveringen FO + pakbon centrale keuze =====
27002
27019
  -- 1.0.176: reserveringen (Voorraad 1.0.93) en pakbon via centrale documentkeuze (Pickschermen 1.0.56).
@@ -27813,13 +27830,19 @@ BEGIN
27813
27830
  'documenttype', v_type, 'breedte_mm', v_l.breedte_mm, 'hoogte_mm', v_l.hoogte_mm, 'reden', NULL) || v_extra;
27814
27831
  END;
27815
27832
  $function$;
27833
+ -- 1.0.197: rechten na de definitie (een nieuwe Shell kent de functie hiervoor nog niet).
27834
+ REVOKE EXECUTE ON FUNCTION public.logistics_document_for_order(uuid, text) FROM PUBLIC, anon;
27835
+ GRANT EXECUTE ON FUNCTION public.logistics_document_for_order(uuid, text) TO authenticated, service_role;
27836
+ ALTER FUNCTION public.logistics_document_for_order(uuid, text) SECURITY INVOKER;
27816
27837
 
27817
27838
 
27818
27839
  -- ===== 1.0.178: fusten en land per kar, eigen traystickers =====
27819
27840
  -- 1.0.178 A: fusten en land per kar in control_get_todays_carts (achteraan toegevoegd)
27820
27841
  DO $m$ BEGIN
27821
- IF pg_get_function_result('public.control_get_todays_carts()'::regprocedure) NOT LIKE '%crates_on_cart%' THEN
27822
- DROP FUNCTION public.control_get_todays_carts();
27842
+ IF to_regprocedure('public.control_get_todays_carts()') IS NOT NULL THEN
27843
+ IF pg_get_function_result('public.control_get_todays_carts()'::regprocedure) NOT LIKE '%crates_on_cart%' THEN
27844
+ DROP FUNCTION public.control_get_todays_carts();
27845
+ END IF;
27823
27846
  END IF;
27824
27847
  END $m$;
27825
27848
 
@@ -28396,7 +28419,7 @@ BEGIN
28396
28419
  AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
28397
28420
  AND coalesce(o.status, '') <> 'geannuleerd'
28398
28421
  AND oi.quarantined_at IS NULL
28399
- AND o.delivery_date >= current_date - 1
28422
+ AND public.logistics_order_date(o.delivery_date) >= current_date - 1
28400
28423
  )
28401
28424
  SELECT g.*,
28402
28425
  (SELECT min(x.regelnaam) FROM regels x WHERE x.order_id = g.order_id AND coalesce(x.crates, 0) <= 0) AS onbekende_regel
@@ -28445,7 +28468,7 @@ BEGIN
28445
28468
  SET status = 'niet_printen', reden = 'Fust hoort niet meer bij deze kar of order vervallen.', updated_at = now()
28446
28469
  WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
28447
28470
  AND NOT (p.bron_id = ANY (v_keep))
28448
- AND p.order_id IN (SELECT o.id FROM public.orders o WHERE o.delivery_date >= current_date - 1);
28471
+ AND p.order_id IN (SELECT o.id FROM public.orders o WHERE public.logistics_order_date(o.delivery_date) >= current_date - 1);
28449
28472
  GET DIAGNOSTICS n_uit = ROW_COUNT;
28450
28473
 
28451
28474
  UPDATE public.logistics_sticker_prints p
@@ -28679,7 +28702,7 @@ BEGIN
28679
28702
  FROM public.orders o
28680
28703
  JOIN public.floriday_salesorders_cache s ON s.floriday_id = o.floriday_sales_order_id AND NOT coalesce(s.is_deleted,false)
28681
28704
  CROSS JOIN LATERAL jsonb_array_elements(CASE WHEN jsonb_typeof(s.data->'additionalServices') = 'array' THEN s.data->'additionalServices' ELSE '[]'::jsonb END) e
28682
- WHERE o.deleted_at IS NULL AND o.delivery_date >= current_date - 2
28705
+ WHERE o.deleted_at IS NULL AND public.logistics_order_date(o.delivery_date) >= current_date - 2
28683
28706
  AND upper(coalesce(e->>'unit','')) = 'LABEL' AND (e->>'numberOfUnits') ~ '^\d+$'
28684
28707
  AND nullif(btrim(coalesce(e->>'stickerId','')),'') IS NOT NULL
28685
28708
  AND NOT EXISTS (SELECT 1 FROM public.floriday_customer_stickers_cache c WHERE c.floriday_id = e->>'stickerId')
@@ -28713,7 +28736,7 @@ BEGIN
28713
28736
  SELECT o.id, o.order_number, nullif(btrim(o.floriday_sales_order_id),'') AS sales
28714
28737
  FROM public.orders o
28715
28738
  WHERE o.deleted_at IS NULL AND nullif(btrim(o.floriday_sales_order_id),'') IS NOT NULL
28716
- AND o.delivery_date >= current_date - 2
28739
+ AND public.logistics_order_date(o.delivery_date) >= current_date - 2
28717
28740
  AND EXISTS (SELECT 1 FROM logistics_active.order_carts oc WHERE oc.order_id = o.id)
28718
28741
  ORDER BY o.delivery_date LIMIT greatest(p_limit,1)
28719
28742
  LOOP
@@ -29987,6 +30010,8 @@ DO $fs$ DECLARE f text; BEGIN
29987
30010
  'logistics_set_order_carrier(uuid,uuid)','logistics_carrier_status()','logistics_carrier_set_backfill(boolean)',
29988
30011
  'logistics_carrier_preview_existing(integer)','logistics_carrier_promote_oud(uuid[],integer)',
29989
30012
  'logistics_order_carrier_label(uuid)','logistics_install_check()','logistics_order_departure(uuid)','logistics_resolve_carrier_for_location_many(jsonb)'] LOOP
30013
+ -- 1.0.197: in een nieuwe Shell kan een functie hier nog ontbreken (verderop gedefinieerd, eigen rechten daar).
30014
+ CONTINUE WHEN to_regprocedure('public.' || f) IS NULL;
29990
30015
  EXECUTE format('REVOKE ALL ON FUNCTION public.%s FROM PUBLIC, anon', f);
29991
30016
  EXECUTE format('GRANT EXECUTE ON FUNCTION public.%s TO authenticated, service_role', f);
29992
30017
  END LOOP;
@@ -30005,9 +30030,10 @@ END $fs$;
30005
30030
  -- 1.0.184: karrenlijst toont de vervoerder uit orders.carrier_id (achteraan toegevoegd).
30006
30031
  -- "Eigen vervoer" en "Afhalen" op carrier_kind / self_pickup, nooit op naam.
30007
30032
  DO $m$ BEGIN
30008
- IF to_regprocedure('public.control_get_todays_carts()') IS NOT NULL
30009
- AND pg_get_function_result('public.control_get_todays_carts()'::regprocedure) NOT LIKE '%carrier_name%' THEN
30010
- DROP FUNCTION public.control_get_todays_carts();
30033
+ IF to_regprocedure('public.control_get_todays_carts()') IS NOT NULL THEN
30034
+ IF pg_get_function_result('public.control_get_todays_carts()'::regprocedure) NOT LIKE '%carrier_name%' THEN
30035
+ DROP FUNCTION public.control_get_todays_carts();
30036
+ END IF;
30011
30037
  END IF;
30012
30038
  END $m$;
30013
30039
 
@@ -31832,3 +31858,2746 @@ BEGIN
31832
31858
  END $$;
31833
31859
  REVOKE ALL ON FUNCTION public.logistics_schedule_tasks_batch(jsonb) FROM PUBLIC, anon;
31834
31860
  GRANT EXECUTE ON FUNCTION public.logistics_schedule_tasks_batch(jsonb) TO authenticated, service_role;
31861
+
31862
+ -- ===== 1.0.194: stickers, direct indelen, lichte ronde, printstatus (db/migrations/20261026110000) =====
31863
+ -- Logistiek Engine 1.0.194: stickers werken weer, direct indelen, lichte minuutronde, printstatus volgt print_jobs.
31864
+ -- Additief: CREATE OR REPLACE / IF NOT EXISTS; triggers via DO-blok met bestaan-check; geen REVOKE/DROP/cron.schedule.
31865
+ SET check_function_bodies = off;
31866
+
31867
+ -- A1: leverdatum (tekst) veilig als datum lezen. NULL als het geen geldige YYYY-MM-DD is.
31868
+ CREATE OR REPLACE FUNCTION public.logistics_order_date(p text)
31869
+ RETURNS date LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE SET search_path TO 'public' AS $$
31870
+ BEGIN
31871
+ IF p IS NULL OR p !~ '^\d{4}-\d{2}-\d{2}' THEN RETURN NULL; END IF;
31872
+ RETURN left(p, 10)::date;
31873
+ EXCEPTION WHEN OTHERS THEN
31874
+ RETURN NULL;
31875
+ END $$;
31876
+ CREATE OR REPLACE FUNCTION public.logistics_order_date(p date)
31877
+ RETURNS date LANGUAGE sql IMMUTABLE PARALLEL SAFE SET search_path TO 'public' AS $$ SELECT p $$;
31878
+
31879
+ -- B7: is de order geannuleerd? Werkt ook als cancelled_at/status in deze shell ontbreken.
31880
+ CREATE OR REPLACE FUNCTION public.logistics_order_geannuleerd(p_order_id uuid)
31881
+ RETURNS boolean LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31882
+ DECLARE j jsonb;
31883
+ BEGIN
31884
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_geannuleerd');
31885
+ SELECT to_jsonb(o) INTO j FROM public.orders o WHERE o.id = p_order_id;
31886
+ IF j IS NULL THEN RETURN false; END IF;
31887
+ RETURN (j->>'cancelled_at') IS NOT NULL
31888
+ OR lower(coalesce(j->>'status', '')) IN ('cancelled', 'canceled', 'geannuleerd');
31889
+ END $$;
31890
+
31891
+ -- Toestand per minuutronde (watermerken) en vastlegging per wek-aanroep.
31892
+ CREATE TABLE IF NOT EXISTS public.logistics_tick_state (
31893
+ key text PRIMARY KEY,
31894
+ value jsonb NOT NULL DEFAULT '{}'::jsonb,
31895
+ updated_at timestamptz NOT NULL DEFAULT now()
31896
+ );
31897
+ GRANT SELECT ON public.logistics_tick_state TO authenticated;
31898
+ GRANT ALL ON public.logistics_tick_state TO service_role;
31899
+ ALTER TABLE public.logistics_tick_state ENABLE ROW LEVEL SECURITY;
31900
+ DO $p$ BEGIN
31901
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='logistics_tick_state' AND policyname='Beheer leest rondetoestand') THEN
31902
+ CREATE POLICY "Beheer leest rondetoestand" ON public.logistics_tick_state FOR SELECT TO authenticated USING (public.logistics_is_beheer());
31903
+ END IF;
31904
+ END $p$;
31905
+
31906
+ CREATE TABLE IF NOT EXISTS public.logistics_kick_log (
31907
+ id bigserial PRIMARY KEY,
31908
+ kicked_at timestamptz NOT NULL DEFAULT clock_timestamp(),
31909
+ request_id bigint,
31910
+ skipped_reason text,
31911
+ round_started_at timestamptz
31912
+ );
31913
+ CREATE INDEX IF NOT EXISTS logistics_kick_log_kicked_at_idx ON public.logistics_kick_log (kicked_at DESC);
31914
+ GRANT SELECT ON public.logistics_kick_log TO authenticated;
31915
+ GRANT ALL ON public.logistics_kick_log TO service_role;
31916
+ GRANT USAGE, SELECT ON SEQUENCE public.logistics_kick_log_id_seq TO service_role;
31917
+ ALTER TABLE public.logistics_kick_log ENABLE ROW LEVEL SECURITY;
31918
+ DO $p$ BEGIN
31919
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='logistics_kick_log' AND policyname='Beheer leest wek-aanroepen') THEN
31920
+ CREATE POLICY "Beheer leest wek-aanroepen" ON public.logistics_kick_log FOR SELECT TO authenticated USING (public.logistics_is_beheer());
31921
+ END IF;
31922
+ END $p$;
31923
+
31924
+ -- B2: time-out van de aanroep naar de Shell. Standaard 30000 ms; ongeldige waarde = luide fout.
31925
+ CREATE OR REPLACE FUNCTION public.logistics_tick_timeout_ms()
31926
+ RETURNS integer LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31927
+ DECLARE v text; n int;
31928
+ BEGIN
31929
+ PERFORM public.logistics_acc_guard('server', 'logistics_tick_timeout_ms');
31930
+ SELECT NULLIF(btrim(value), '') INTO v FROM public.module_runtime_config WHERE key = 'logistics_tick_timeout_ms';
31931
+ IF v IS NULL THEN RETURN 30000; END IF;
31932
+ IF v !~ '^\d{3,6}$' THEN
31933
+ RAISE EXCEPTION 'logistics_tick_timeout_ms in module_runtime_config is ongeldig (%); verwacht een getal in milliseconden', v;
31934
+ END IF;
31935
+ n := v::int;
31936
+ IF n < 1000 OR n > 120000 THEN
31937
+ RAISE EXCEPTION 'logistics_tick_timeout_ms (%) valt buiten 1000–120000 ms', n;
31938
+ END IF;
31939
+ RETURN n;
31940
+ END $$;
31941
+
31942
+ -- Instellingen aanvullen (idempotent). Bestaande waarden blijven, behalve de oude claim-timeout 10.
31943
+ CREATE OR REPLACE FUNCTION public.logistics_ensure_tick_settings()
31944
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31945
+ DECLARE v_t int := 0; v_c int := 0;
31946
+ BEGIN
31947
+ PERFORM public.logistics_acc_guard('server', 'logistics_ensure_tick_settings');
31948
+ INSERT INTO public.module_runtime_config (key, value, updated_at)
31949
+ VALUES ('logistics_tick_timeout_ms', '30000', now())
31950
+ ON CONFLICT (key) DO NOTHING;
31951
+ GET DIAGNOSTICS v_t = ROW_COUNT;
31952
+ INSERT INTO public.logistics_plan_settings (key, value, updated_at)
31953
+ VALUES ('claim_timeout_minutes', '2', now())
31954
+ ON CONFLICT (key) DO UPDATE SET value = '2', updated_at = now()
31955
+ WHERE NULLIF(btrim(public.logistics_plan_settings.value), '') IS NULL
31956
+ OR btrim(public.logistics_plan_settings.value) = '10';
31957
+ GET DIAGNOSTICS v_c = ROW_COUNT;
31958
+ RETURN jsonb_build_object('timeoutIngesteld', v_t > 0, 'claimTimeoutBijgewerkt', v_c > 0);
31959
+ END $$;
31960
+
31961
+ -- B5: periodieke ensure-stappen hooguit 1× per uur.
31962
+ CREATE OR REPLACE FUNCTION public.logistics_ensure_periodiek()
31963
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31964
+ DECLARE v_last timestamptz; v_r jsonb := '{}'::jsonb;
31965
+ BEGIN
31966
+ PERFORM public.logistics_acc_guard('server', 'logistics_ensure_periodiek');
31967
+ SELECT (value->>'at')::timestamptz INTO v_last FROM public.logistics_tick_state WHERE key = 'ensure_periodiek';
31968
+ IF v_last IS NOT NULL AND v_last > now() - interval '1 hour' THEN
31969
+ RETURN jsonb_build_object('overgeslagen', 'binnen het uur', 'laatst', v_last);
31970
+ END IF;
31971
+ v_r := v_r || jsonb_build_object('rebuffer', public.logistics_ensure_rebuffer_setting());
31972
+ v_r := v_r || jsonb_build_object('tick', public.logistics_ensure_tick_settings());
31973
+ INSERT INTO public.logistics_tick_state(key, value, updated_at)
31974
+ VALUES ('ensure_periodiek', jsonb_build_object('at', now()) || v_r, now())
31975
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
31976
+ RETURN v_r;
31977
+ END $$;
31978
+
31979
+ -- B3: laadtoestand alleen bij wijziging in planwachtrij/taken, anders hooguit elke 5 minuten.
31980
+ CREATE OR REPLACE FUNCTION public.logistics_refresh_plan_state_als_nodig()
31981
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31982
+ DECLARE v_wm timestamptz; v_prev timestamptz; v_at timestamptz; v_res jsonb;
31983
+ BEGIN
31984
+ PERFORM public.logistics_acc_guard('server', 'logistics_refresh_plan_state_als_nodig');
31985
+ SELECT greatest((SELECT max(updated_at) FROM public.logistics_plan_queue),
31986
+ (SELECT max(updated_at) FROM public.logistics_tasks)) INTO v_wm;
31987
+ SELECT (value->>'wm')::timestamptz, (value->>'at')::timestamptz INTO v_prev, v_at
31988
+ FROM public.logistics_tick_state WHERE key = 'refresh_plan_state';
31989
+ IF v_at IS NOT NULL AND v_wm IS NOT DISTINCT FROM v_prev AND v_at > now() - interval '5 minutes' THEN
31990
+ RETURN jsonb_build_object('overgeslagen', 'geen wijziging');
31991
+ END IF;
31992
+ v_res := public.logistics_refresh_plan_state(p_limit => 500);
31993
+ INSERT INTO public.logistics_tick_state(key, value, updated_at)
31994
+ VALUES ('refresh_plan_state', jsonb_build_object('wm', v_wm, 'at', now()), now())
31995
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
31996
+ RETURN v_res;
31997
+ END $$;
31998
+
31999
+ -- B1: een wek-aanroep tijdens een lopende ronde oppakken vóór het slot vrijgaat.
32000
+ CREATE OR REPLACE FUNCTION public.logistics_kick_pending_take(p_since timestamptz)
32001
+ RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
32002
+ DECLARE v_val timestamptz;
32003
+ BEGIN
32004
+ PERFORM public.logistics_acc_guard('server', 'logistics_kick_pending_take');
32005
+ SELECT NULLIF(s.value, '')::timestamptz INTO v_val FROM public.logistics_plan_settings s
32006
+ WHERE s.key = 'kick_pending' FOR UPDATE;
32007
+ IF v_val IS NULL THEN RETURN false; END IF;
32008
+ UPDATE public.logistics_plan_settings s SET value = '', updated_at = now() WHERE s.key = 'kick_pending';
32009
+ RETURN v_val IS NOT NULL AND v_val >= p_since;
32010
+ END $$;
32011
+
32012
+ -- B0: start van de ronde koppelen aan de wek-aanroepen die erop wachtten.
32013
+ CREATE OR REPLACE FUNCTION public.logistics_kick_round_start()
32014
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
32015
+ DECLARE n int;
32016
+ BEGIN
32017
+ PERFORM public.logistics_acc_guard('server', 'logistics_kick_round_start');
32018
+ UPDATE public.logistics_kick_log SET round_started_at = clock_timestamp()
32019
+ WHERE round_started_at IS NULL AND kicked_at > now() - interval '2 minutes';
32020
+ GET DIAGNOSTICS n = ROW_COUNT;
32021
+ RETURN n;
32022
+ END $$;
32023
+
32024
+ -- B0: meting per wek-aanroep: tijd wek, verzoek-id, antwoord van pg_net, start ronde.
32025
+ CREATE OR REPLACE FUNCTION public.logistics_kick_metingen(p_limit integer)
32026
+ RETURNS TABLE(kicked_at timestamptz, request_id bigint, skipped_reason text, antwoord_at timestamptz,
32027
+ status_code integer, time_out boolean, fout text, round_started_at timestamptz,
32028
+ wacht_tot_antwoord_ms integer, wacht_tot_ronde_ms integer)
32029
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
32030
+ BEGIN
32031
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_kick_metingen');
32032
+ IF to_regclass('net._http_response') IS NULL THEN
32033
+ RETURN QUERY SELECT k.kicked_at, k.request_id, k.skipped_reason, NULL::timestamptz, NULL::int, NULL::boolean,
32034
+ NULL::text, k.round_started_at, NULL::int,
32035
+ (extract(epoch FROM k.round_started_at - k.kicked_at) * 1000)::int
32036
+ FROM public.logistics_kick_log k ORDER BY k.kicked_at DESC LIMIT least(greatest(coalesce(p_limit, 50), 1), 500);
32037
+ RETURN;
32038
+ END IF;
32039
+ RETURN QUERY EXECUTE $q$
32040
+ SELECT k.kicked_at, k.request_id, k.skipped_reason, r.created, r.status_code, r.timed_out,
32041
+ left(r.error_msg, 200), k.round_started_at,
32042
+ (extract(epoch FROM r.created - k.kicked_at) * 1000)::int,
32043
+ (extract(epoch FROM k.round_started_at - k.kicked_at) * 1000)::int
32044
+ FROM public.logistics_kick_log k
32045
+ LEFT JOIN net._http_response r ON r.id = k.request_id
32046
+ ORDER BY k.kicked_at DESC LIMIT $1$q$ USING least(greatest(coalesce(p_limit, 50), 1), 500);
32047
+ END $$;
32048
+
32049
+ -- A3: uitkomst van de HTTP-ronde (stickers) vastleggen in logistics_tick_runs.steps en rondetoestand.
32050
+ CREATE OR REPLACE FUNCTION public.logistics_tick_http_log(p_steps jsonb)
32051
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
32052
+ DECLARE v_fouten jsonb := coalesce(p_steps->'stickerSyncFouten', '[]'::jsonb);
32053
+ BEGIN
32054
+ PERFORM public.logistics_acc_guard('server', 'logistics_tick_http_log');
32055
+ INSERT INTO public.logistics_tick_runs(started_at, finished_at, duration_ms, has_error, steps)
32056
+ VALUES (coalesce((p_steps->>'gestart')::timestamptz, now()), now(),
32057
+ nullif(p_steps->>'duurMs', '')::int,
32058
+ jsonb_typeof(v_fouten) = 'array' AND jsonb_array_length(v_fouten) > 0,
32059
+ jsonb_build_object('bron', 'http') || p_steps);
32060
+ INSERT INTO public.logistics_tick_state(key, value, updated_at)
32061
+ VALUES ('sticker_sync', jsonb_build_object('at', now(), 'fouten', v_fouten), now())
32062
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
32063
+ END $$;
32064
+
32065
+ -- C3: loopt er een (niet verlopen) pakbonopdracht voor deze order? Zelfde filter als logistics_cart_is_frozen.
32066
+ CREATE OR REPLACE FUNCTION public.logistics_order_print_actief(p_order_number text)
32067
+ RETURNS boolean LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
32068
+ DECLARE v boolean;
32069
+ BEGIN
32070
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_print_actief');
32071
+ IF p_order_number IS NULL OR to_regclass('public.print_jobs') IS NULL THEN RETURN false; END IF;
32072
+ EXECUTE $q$
32073
+ SELECT EXISTS (
32074
+ SELECT 1 FROM public.print_jobs pj
32075
+ WHERE pj.documenttype::text = 'pakbon'
32076
+ AND COALESCE(pj.is_herprint, false) = false
32077
+ AND pj.status::text IN ('wachtend', 'bezig', 'voltooid')
32078
+ AND COALESCE(pj.foutcode, '') NOT IN ('verlopen', 'expired')
32079
+ AND COALESCE(pj.verwerkingsfase, '') NOT IN ('verlopen', 'expired')
32080
+ AND (EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.print_job_id = pj.id AND q.order_number = $1 AND q.cancelled_at IS NULL)
32081
+ OR pj.payload->>'cart_id' IN (SELECT oc.id::text FROM logistics_active.order_carts oc
32082
+ JOIN public.orders o ON o.id = oc.order_id WHERE o.order_number = $1)))$q$
32083
+ INTO v USING p_order_number;
32084
+ RETURN coalesce(v, false);
32085
+ END $$;
32086
+
32087
+ -- C4: printstatus in batch (max. 500 ids, één rij per id).
32088
+ CREATE OR REPLACE FUNCTION public.logistics_print_status(p_queue_item_ids uuid[])
32089
+ RETURNS TABLE(queue_item_id uuid, status text, printnode_status text, fase text, fase_gestart_op timestamptz, melding text)
32090
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
32091
+ DECLARE v_fgo text; v_log text;
32092
+ BEGIN
32093
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_print_status');
32094
+ IF coalesce(cardinality(p_queue_item_ids), 0) > 500 THEN
32095
+ RAISE EXCEPTION 'logistics_print_status: maximaal 500 ids per aanroep (gekregen: %)', cardinality(p_queue_item_ids)
32096
+ USING ERRCODE = '22023';
32097
+ END IF;
32098
+ IF coalesce(cardinality(p_queue_item_ids), 0) = 0 THEN RETURN; END IF;
32099
+ IF to_regclass('public.print_jobs') IS NULL THEN
32100
+ RETURN QUERY SELECT u.id, 'geen_printmodule'::text, NULL::text, NULL::text, NULL::timestamptz,
32101
+ 'De printmodule is in deze omgeving niet geïnstalleerd.'::text
32102
+ FROM (SELECT DISTINCT x AS id FROM unnest(p_queue_item_ids) x) u;
32103
+ RETURN;
32104
+ END IF;
32105
+ -- fase_gestart_op komt van Layouts; alleen lezen als de kolom bestaat.
32106
+ v_fgo := CASE WHEN EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public'
32107
+ AND table_name='print_jobs' AND column_name='fase_gestart_op')
32108
+ THEN 'pj.fase_gestart_op' ELSE 'NULL::timestamptz' END;
32109
+ -- 'fase' = print_jobs.verwerkingsfase; ontbreekt die, dan de laatste print_job_logs-gebeurtenis.
32110
+ v_log := CASE WHEN to_regclass('public.print_job_logs') IS NOT NULL
32111
+ THEN 'SELECT l.gebeurtenis FROM public.print_job_logs l WHERE l.job_id = pj.id ORDER BY l.created_at DESC LIMIT 1'
32112
+ ELSE 'SELECT NULL::text AS gebeurtenis' END;
32113
+ RETURN QUERY EXECUTE format($q$
32114
+ SELECT u.id,
32115
+ CASE WHEN q.id IS NULL THEN 'onbekende_regel'
32116
+ WHEN pj.id IS NULL THEN 'geen_opdracht'
32117
+ ELSE pj.status::text END,
32118
+ pj.printnode_status,
32119
+ COALESCE(NULLIF(pj.verwerkingsfase, ''), lg.gebeurtenis),
32120
+ %s,
32121
+ COALESCE(NULLIF(pj.foutmelding, ''), NULLIF(pj.printnode_bericht, ''))
32122
+ FROM (SELECT DISTINCT x AS id FROM unnest($1) x) u
32123
+ LEFT JOIN public.pick_queue q ON q.id = u.id
32124
+ LEFT JOIN public.print_jobs pj ON pj.id = q.print_job_id
32125
+ LEFT JOIN LATERAL (%s) lg ON true$q$, v_fgo, v_log)
32126
+ USING p_queue_item_ids;
32127
+ END $$;
32128
+
32129
+ -- C2: printed_at/print_count volgen de echte uitkomst van de printopdracht.
32130
+ CREATE OR REPLACE FUNCTION public.logistics_print_job_voltooid_trg()
32131
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
32132
+ BEGIN
32133
+ IF NEW.status::text = 'voltooid' AND (TG_OP = 'INSERT' OR OLD.status IS DISTINCT FROM NEW.status) THEN
32134
+ UPDATE public.pick_queue q
32135
+ SET printed_at = COALESCE(NEW.voltooid_op, now()),
32136
+ print_count = CASE WHEN q.printed_at IS NULL OR q.printed_at < NEW.created_at
32137
+ THEN COALESCE(q.print_count, 0) + 1 ELSE q.print_count END,
32138
+ updated_at = now()
32139
+ WHERE q.print_job_id = NEW.id
32140
+ AND q.printed_at IS DISTINCT FROM COALESCE(NEW.voltooid_op, now());
32141
+ END IF;
32142
+ RETURN NEW;
32143
+ EXCEPTION WHEN OTHERS THEN
32144
+ -- Een printopdracht mag nooit mislukken door deze bijwerking; wel zichtbaar maken.
32145
+ BEGIN
32146
+ INSERT INTO public.logistics_trace_events(phase, step, status, reason)
32147
+ VALUES ('printen', 'printed_at bijwerken na voltooid', 'mislukt', left(SQLERRM, 300));
32148
+ EXCEPTION WHEN OTHERS THEN NULL; END;
32149
+ RETURN NEW;
32150
+ END $$;
32151
+
32152
+ DO $t$ BEGIN
32153
+ IF to_regclass('public.print_jobs') IS NULL THEN
32154
+ RAISE NOTICE 'logistiek-engine: print_jobs ontbreekt; printstatus-trigger wordt aangemaakt zodra de printmodule er is (opnieuw draaien)';
32155
+ ELSIF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_logistics_print_job_voltooid' AND tgrelid = 'public.print_jobs'::regclass) THEN
32156
+ CREATE TRIGGER trg_logistics_print_job_voltooid
32157
+ AFTER INSERT OR UPDATE OF status ON public.print_jobs
32158
+ FOR EACH ROW EXECUTE FUNCTION public.logistics_print_job_voltooid_trg();
32159
+ END IF;
32160
+ END $t$;
32161
+
32162
+ -- Installatiecontrole 1.0.194.
32163
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_194()
32164
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
32165
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
32166
+ DECLARE v jsonb; v_at timestamptz; n int; v_pc boolean;
32167
+ BEGIN
32168
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_install_check_194');
32169
+ SELECT value INTO v FROM public.logistics_tick_state WHERE key = 'sticker_sync';
32170
+ v_at := (v->>'at')::timestamptz;
32171
+ onderdeel := 'Engine: sticker-sync laatste run zonder fout';
32172
+ ok := v IS NOT NULL AND jsonb_array_length(coalesce(v->'fouten', '[]'::jsonb)) = 0 AND v_at > now() - interval '15 minutes';
32173
+ toelichting := CASE WHEN v IS NULL THEN 'nog geen ronde vastgelegd (1.0.194 of nieuwer moet draaien)'
32174
+ WHEN v_at <= now() - interval '15 minutes' THEN 'laatste ronde ' || to_char(v_at, 'DD-MM HH24:MI') || ' (ouder dan 15 min)'
32175
+ WHEN ok THEN 'geen fouten (' || to_char(v_at, 'HH24:MI:SS') || ')'
32176
+ ELSE 'fout: ' || left((v->'fouten')::text, 300) END;
32177
+ RETURN NEXT;
32178
+
32179
+ onderdeel := 'Engine: wek-aanroep gaat niet verloren (kick_pending-mechanisme aanwezig)';
32180
+ ok := EXISTS (SELECT 1 FROM pg_proc WHERE pronamespace='public'::regnamespace AND proname='logistics_plan_kick' AND prosrc LIKE '%kick_pending%')
32181
+ AND to_regprocedure('public.logistics_kick_pending_take(timestamptz)') IS NOT NULL;
32182
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine 1.0.194 via schemasync' END; RETURN NEXT;
32183
+
32184
+ onderdeel := 'Engine: tick-aanroep heeft eigen time-out';
32185
+ SELECT count(*) INTO n FROM pg_proc WHERE pronamespace='public'::regnamespace
32186
+ AND proname IN ('logistics_plan_kick', 'logistics_recalc_tick_kick') AND prosrc LIKE '%timeout_milliseconds%';
32187
+ ok := n = 2;
32188
+ BEGIN
32189
+ toelichting := CASE WHEN ok THEN public.logistics_tick_timeout_ms() || ' ms' ELSE n || ' van 2 aanroepen met time-out' END;
32190
+ EXCEPTION WHEN OTHERS THEN ok := false; toelichting := SQLERRM; END;
32191
+ RETURN NEXT;
32192
+
32193
+ onderdeel := 'Engine: claim-timeout ≤ 3 min';
32194
+ BEGIN
32195
+ n := public.logistics_plan_setting('claim_timeout_minutes', 2)::int;
32196
+ ok := n <= 3; toelichting := n || ' min';
32197
+ EXCEPTION WHEN OTHERS THEN ok := false; toelichting := SQLERRM; END;
32198
+ RETURN NEXT;
32199
+
32200
+ onderdeel := 'Engine: printstatus-trigger aanwezig';
32201
+ ok := to_regclass('public.print_jobs') IS NOT NULL
32202
+ AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_logistics_print_job_voltooid' AND tgrelid = 'public.print_jobs'::regclass);
32203
+ toelichting := CASE WHEN to_regclass('public.print_jobs') IS NULL THEN 'print_jobs ontbreekt (printmodule niet geïnstalleerd)'
32204
+ WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: schemabestand opnieuw draaien' END;
32205
+ RETURN NEXT;
32206
+
32207
+ -- printed_at volgt print_jobs pas echt als geen enkele functie hem nog bij het aanmaken zet.
32208
+ SELECT EXISTS (SELECT 1 FROM pg_proc WHERE pronamespace='public'::regnamespace
32209
+ AND proname IN ('pick_complete_print', 'pick_complete_print_cart', 'pick_complete_print_carts')
32210
+ AND prosrc ~* 'printed_at\s*=\s*(now|clock_timestamp)') INTO v_pc;
32211
+ onderdeel := 'Engine: printed_at volgt print_jobs';
32212
+ ok := to_regprocedure('public.logistics_print_job_voltooid_trg()') IS NOT NULL AND NOT v_pc;
32213
+ toelichting := CASE WHEN ok THEN 'printed_at wordt alleen gezet bij status voltooid'
32214
+ WHEN v_pc THEN 'Engine-trigger aanwezig, maar Pickschermen (pick_complete_print*) zet printed_at nog bij het aanmaken'
32215
+ ELSE 'ontbreekt: Engine 1.0.194' END;
32216
+ RETURN NEXT;
32217
+
32218
+ onderdeel := 'Engine: leverdatum veilig als datum (logistics_order_date)';
32219
+ ok := to_regprocedure('public.logistics_order_date(text)') IS NOT NULL
32220
+ AND NOT EXISTS (SELECT 1 FROM pg_proc WHERE pronamespace='public'::regnamespace
32221
+ AND proname IN ('logistics_sticker_sync','logistics_own_sticker_sync','logistics_own_traysticker_sync')
32222
+ AND prosrc ~ '\mo\.delivery_date\s*>=');
32223
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'sticker-sync vergelijkt leverdatum nog als tekst' END; RETURN NEXT;
32224
+ END $$;
32225
+
32226
+ CREATE OR REPLACE FUNCTION public.logistics_own_sticker_sync(p_limit integer DEFAULT 200)
32227
+ RETURNS jsonb
32228
+ LANGUAGE plpgsql
32229
+ SECURITY DEFINER
32230
+ SET search_path TO 'public'
32231
+ AS $function$
32232
+ DECLARE
32233
+ r record;
32234
+ v_bron text;
32235
+ v_n int;
32236
+ v_sticker uuid;
32237
+ v_status text;
32238
+ v_reden text;
32239
+ v_bijgewerkt int := 0;
32240
+ v_vrij int := 0;
32241
+ v_uit int := 0;
32242
+ BEGIN
32243
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_own_sticker_sync');
32244
+ FOR r IN
32245
+ SELECT o.id AS order_id, o.order_number, o.customer_id, oi.product_id,
32246
+ sum(public.logistics_own_sticker_count(oi.unit, oi.quantity, oi.plants_per_crate, prd.plants_per_crate))::int AS planten,
32247
+ bool_or(public.logistics_own_sticker_count(oi.unit, oi.quantity, oi.plants_per_crate, prd.plants_per_crate) IS NULL) AS onbekend
32248
+ FROM public.orders o
32249
+ JOIN public.order_items oi ON oi.order_id = o.id
32250
+ LEFT JOIN public.products prd ON prd.id = oi.product_id
32251
+ WHERE oi.needs_sticker IS TRUE
32252
+ AND oi.quarantined_at IS NULL
32253
+ AND oi.product_id IS NOT NULL
32254
+ AND coalesce(o.order_source, '') <> 'floriday'
32255
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
32256
+ AND coalesce(o.status, '') <> 'geannuleerd'
32257
+ AND public.logistics_order_date(o.delivery_date) >= current_date - 1
32258
+ GROUP BY o.id, o.order_number, o.customer_id, oi.product_id
32259
+ LIMIT greatest(1, p_limit)
32260
+ LOOP
32261
+ v_bron := r.order_id::text || ':' || r.product_id::text;
32262
+ SELECT count(*), (array_agg(s.id))[1] INTO v_n, v_sticker
32263
+ FROM public.customer_product_stickers s
32264
+ JOIN public.customers c ON c.id = r.customer_id
32265
+ WHERE s.trade_item_key = r.product_id::text
32266
+ AND s.customer_key IN (c.id::text, 'tf:' || coalesce(c.twinfield_code, '-'), 'cn:' || coalesce(c.customer_number, '-'));
32267
+ v_status := 'wacht_op_moment'; v_reden := NULL;
32268
+ IF r.onbekend THEN
32269
+ 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.';
32270
+ ELSIF coalesce(r.planten, 0) < 1 THEN
32271
+ v_status := 'geblokkeerd'; v_reden := 'Aantal planten is 0; geen sticker te printen.';
32272
+ ELSIF v_n = 0 THEN
32273
+ v_status := 'geblokkeerd'; v_reden := 'Geen eigen sticker gevonden voor deze klant en dit product. Voeg hem toe bij de klant.';
32274
+ ELSIF v_n > 1 THEN
32275
+ v_status := 'geblokkeerd'; v_reden := 'Meer dan één eigen sticker voor deze klant en dit product; er mag er maar één zijn.';
32276
+ END IF;
32277
+
32278
+ UPDATE public.logistics_sticker_prints
32279
+ SET kopieen = greatest(coalesce(r.planten, 0), 1),
32280
+ eigen_sticker_id = CASE WHEN v_n = 1 THEN v_sticker ELSE NULL END,
32281
+ status = CASE WHEN status = 'geblokkeerd' AND v_status = 'wacht_op_moment' THEN 'wacht_op_moment'
32282
+ WHEN status = 'niet_printen' THEN v_status
32283
+ WHEN status = 'wacht_op_moment' THEN v_status ELSE status END,
32284
+ reden = CASE WHEN status IN ('wacht_op_moment', 'geblokkeerd', 'niet_printen') THEN v_reden ELSE reden END,
32285
+ updated_at = now()
32286
+ WHERE soort = 'eigen_sticker' AND bron_id = v_bron
32287
+ AND status IN ('wacht_op_moment', 'geblokkeerd', 'niet_printen');
32288
+ IF NOT FOUND AND NOT EXISTS (SELECT 1 FROM public.logistics_sticker_prints WHERE soort = 'eigen_sticker' AND bron_id = v_bron) THEN
32289
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, kopieen, status, reden, eigen_sticker_id)
32290
+ VALUES ('eigen_sticker', v_bron, r.order_id, r.order_number, greatest(coalesce(r.planten, 0), 1), v_status, v_reden,
32291
+ CASE WHEN v_n = 1 THEN v_sticker ELSE NULL END);
32292
+ END IF;
32293
+ v_bijgewerkt := v_bijgewerkt + 1;
32294
+ END LOOP;
32295
+
32296
+ UPDATE public.logistics_sticker_prints p
32297
+ SET status = 'niet_printen', reden = 'Sticker uitgezet op de orderregel of order vervallen.', updated_at = now()
32298
+ WHERE p.soort = 'eigen_sticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
32299
+ AND NOT EXISTS (
32300
+ SELECT 1 FROM public.order_items oi JOIN public.orders o ON o.id = oi.order_id
32301
+ WHERE oi.order_id = p.order_id AND p.bron_id = oi.order_id::text || ':' || oi.product_id::text
32302
+ AND oi.needs_sticker IS TRUE AND oi.quarantined_at IS NULL
32303
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.status, '') <> 'geannuleerd');
32304
+ GET DIAGNOSTICS v_uit = ROW_COUNT;
32305
+
32306
+ WITH vrij AS (
32307
+ SELECT p.id,
32308
+ (SELECT public.logistics_try_uuid(q.payload->>'cartId')
32309
+ FROM public.pick_queue q
32310
+ WHERE q.cancelled_at IS NULL AND q.print_job_id IS NOT NULL
32311
+ AND (q.payload->>'orderId' = p.order_id::text OR q.payload->'orderIds' ? p.order_id::text)
32312
+ ORDER BY q.created_at LIMIT 1) AS kar,
32313
+ (SELECT k.print_plek_id::text
32314
+ FROM public.pick_queue q
32315
+ JOIN public.print_plek_koppelingen k ON k.bron IN ('pickstation', 'pick_station') AND k.bron_id = q.station_id::text
32316
+ WHERE q.cancelled_at IS NULL AND q.print_job_id IS NOT NULL
32317
+ AND (q.payload->>'orderId' = p.order_id::text OR q.payload->'orderIds' ? p.order_id::text)
32318
+ ORDER BY q.created_at, k.created_at LIMIT 1) AS plek,
32319
+ s.width_mm, s.height_mm,
32320
+ public.logistics_document_for_order(p.order_id, 'eigen_sticker') AS lay
32321
+ FROM public.logistics_sticker_prints p
32322
+ LEFT JOIN public.customer_product_stickers s ON s.id = p.eigen_sticker_id
32323
+ WHERE p.soort = 'eigen_sticker' AND p.status = 'wacht_op_moment'
32324
+ AND EXISTS (SELECT 1 FROM public.pick_queue q
32325
+ WHERE q.cancelled_at IS NULL AND q.print_job_id IS NOT NULL
32326
+ AND (q.payload->>'orderId' = p.order_id::text OR q.payload->'orderIds' ? p.order_id::text))
32327
+ )
32328
+ UPDATE public.logistics_sticker_prints p
32329
+ SET status = 'klaar', cart_id = vrij.kar, print_plek_id = vrij.plek,
32330
+ breedte_mm = coalesce(vrij.width_mm, (vrij.lay->>'breedte_mm')::numeric),
32331
+ hoogte_mm = coalesce(vrij.height_mm, (vrij.lay->>'hoogte_mm')::numeric),
32332
+ updated_at = now()
32333
+ FROM vrij
32334
+ WHERE p.id = vrij.id;
32335
+ GET DIAGNOSTICS v_vrij = ROW_COUNT;
32336
+
32337
+ RETURN jsonb_build_object('regels', v_bijgewerkt, 'vrijgegeven', v_vrij, 'niet_printen', v_uit);
32338
+ END;
32339
+ $function$
32340
+ ;
32341
+
32342
+ CREATE OR REPLACE FUNCTION public.logistics_own_traysticker_sync(p_limit integer DEFAULT 200)
32343
+ RETURNS jsonb
32344
+ LANGUAGE plpgsql
32345
+ SECURITY DEFINER
32346
+ SET search_path TO 'public'
32347
+ AS $function$
32348
+ DECLARE
32349
+ s jsonb; v_tray text; v_rel text; v_station text;
32350
+ r record; v_bron text; v_keep text[] := '{}'; v_wacht_orders uuid[] := '{}';
32351
+ n int := 0; n_geblokkeerd int := 0; n_vrij int := 0; n_uit int := 0;
32352
+ BEGIN
32353
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_own_traysticker_sync');
32354
+ SELECT settings INTO s FROM public.logistics_rule_configs
32355
+ ORDER BY is_active DESC, (status = 'actief') DESC, updated_at DESC LIMIT 1;
32356
+ v_tray := public.logistics_rule_text(s, 'pickstations', 'traysticker_release', 'uit');
32357
+ IF v_tray = 'uit' THEN
32358
+ RETURN jsonb_build_object('trayRelease', v_tray, 'regels', 0);
32359
+ END IF;
32360
+ v_rel := public.logistics_rule_text(s, 'pickstations', 'sticker_release', 'with_pick_release');
32361
+ v_station := CASE WHEN v_rel = 'with_packing_slip'
32362
+ THEN public.logistics_rule_text(s, 'pickstations', 'packing_slip_station', '')
32363
+ ELSE public.logistics_rule_text(s, 'pickstations', 'sticker_station', '') END;
32364
+
32365
+ FOR r IN
32366
+ WITH regels AS (
32367
+ SELECT o.id AS order_id, o.order_number, ci.cart_id, ci.id AS ci_id, ci.crates, ci.created_at,
32368
+ coalesce(ci.product_name, oi.product_name, 'onbekend') AS regelnaam
32369
+ FROM public.orders o
32370
+ JOIN public.order_items oi ON oi.order_id = o.id
32371
+ JOIN public.order_cart_items ci ON ci.order_item_id = oi.id
32372
+ WHERE coalesce(o.order_source, '') <> 'floriday'
32373
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
32374
+ AND coalesce(o.status, '') <> 'geannuleerd'
32375
+ AND oi.quarantined_at IS NULL
32376
+ AND public.logistics_order_date(o.delivery_date) >= current_date - 1
32377
+ )
32378
+ SELECT g.*,
32379
+ (SELECT min(x.regelnaam) FROM regels x WHERE x.order_id = g.order_id AND coalesce(x.crates, 0) <= 0) AS onbekende_regel
32380
+ FROM regels g
32381
+ ORDER BY g.order_number, g.cart_id, g.created_at, g.ci_id
32382
+ LIMIT greatest(1, p_limit)
32383
+ LOOP
32384
+ IF r.onbekende_regel IS NOT NULL THEN
32385
+ v_wacht_orders := v_wacht_orders || r.order_id;
32386
+ v_bron := r.order_id::text || ':fustaantal-onbekend';
32387
+ v_keep := v_keep || v_bron;
32388
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, status, reden)
32389
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, NULL, 1, 'geblokkeerd',
32390
+ 'Fustaantal van regel ' || r.onbekende_regel || ' onbekend. Alle traystickers van deze order wachten, zodat de nummering klopt.')
32391
+ ON CONFLICT DO NOTHING;
32392
+ n_geblokkeerd := n_geblokkeerd + 1;
32393
+ CONTINUE;
32394
+ END IF;
32395
+ FOR k IN 1 .. r.crates LOOP
32396
+ v_bron := r.order_id::text || ':' || r.cart_id::text || ':' || r.ci_id::text || ':' || k::text;
32397
+ v_keep := v_keep || v_bron;
32398
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, print_plek_id, status, reden)
32399
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, r.cart_id, 1,
32400
+ public.logistics_sticker_cart_plek(r.cart_id, v_station), 'wacht_op_moment', 'Wacht op de pakbon van deze kar.')
32401
+ ON CONFLICT DO NOTHING;
32402
+ UPDATE public.logistics_sticker_prints
32403
+ SET status = 'wacht_op_moment', reden = 'Wacht op de pakbon van deze kar.', updated_at = now()
32404
+ WHERE soort = 'eigen_traysticker' AND bron_id = v_bron AND status = 'geblokkeerd'
32405
+ AND reden LIKE 'Fustaantal van regel %';
32406
+ n := n + 1;
32407
+ END LOOP;
32408
+ END LOOP;
32409
+
32410
+ UPDATE public.logistics_sticker_prints p
32411
+ SET status = 'geblokkeerd',
32412
+ reden = (SELECT b.reden FROM public.logistics_sticker_prints b
32413
+ WHERE b.soort = 'eigen_traysticker' AND b.bron_id = p.order_id::text || ':fustaantal-onbekend' LIMIT 1),
32414
+ updated_at = now()
32415
+ WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'klaar')
32416
+ AND p.order_id = ANY (v_wacht_orders);
32417
+ v_keep := v_keep || ARRAY(SELECT p.bron_id FROM public.logistics_sticker_prints p
32418
+ WHERE p.soort = 'eigen_traysticker' AND p.order_id = ANY (v_wacht_orders)
32419
+ AND p.reden LIKE 'Fustaantal van regel %');
32420
+
32421
+ UPDATE public.logistics_sticker_prints p
32422
+ SET status = 'niet_printen', reden = 'Fust hoort niet meer bij deze kar of order vervallen.', updated_at = now()
32423
+ WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
32424
+ AND NOT (p.bron_id = ANY (v_keep))
32425
+ AND p.order_id IN (SELECT o.id FROM public.orders o WHERE public.logistics_order_date(o.delivery_date) >= current_date - 1);
32426
+ GET DIAGNOSTICS n_uit = ROW_COUNT;
32427
+
32428
+ UPDATE public.logistics_sticker_prints p
32429
+ SET status = 'klaar', reden = NULL,
32430
+ print_plek_id = coalesce(p.print_plek_id, public.logistics_sticker_cart_plek(p.cart_id, v_station)),
32431
+ updated_at = now()
32432
+ WHERE p.soort = 'eigen_traysticker' AND p.status = 'wacht_op_moment'
32433
+ AND p.cart_id IS NOT NULL AND public.logistics_cart_slip_queued(p.cart_id);
32434
+ GET DIAGNOSTICS n_vrij = ROW_COUNT;
32435
+
32436
+ RETURN jsonb_build_object('trayRelease', v_tray, 'fusten', n, 'geblokkeerd', n_geblokkeerd, 'vrijgegeven', n_vrij, 'niet_printen', n_uit);
32437
+ END; $function$
32438
+ ;
32439
+
32440
+ CREATE OR REPLACE FUNCTION public.logistics_sticker_sync(p_limit integer DEFAULT 200)
32441
+ RETURNS jsonb
32442
+ LANGUAGE plpgsql
32443
+ SECURITY DEFINER
32444
+ SET search_path TO 'public'
32445
+ AS $function$
32446
+ DECLARE
32447
+ s jsonb; v_rel text; v_station text; v_tray text;
32448
+ st record; r_ord record; d jsonb; fi jsonb; v_n int; v_order uuid; v_onr text;
32449
+ v_copies int; v_size numeric[]; v_type text; v_should text; v_keep uuid[]; v_del text; v_fo text; v_cart uuid;
32450
+ v_crates_tot int; v_pieces_tot int; r record; v_rows int; v_cp jsonb; v_bron text; v_ncarts int; sv record;
32451
+ n_st int := 0; n_tray int := 0;
32452
+ BEGIN
32453
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_sticker_sync');
32454
+ SELECT settings INTO s FROM public.logistics_rule_configs
32455
+ ORDER BY is_active DESC, (status = 'actief') DESC, updated_at DESC LIMIT 1;
32456
+ v_rel := public.logistics_rule_text(s, 'pickstations', 'sticker_release', 'with_pick_release');
32457
+ v_station := CASE WHEN v_rel = 'with_packing_slip'
32458
+ THEN public.logistics_rule_text(s, 'pickstations', 'packing_slip_station', '')
32459
+ ELSE public.logistics_rule_text(s, 'pickstations', 'sticker_station', '') END;
32460
+ v_tray := public.logistics_rule_text(s, 'pickstations', 'traysticker_release', 'uit');
32461
+
32462
+ FOR st IN SELECT * FROM public.floriday_customer_stickers_cache ORDER BY updated_at DESC LIMIT greatest(p_limit,1) LOOP
32463
+ n_st := n_st + 1;
32464
+ d := coalesce(st.data, '{}'::jsonb);
32465
+ fi := coalesce(d->'stickerFileInformation', '{}'::jsonb);
32466
+ v_size := CASE WHEN st.sticker_width_mm IS NOT NULL AND st.sticker_height_mm IS NOT NULL
32467
+ THEN ARRAY[st.sticker_width_mm::numeric, st.sticker_height_mm::numeric]
32468
+ ELSE public.logistics_sticker_parse_size(d->'stickerSize') END;
32469
+ v_type := upper(nullif(btrim(coalesce(st.sticker_type, d->>'stickerType', '')), ''));
32470
+ v_should := lower(coalesce(st.should_be_printed::text, fi->>'shouldBePrinted', ''));
32471
+
32472
+ -- Koppeling volgens Suppliers API 2026v1: alleen via salesOrderId.
32473
+ SELECT count(*), (array_agg(id))[1], (array_agg(order_number))[1] INTO v_n, v_order, v_onr
32474
+ FROM public.orders WHERE deleted_at IS NULL
32475
+ AND nullif(btrim(coalesce(st.sales_order_id, d->>'salesOrderId', '')), '') IS NOT NULL
32476
+ AND floriday_sales_order_id = coalesce(st.sales_order_id, d->>'salesOrderId');
32477
+
32478
+ IF coalesce(st.is_deleted,false) OR lower(coalesce(d->>'isDeleted','')) = 'true' THEN
32479
+ UPDATE public.logistics_sticker_prints SET status = 'niet_printen', reden = 'De koper heeft deze sticker verwijderd.'
32480
+ WHERE soort = 'sticker' AND bron_id = st.floriday_id AND status NOT IN ('in_printwachtrij','afgemeld');
32481
+ CONTINUE;
32482
+ END IF;
32483
+ IF lower(coalesce(d->>'isHandled','')) = 'true' THEN
32484
+ UPDATE public.logistics_sticker_prints SET status = 'afgemeld', reden = 'Al afgemeld in Floriday.', handled_at = coalesce(handled_at, now())
32485
+ WHERE soort = 'sticker' AND bron_id = st.floriday_id AND status <> 'afgemeld';
32486
+ CONTINUE;
32487
+ END IF;
32488
+ IF v_n = 1 AND public.logistics_sales_order_external_fulfilled(v_order, coalesce(st.sales_order_id, d->>'salesOrderId')) THEN
32489
+ UPDATE public.logistics_sticker_prints SET status = 'niet_printen', reden = 'Afgehandeld in Floriday; door ons niet geprint en niet afgemeld.'
32490
+ WHERE soort = 'sticker' AND bron_id = st.floriday_id AND status NOT IN ('in_printwachtrij','afgemeld');
32491
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, 0, v_size, NULL, NULL, 'niet_printen', 'Afgehandeld in Floriday; door ons niet geprint en niet afgemeld.');
32492
+ CONTINUE;
32493
+ END IF;
32494
+ IF v_n <> 1 THEN
32495
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, NULL, NULL, NULL, 0, v_size, NULL, NULL, 'geblokkeerd',
32496
+ format('Geen eenduidige order bij Floriday-verkooporder %s (%s gevonden).', coalesce(st.sales_order_id, d->>'salesOrderId', 'onbekend'), v_n));
32497
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type WHERE soort='sticker' AND bron_id = st.floriday_id;
32498
+ CONTINUE;
32499
+ END IF;
32500
+ IF v_should = 'false' THEN
32501
+ -- Geen printopdracht; zichtbaar als 'niet_printen'. Wordt ook niet afgemeld in Floriday.
32502
+ UPDATE public.logistics_sticker_prints SET status = 'niet_printen',
32503
+ reden = 'Floriday: deze sticker hoeft niet geprint te worden (shouldBePrinted = false).'
32504
+ WHERE soort = 'sticker' AND bron_id = st.floriday_id AND cart_id IS NOT NULL AND status NOT IN ('in_printwachtrij','afgemeld');
32505
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, 0, v_size, NULL, NULL, 'niet_printen',
32506
+ 'Floriday: deze sticker hoeft niet geprint te worden (shouldBePrinted = false).');
32507
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type WHERE soort='sticker' AND bron_id = st.floriday_id;
32508
+ CONTINUE;
32509
+ END IF;
32510
+ IF lower(coalesce(fi->>'deliverSeparately','')) = 'true' THEN
32511
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, 0, v_size, NULL, NULL, 'geblokkeerd',
32512
+ 'Sticker moet apart worden geleverd (deliverSeparately); kies handmatig hoe die mee gaat.');
32513
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type WHERE soort='sticker' AND bron_id = st.floriday_id;
32514
+ CONTINUE;
32515
+ END IF;
32516
+ IF nullif(btrim(coalesce(d->>'salesOrderCorrectionRequestId','')),'') IS NOT NULL THEN
32517
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, 0, v_size, NULL, NULL, 'wacht_op_moment',
32518
+ 'Wacht tot de correctie op de verkooporder is verwerkt.');
32519
+ CONTINUE;
32520
+ END IF;
32521
+ -- 1.0.182: aantal uit numberOfCopies -> additionalServices.numberOfUnits (LABEL) -> afgeleid uit stickersoort.
32522
+ v_cp := public.logistics_sticker_copies(v_order, st.floriday_id, v_type, fi, nullif(btrim(coalesce(st.trade_item_id,'')),''));
32523
+ v_copies := nullif(v_cp->>'aantal','')::int; v_bron := v_cp->>'bron';
32524
+ IF coalesce(v_copies,0) <= 0 THEN
32525
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, 0, v_size, NULL, NULL, 'geblokkeerd',
32526
+ 'Aantal stickers onbekend: geen numberOfCopies, geen numberOfUnits (LABEL) en niet af te leiden uit de stickersoort; niet geprint.');
32527
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type, kopieen_bron = NULL WHERE soort='sticker' AND bron_id = st.floriday_id;
32528
+ CONTINUE;
32529
+ END IF;
32530
+ IF v_type = 'DOCUMENT' THEN
32531
+ SELECT oc.id INTO v_cart FROM logistics_active.order_carts oc WHERE oc.order_id = v_order ORDER BY oc.cart_number, oc.created_at LIMIT 1;
32532
+ IF v_cart IS NULL THEN
32533
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, v_copies, v_size, NULL, NULL, 'wacht_op_moment',
32534
+ 'Wacht tot de order op een kar staat.');
32535
+ ELSE
32536
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, v_cart, v_copies, v_size,
32537
+ public.logistics_sticker_cart_plek(v_cart, v_station), NULL,
32538
+ CASE WHEN public.logistics_cart_slip_queued(v_cart) THEN 'klaar' ELSE 'wacht_op_moment' END,
32539
+ CASE WHEN public.logistics_cart_slip_queued(v_cart) THEN NULL ELSE 'Wacht op de pakbon van deze kar.' END);
32540
+ UPDATE public.logistics_sticker_prints SET status = 'niet_printen', reden = 'Vervangen door regels per kar.'
32541
+ WHERE soort = 'sticker' AND bron_id = st.floriday_id AND (cart_id IS NULL OR cart_id <> v_cart)
32542
+ AND status NOT IN ('in_printwachtrij','afgemeld','niet_printen');
32543
+ END IF;
32544
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type, kopieen_bron = v_bron WHERE soort='sticker' AND bron_id = st.floriday_id;
32545
+ CONTINUE;
32546
+ END IF;
32547
+ IF v_type IS NULL OR v_type NOT IN ('POT','PRODUCT','SLEEVE','PACKAGE','LAYER','LOAD_CARRIER') THEN
32548
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, v_copies, v_size, NULL, NULL, 'geblokkeerd',
32549
+ format('Onbekende of niet-ondersteunde stickersoort (%s); kies handmatig hoe deze sticker mee gaat.', coalesce(v_type,'leeg')));
32550
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type WHERE soort='sticker' AND bron_id = st.floriday_id;
32551
+ CONTINUE;
32552
+ END IF;
32553
+
32554
+ v_keep := ARRAY[]::uuid[]; v_rows := 0; v_crates_tot := 0; v_pieces_tot := 0;
32555
+ FOR r IN SELECT * FROM public.logistics_sticker_distribution(v_order, v_type, v_copies, nullif(btrim(coalesce(st.trade_item_id,'')),'')) LOOP
32556
+ v_rows := v_rows + 1; v_keep := v_keep || r.cart_id;
32557
+ v_crates_tot := v_crates_tot + r.crates; v_pieces_tot := v_pieces_tot + r.pieces;
32558
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, r.cart_id, r.kopieen, v_size,
32559
+ public.logistics_sticker_cart_plek(r.cart_id, v_station), NULL,
32560
+ CASE WHEN public.logistics_cart_slip_queued(r.cart_id) THEN 'klaar' ELSE 'wacht_op_moment' END,
32561
+ CASE WHEN public.logistics_cart_slip_queued(r.cart_id) THEN NULL ELSE 'Wacht op de pakbon van deze kar.' END);
32562
+ END LOOP;
32563
+ IF v_rows = 0 THEN
32564
+ PERFORM public.logistics_sticker_upsert('sticker', st.floriday_id, v_order, v_onr, NULL, v_copies, v_size, NULL, NULL, 'geblokkeerd',
32565
+ 'De verdeling over karren sluit niet: de order staat (nog) niet op een kar, of de karren hebben geen fusten/stuks van dit artikel.');
32566
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type WHERE soort='sticker' AND bron_id = st.floriday_id;
32567
+ CONTINUE;
32568
+ END IF;
32569
+ UPDATE public.logistics_sticker_prints SET sticker_type = v_type, kopieen_bron = v_bron WHERE soort='sticker' AND bron_id = st.floriday_id;
32570
+ IF (v_type IN ('PACKAGE','LAYER') AND v_crates_tot <> v_copies) OR (v_type IN ('POT','PRODUCT','SLEEVE') AND v_pieces_tot <> v_copies) THEN
32571
+ BEGIN
32572
+ INSERT INTO public.logistics_trace_events(phase, step, status, reason, order_ref)
32573
+ SELECT 'stickers', 'Aantal klantstickers wijkt af', 'waarschuwing',
32574
+ format('Floriday vraagt %s (%s); op de karren: %s fusten / %s stuks. Het aantal van Floriday wordt geprint.',
32575
+ v_copies, v_type, v_crates_tot, v_pieces_tot), v_onr
32576
+ WHERE NOT EXISTS (SELECT 1 FROM public.logistics_trace_events e WHERE e.order_ref = v_onr AND e.step = 'Aantal klantstickers wijkt af'
32577
+ AND e.created_at > now() - interval '1 day');
32578
+ EXCEPTION WHEN OTHERS THEN NULL; END;
32579
+ END IF;
32580
+ UPDATE public.logistics_sticker_prints SET status = 'niet_printen',
32581
+ reden = CASE WHEN cart_id IS NULL THEN 'Vervangen door regels per kar.' ELSE 'Kar hoort niet meer bij deze sticker.' END
32582
+ WHERE soort = 'sticker' AND bron_id = st.floriday_id
32583
+ AND (cart_id IS NULL OR NOT (cart_id = ANY (v_keep)))
32584
+ AND status NOT IN ('in_printwachtrij','afgemeld','niet_printen');
32585
+ END LOOP;
32586
+
32587
+ -- 1.0.182: stickers die alleen in additionalServices van de verkooporder staan (stickercache leeg of nog niet bijgewerkt).
32588
+ FOR sv IN
32589
+ SELECT o.id AS order_id, o.order_number, e->>'stickerId' AS sticker_id, sum((e->>'numberOfUnits')::int)::int AS units
32590
+ FROM public.orders o
32591
+ JOIN public.floriday_salesorders_cache s ON s.floriday_id = o.floriday_sales_order_id AND NOT coalesce(s.is_deleted,false)
32592
+ CROSS JOIN LATERAL jsonb_array_elements(CASE WHEN jsonb_typeof(s.data->'additionalServices') = 'array' THEN s.data->'additionalServices' ELSE '[]'::jsonb END) e
32593
+ WHERE o.deleted_at IS NULL AND public.logistics_order_date(o.delivery_date) >= current_date - 2
32594
+ AND upper(coalesce(e->>'unit','')) = 'LABEL' AND (e->>'numberOfUnits') ~ '^\d+$'
32595
+ AND nullif(btrim(coalesce(e->>'stickerId','')),'') IS NOT NULL
32596
+ AND NOT EXISTS (SELECT 1 FROM public.floriday_customer_stickers_cache c WHERE c.floriday_id = e->>'stickerId')
32597
+ AND NOT public.logistics_sales_order_external_fulfilled(o.id, o.floriday_sales_order_id)
32598
+ GROUP BY o.id, o.order_number, e->>'stickerId'
32599
+ LIMIT greatest(p_limit,1)
32600
+ LOOP
32601
+ IF coalesce(sv.units,0) <= 0 THEN CONTINUE; END IF;
32602
+ SELECT count(*), (array_agg(oc.id ORDER BY oc.cart_number, oc.created_at))[1] INTO v_ncarts, v_cart
32603
+ FROM logistics_active.order_carts oc WHERE oc.order_id = sv.order_id;
32604
+ IF v_ncarts = 1 THEN
32605
+ PERFORM public.logistics_sticker_upsert('sticker', sv.sticker_id, sv.order_id, sv.order_number, v_cart, sv.units, NULL,
32606
+ public.logistics_sticker_cart_plek(v_cart, v_station), NULL,
32607
+ CASE WHEN public.logistics_cart_slip_queued(v_cart) THEN 'klaar' ELSE 'wacht_op_moment' END,
32608
+ CASE WHEN public.logistics_cart_slip_queued(v_cart) THEN NULL ELSE 'Wacht op de pakbon van deze kar.' END);
32609
+ UPDATE public.logistics_sticker_prints SET status = 'niet_printen', reden = 'Vervangen door regels per kar.'
32610
+ WHERE soort = 'sticker' AND bron_id = sv.sticker_id AND (cart_id IS NULL OR cart_id <> v_cart)
32611
+ AND status NOT IN ('in_printwachtrij','afgemeld','niet_printen');
32612
+ ELSE
32613
+ PERFORM public.logistics_sticker_upsert('sticker', sv.sticker_id, sv.order_id, sv.order_number, NULL, sv.units, NULL, NULL, NULL, 'wacht_op_moment',
32614
+ CASE WHEN v_ncarts = 0 THEN 'Wacht tot de order op een kar staat.'
32615
+ ELSE format('Stickersoort nog onbekend; wacht op de stickergegevens van Floriday om %s stickers over %s karren te verdelen.', sv.units, v_ncarts) END);
32616
+ END IF;
32617
+ UPDATE public.logistics_sticker_prints SET kopieen_bron = 'numberOfUnits' WHERE soort='sticker' AND bron_id = sv.sticker_id
32618
+ AND status NOT IN ('in_printwachtrij','afgemeld');
32619
+ n_st := n_st + 1;
32620
+ END LOOP;
32621
+
32622
+ IF v_tray <> 'uit' THEN
32623
+ FOR r_ord IN
32624
+ SELECT o.id, o.order_number, nullif(btrim(o.floriday_sales_order_id),'') AS sales
32625
+ FROM public.orders o
32626
+ WHERE o.deleted_at IS NULL AND nullif(btrim(o.floriday_sales_order_id),'') IS NOT NULL
32627
+ AND public.logistics_order_date(o.delivery_date) >= current_date - 2
32628
+ AND EXISTS (SELECT 1 FROM logistics_active.order_carts oc WHERE oc.order_id = o.id)
32629
+ ORDER BY o.delivery_date LIMIT greatest(p_limit,1)
32630
+ LOOP
32631
+ SELECT d2.floriday_id INTO v_del FROM public.floriday_delivery_orders_cache d2
32632
+ WHERE d2.data @> jsonb_build_object('fulfillmentRequests', jsonb_build_array(jsonb_build_object('salesOrderId', r_ord.sales)))
32633
+ ORDER BY d2.fetched_at DESC LIMIT 1;
32634
+ IF v_del IS NULL THEN CONTINUE; END IF;
32635
+ IF EXISTS (SELECT 1 FROM public.logistics_sticker_prints WHERE soort = 'traysticker' AND bron_id = v_del) THEN
32636
+ v_fo := public.logistics_afhandeling_fulfillment_order(r_ord.id)->>'fulfillmentOrderId';
32637
+ IF v_fo IS NOT NULL THEN
32638
+ UPDATE public.logistics_sticker_prints SET fulfillment_order_id = v_fo
32639
+ WHERE soort = 'traysticker' AND bron_id = v_del AND fulfillment_order_id IS DISTINCT FROM v_fo
32640
+ AND status NOT IN ('in_printwachtrij','afgemeld');
32641
+ END IF;
32642
+ CONTINUE;
32643
+ END IF;
32644
+ SELECT oc.id INTO v_cart FROM logistics_active.order_carts oc WHERE oc.order_id = r_ord.id ORDER BY oc.cart_number, oc.created_at LIMIT 1;
32645
+ v_fo := public.logistics_afhandeling_fulfillment_order(r_ord.id)->>'fulfillmentOrderId';
32646
+ PERFORM public.logistics_sticker_upsert('traysticker', v_del, r_ord.id, r_ord.order_number, v_cart, 1, NULL,
32647
+ public.logistics_sticker_cart_plek(v_cart, v_station), v_fo, 'wacht_op_moment',
32648
+ 'Wacht op geaccepteerde belading in Floriday.');
32649
+ n_tray := n_tray + 1;
32650
+ END LOOP;
32651
+ END IF;
32652
+
32653
+ RETURN jsonb_build_object('stickers', n_st, 'trayNieuw', n_tray, 'release', v_rel, 'trayRelease', v_tray,
32654
+ 'klantstickerMoment', 'with_packing_slip');
32655
+ END; $function$
32656
+ ;
32657
+
32658
+ CREATE OR REPLACE FUNCTION public.logistics_plan_kick()
32659
+ RETURNS void
32660
+ LANGUAGE plpgsql
32661
+ SECURITY DEFINER
32662
+ SET search_path TO 'public'
32663
+ AS $function$
32664
+ DECLARE
32665
+ v_base text; v_key text; v_path text; v_last timestamptz;
32666
+ v_debounce int := public.logistics_plan_setting('kick_debounce_seconds', 5)::int;
32667
+ v_running boolean; v_req bigint; v_timeout int := public.logistics_tick_timeout_ms();
32668
+ BEGIN
32669
+ PERFORM public.logistics_acc_guard('server', 'logistics_plan_kick');
32670
+ -- Loopt er al een tick-ronde (slot 'tick:ronde')? Dan geen tweede ronde wekken.
32671
+ -- Fail-closed: kan dit niet worden vastgesteld, dan valt de EXCEPTION-tak
32672
+ -- hieronder en wordt de wek-aanroep overgeslagen; de minuut-cron is het vangnet.
32673
+ SELECT EXISTS (
32674
+ SELECT 1 FROM public.logistics_plan_locks
32675
+ WHERE lock_key = 'tick:ronde' AND locked_until > now()
32676
+ ) INTO v_running;
32677
+ IF v_running IS DISTINCT FROM false THEN
32678
+ -- 1.0.194: niet laten vallen; de lopende ronde pakt de vlag vóór het vrijgeven op.
32679
+ INSERT INTO public.logistics_plan_settings(key, value, updated_at)
32680
+ VALUES ('kick_pending', clock_timestamp()::text, now())
32681
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
32682
+ BEGIN
32683
+ INSERT INTO public.logistics_kick_log(skipped_reason) VALUES ('ronde_loopt_vlag_gezet');
32684
+ EXCEPTION WHEN OTHERS THEN NULL; END;
32685
+ RETURN;
32686
+ END IF;
32687
+
32688
+ SELECT value INTO v_base FROM public.module_runtime_config WHERE key = 'logistics_tick_base_url';
32689
+ SELECT value INTO v_key FROM public.module_runtime_config WHERE key = 'logistics_tick_apikey';
32690
+ SELECT value INTO v_path FROM public.module_runtime_config WHERE key = 'logistics_tick_path';
32691
+ IF v_base IS NULL OR v_key IS NULL THEN RETURN; END IF;
32692
+ v_path := COALESCE(v_path, '/api/public/hooks/logistics-recalc-tick');
32693
+
32694
+ SELECT NULLIF(value,'')::timestamptz INTO v_last
32695
+ FROM public.logistics_plan_settings WHERE key = 'last_kick_at';
32696
+ IF v_last IS NOT NULL AND v_last > now() - make_interval(secs => v_debounce) THEN
32697
+ BEGIN INSERT INTO public.logistics_kick_log(skipped_reason) VALUES ('debounce'); EXCEPTION WHEN OTHERS THEN NULL; END;
32698
+ RETURN;
32699
+ END IF;
32700
+
32701
+ INSERT INTO public.logistics_plan_settings(key, value, updated_at)
32702
+ VALUES ('last_kick_at', now()::text, now())
32703
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
32704
+
32705
+ v_req := net.http_post(
32706
+ url := rtrim(v_base,'/') || v_path,
32707
+ headers := jsonb_build_object('Content-Type','application/json','apikey', v_key),
32708
+ body := '{}'::jsonb,
32709
+ timeout_milliseconds := v_timeout
32710
+ );
32711
+ BEGIN INSERT INTO public.logistics_kick_log(request_id) VALUES (v_req); EXCEPTION WHEN OTHERS THEN NULL; END;
32712
+ EXCEPTION WHEN OTHERS THEN
32713
+ NULL;
32714
+ END;
32715
+ $function$
32716
+ ;
32717
+
32718
+ CREATE OR REPLACE FUNCTION public.logistics_recalc_tick_kick()
32719
+ RETURNS bigint
32720
+ LANGUAGE plpgsql
32721
+ SECURITY DEFINER
32722
+ SET search_path TO 'public'
32723
+ AS $function$
32724
+ DECLARE
32725
+ v_base text; v_key text; v_path text; v_req bigint; v_host text;
32726
+ BEGIN
32727
+ PERFORM public.logistics_acc_guard('server', 'logistics_recalc_tick_kick');
32728
+ BEGIN PERFORM public.logistics_tick_calls_resolve(); EXCEPTION WHEN others THEN NULL; END;
32729
+
32730
+ SELECT NULLIF(btrim(value),'') INTO v_base FROM public.module_runtime_config WHERE key = 'logistics_tick_base_url';
32731
+ SELECT NULLIF(btrim(value),'') INTO v_key FROM public.module_runtime_config WHERE key = 'logistics_tick_apikey';
32732
+ SELECT NULLIF(btrim(value),'') INTO v_path FROM public.module_runtime_config WHERE key = 'logistics_tick_path';
32733
+ v_host := substring(v_base from '^[a-zA-Z]+://([^/?#]+)');
32734
+
32735
+ IF v_base IS NULL OR v_key IS NULL THEN
32736
+ INSERT INTO public.logistics_tick_calls (target_host, skipped_reason, engine_ok, error, resolved_at)
32737
+ VALUES (v_host,
32738
+ CASE WHEN v_base IS NULL THEN 'geen_adres' ELSE 'geen_sleutel' END,
32739
+ false,
32740
+ CASE WHEN v_base IS NULL THEN 'geen adres ingesteld' ELSE 'geen sleutel ingesteld' END,
32741
+ now());
32742
+ RAISE WARNING 'logistics_recalc_tick_kick: geen aanroep — logistics_tick_base_url en/of logistics_tick_apikey ontbreekt in module_runtime_config. Vul deze voor deze omgeving in.';
32743
+ RETURN NULL;
32744
+ END IF;
32745
+ v_path := COALESCE(v_path, '/api/public/hooks/logistics-recalc-tick');
32746
+ v_req := net.http_post(
32747
+ url := rtrim(v_base,'/') || v_path,
32748
+ headers := jsonb_build_object('Content-Type','application/json','apikey', v_key),
32749
+ body := '{}'::jsonb,
32750
+ timeout_milliseconds := public.logistics_tick_timeout_ms()
32751
+ );
32752
+ INSERT INTO public.logistics_tick_calls (request_id, target_host) VALUES (v_req, v_host);
32753
+ RETURN v_req;
32754
+ END;
32755
+ $function$
32756
+ ;
32757
+
32758
+ CREATE OR REPLACE FUNCTION public.logistics_plan_release_stale()
32759
+ RETURNS integer
32760
+ LANGUAGE plpgsql
32761
+ SECURITY DEFINER
32762
+ SET search_path TO 'public'
32763
+ AS $function$
32764
+ DECLARE
32765
+ v_n int := 0;
32766
+ v_min int := public.logistics_plan_setting('claim_timeout_minutes', 2)::int;
32767
+ v_max int := GREATEST(1, public.logistics_plan_setting('max_attempts', 3)::int);
32768
+ v_txt text;
32769
+ r record;
32770
+ BEGIN
32771
+ PERFORM public.logistics_acc_guard('server', 'logistics_plan_release_stale');
32772
+ v_txt := 'de ronde heeft deze indeling na ' || v_min || ' minuten niet afgesloten (claim niet afgerond)';
32773
+ FOR r IN
32774
+ UPDATE public.logistics_plan_queue q
32775
+ SET status = CASE WHEN q.attempts >= v_max OR q.reason_code = 'claim_niet_afgerond' THEN 'geblokkeerd' ELSE 'pending' END,
32776
+ error = v_txt,
32777
+ reason = CASE WHEN q.attempts >= v_max OR q.reason_code = 'claim_niet_afgerond'
32778
+ THEN v_txt || '; herhaald, daarom geblokkeerd' ELSE q.reason END,
32779
+ reason_code = 'claim_niet_afgerond',
32780
+ claimed_at = NULL,
32781
+ next_attempt_at = now() + make_interval(mins => LEAST(30, GREATEST(1, q.attempts))),
32782
+ updated_at = now()
32783
+ WHERE q.status = 'running'
32784
+ AND q.claimed_at < now() - make_interval(mins => v_min)
32785
+ RETURNING q.order_id, q.attempts, q.status
32786
+ LOOP
32787
+ v_n := v_n + 1;
32788
+ BEGIN
32789
+ PERFORM public.logistics_trace_order_step(
32790
+ r.order_id, 'stap:beladingindeling', 'claim niet afgerond',
32791
+ CASE WHEN r.status = 'geblokkeerd' THEN 'mislukt' ELSE 'wacht' END,
32792
+ 'ronde sloot de indeling niet af',
32793
+ CASE WHEN r.status = 'geblokkeerd'
32794
+ THEN 'De ronde sloot de indeling voor de tweede keer niet af. De order is geblokkeerd zodat dit niet blijft herhalen; bekijk de reden en bied hem opnieuw aan.'
32795
+ ELSE 'De ronde sloot de indeling niet af binnen ' || v_min || ' minuten. Eén nieuwe poging volgt.'
32796
+ END,
32797
+ 'Karrenindeling', v_txt, 'regel');
32798
+ EXCEPTION WHEN OTHERS THEN NULL;
32799
+ END;
32800
+ END LOOP;
32801
+ RETURN v_n;
32802
+ END $function$
32803
+ ;
32804
+
32805
+ CREATE OR REPLACE FUNCTION public.logistics_plan_enqueue(p_order_id uuid, p_reason text DEFAULT 'change'::text)
32806
+ RETURNS void
32807
+ LANGUAGE plpgsql
32808
+ SECURITY DEFINER
32809
+ SET search_path TO 'public'
32810
+ AS $function$
32811
+ DECLARE
32812
+ v_ok boolean;
32813
+ v_id uuid;
32814
+ v_status text;
32815
+ v_decision text;
32816
+ v_target uuid := p_order_id;
32817
+ v_wacht jsonb;
32818
+ BEGIN
32819
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_plan_enqueue');
32820
+ IF COALESCE(current_setting('logistics.merging', true), '') <> '1' THEN
32821
+ BEGIN
32822
+ PERFORM set_config('logistics.merging', '1', true);
32823
+ v_target := COALESCE(public.logistics_merge_floriday_lines(p_order_id), p_order_id);
32824
+ PERFORM set_config('logistics.merging', '0', true);
32825
+ EXCEPTION WHEN OTHERS THEN
32826
+ v_target := p_order_id;
32827
+ END;
32828
+ END IF;
32829
+
32830
+ -- A2: wachttijd na een mislukte indeling respecteren bij automatische meldingen.
32831
+ v_wacht := public.logistics_plan_wachttijd(v_target, p_reason);
32832
+ IF COALESCE((v_wacht->>'wacht')::boolean, false) THEN
32833
+ PERFORM public.logistics_plan_wachttijd_melden(v_target, p_reason, v_wacht);
32834
+ RETURN;
32835
+ END IF;
32836
+
32837
+ SELECT (o.deleted_at IS NULL AND COALESCE(o.is_reservation, false) = false AND o.plan_locked_at IS NULL)
32838
+ INTO v_ok FROM public.orders o WHERE o.id = v_target;
32839
+ -- 1.0.194: geannuleerde orders niet opnieuw indelen (alleen als de kolommen bestaan).
32840
+ IF COALESCE(v_ok, false) AND public.logistics_order_geannuleerd(v_target) THEN
32841
+ PERFORM public.logistics_trace_order_step(
32842
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'overgeslagen',
32843
+ 'niet aangemeld', 'Order is geannuleerd', NULL);
32844
+ RETURN;
32845
+ END IF;
32846
+ IF NOT COALESCE(v_ok, false) THEN
32847
+ PERFORM public.logistics_trace_order_step(
32848
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'overgeslagen',
32849
+ 'niet aangemeld', 'Order is verwijderd, samengevoegd, een reservering of de indeling staat vast', NULL);
32850
+ RETURN;
32851
+ END IF;
32852
+
32853
+ SELECT q.id, q.status INTO v_id, v_status
32854
+ FROM public.logistics_plan_queue q
32855
+ WHERE q.order_id = v_target
32856
+ AND q.status IN ('pending', 'running', 'geblokkeerd')
32857
+ ORDER BY q.updated_at DESC
32858
+ LIMIT 1
32859
+ FOR UPDATE;
32860
+
32861
+ IF v_id IS NULL THEN
32862
+ BEGIN
32863
+ INSERT INTO public.logistics_plan_queue(order_id, reason) VALUES (v_target, p_reason);
32864
+ v_decision := 'nieuw aangemeld voor indeling';
32865
+ EXCEPTION WHEN unique_violation THEN
32866
+ v_decision := 'stond al in de wachtrij';
32867
+ END;
32868
+ ELSIF v_status = 'geblokkeerd' THEN
32869
+ UPDATE public.logistics_plan_queue
32870
+ SET status = 'pending', attempts = 0, reason = p_reason, reason_code = NULL,
32871
+ error = NULL, claimed_at = NULL, next_attempt_at = now(), updated_at = now()
32872
+ WHERE id = v_id;
32873
+ v_decision := 'blokkade opgeheven, opnieuw aangemeld';
32874
+ ELSE
32875
+ UPDATE public.logistics_plan_queue
32876
+ SET reason = p_reason, next_attempt_at = LEAST(next_attempt_at, now()), updated_at = now()
32877
+ WHERE id = v_id;
32878
+ v_decision := 'opnieuw aangemeld';
32879
+ END IF;
32880
+
32881
+ PERFORM public.logistics_trace_order_step(
32882
+ v_target, 'stap:orders_combineren', 'order aanmelden voor indeling', 'geslaagd',
32883
+ v_decision, p_reason, 'Karrenindeling');
32884
+
32885
+ PERFORM public.logistics_plan_kick();
32886
+ END;
32887
+ $function$
32888
+ ;
32889
+
32890
+ CREATE OR REPLACE FUNCTION public.logistics_sync_pick_queue_core()
32891
+ RETURNS jsonb
32892
+ LANGUAGE plpgsql
32893
+ SECURITY DEFINER
32894
+ SET search_path TO 'public'
32895
+ AS $function$
32896
+ DECLARE
32897
+ v_type_id uuid; v_station uuid; v_screen uuid; v_status uuid;
32898
+ v_inserted integer := 0; v_updated integer := 0; v_closed integer := 0; v_candidates integer := 0;
32899
+ v_today date; v_forced date; v_pickday date; v_target date; r_unit record; v_start timestamptz; v_due timestamptz;
32900
+ v_carts integer; v_percart boolean := public.logistics_pick_barcode_enabled();
32901
+ v_lines integer; v_breakdown jsonb; v_pickmode text; r_cart record; v_barcode text;
32902
+ v_cart_lines integer; v_fix jsonb; v_cart_payload jsonb; v_cart_rows integer;
32903
+ v_wait timestamptz; v_reason text; v_ok boolean;
32904
+ BEGIN
32905
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_sync_pick_queue_core');
32906
+ v_today := public.logistics_local_today();
32907
+ v_forced := NULLIF(current_setting('logistics.forced_pick_day', true), '')::date;
32908
+ SELECT id INTO v_type_id FROM public.pick_document_types WHERE code = 'pakbon' LIMIT 1;
32909
+ IF v_type_id IS NULL THEN RETURN jsonb_build_object('error', 'documenttype pakbon ontbreekt'); END IF;
32910
+ SELECT id INTO v_status FROM public.pick_document_statuses WHERE code = 'queued' LIMIT 1;
32911
+ v_pickmode := COALESCE(public.logistics_pick_mode()->>'pickMode', '');
32912
+
32913
+ v_station := public.logistics_document_station('pakbon');
32914
+ IF v_station IS NULL THEN
32915
+ SELECT s.station_id INTO v_station FROM public.pick_station_screens s WHERE s.name ILIKE '%pakbon%' ORDER BY s.created_at LIMIT 1;
32916
+ END IF;
32917
+ IF v_station IS NULL THEN SELECT id INTO v_station FROM public.pick_stations ORDER BY created_at LIMIT 1; END IF;
32918
+ SELECT s.id INTO v_screen FROM public.pick_station_screens s
32919
+ WHERE s.station_id = v_station
32920
+ AND (s.name ILIKE '%pakbon%' OR EXISTS (SELECT 1 FROM public.pick_screen_document_types d WHERE d.screen_id = s.id AND d.document_type_id = v_type_id))
32921
+ ORDER BY s.created_at LIMIT 1;
32922
+ IF v_screen IS NULL THEN
32923
+ SELECT s.id INTO v_screen FROM public.pick_station_screens s WHERE s.station_id = v_station ORDER BY created_at LIMIT 1;
32924
+ END IF;
32925
+
32926
+ -- Afsluiten in plaats van verwijderen.
32927
+ v_closed := public.logistics_prune_handled_pick_queue();
32928
+ -- Order geannuleerd of verwijderd: annuleren.
32929
+ v_closed := v_closed + public.logistics_pick_queue_close(ARRAY(
32930
+ SELECT q.id FROM public.pick_queue q
32931
+ WHERE q.cancelled_at IS NULL AND q.completed_at IS NULL
32932
+ AND NOT EXISTS (SELECT 1 FROM public.orders o WHERE o.order_number = q.order_number
32933
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.status, '') <> 'geannuleerd')),
32934
+ 'cancel', 'Order geannuleerd of verwijderd');
32935
+ -- Kar bestaat niet meer: vervallen door herindeling.
32936
+ v_closed := v_closed + public.logistics_pick_queue_close(ARRAY(
32937
+ SELECT q.id FROM public.pick_queue q
32938
+ WHERE q.cancelled_at IS NULL AND q.completed_at IS NULL AND q.printed_at IS NULL
32939
+ AND (q.payload->>'cartRowFor') IS NOT NULL
32940
+ AND NOT EXISTS (SELECT 1 FROM logistics_active.order_carts c WHERE c.id::text = q.reference)),
32941
+ 'cancel', 'Kar vervallen door herindeling');
32942
+ -- Pickmethode niet meer "per kar": karregels vervallen.
32943
+ IF v_pickmode <> 'cart' THEN
32944
+ v_closed := v_closed + public.logistics_pick_queue_close(ARRAY(
32945
+ SELECT q.id FROM public.pick_queue q
32946
+ WHERE q.cancelled_at IS NULL AND q.completed_at IS NULL AND q.printed_at IS NULL
32947
+ AND (q.payload->>'cartRowFor') IS NOT NULL),
32948
+ 'cancel', 'Kar vervallen: pickmethode is niet meer per kar');
32949
+ END IF;
32950
+
32951
+ SELECT min(d) INTO v_pickday FROM (
32952
+ SELECT public.logistics_order_pick_day(ord.order_number) AS d FROM public.orders ord
32953
+ WHERE ord.order_number IS NOT NULL
32954
+ AND public.logistics_order_is_topick(ord.id, ord.order_number)
32955
+ AND NOT public.logistics_order_handled(ord.id)
32956
+ AND public.logistics_order_released(ord.order_number)
32957
+ AND EXISTS (SELECT 1 FROM public.logistics_tasks lt WHERE lt.order_ref = ord.order_number AND lt.task_type = 'pick.execute'
32958
+ AND lt.status NOT IN ('done','cancelled','superseded','skipped'))
32959
+ ) s WHERE d >= v_today;
32960
+ IF v_forced IS NOT NULL THEN v_pickday := v_forced; END IF;
32961
+ v_target := v_pickday;
32962
+
32963
+ IF v_pickday IS NOT NULL THEN
32964
+ SELECT count(*) INTO v_candidates FROM public.orders ord
32965
+ WHERE ord.order_number IS NOT NULL
32966
+ AND public.logistics_order_is_topick(ord.id, ord.order_number)
32967
+ AND NOT public.logistics_order_handled(ord.id)
32968
+ AND (v_forced IS NOT NULL OR public.logistics_order_released(ord.order_number))
32969
+ AND public.logistics_order_pick_day(ord.order_number) = v_target
32970
+ AND EXISTS (SELECT 1 FROM public.logistics_tasks lt WHERE lt.order_ref = ord.order_number AND lt.task_type = 'pick.execute'
32971
+ AND lt.status NOT IN ('done','cancelled','superseded','skipped'));
32972
+ END IF;
32973
+
32974
+ IF v_pickday IS NULL OR COALESCE(v_candidates, 0) = 0 THEN
32975
+ RETURN jsonb_build_object('station', v_station, 'screen', v_screen, 'pickdag', v_pickday, 'toegevoegd', 0,
32976
+ 'afgesloten', v_closed, 'kandidaten', COALESCE(v_candidates, 0),
32977
+ 'per_kar', v_pickmode = 'cart', 'kleinsteTelt', v_percart, 'pickmethode', v_pickmode,
32978
+ 'guard', 'geen kandidaten');
32979
+ END IF;
32980
+
32981
+ -- 1.0.152: een geprinte regel blijft open tot scan, annulering of vervallen kar.
32982
+
32983
+ FOR r_unit IN
32984
+ WITH kandidaat AS (
32985
+ SELECT ord.id, ord.order_number, ord.customer_name, ord.delivery_date, ord.buffer_id, ord.custom_fields
32986
+ FROM public.orders ord
32987
+ WHERE ord.order_number IS NOT NULL
32988
+ AND public.logistics_order_is_topick(ord.id, ord.order_number)
32989
+ AND NOT public.logistics_order_handled(ord.id)
32990
+ AND (v_forced IS NOT NULL OR public.logistics_order_released(ord.order_number))
32991
+ AND public.logistics_order_pick_day(ord.order_number) = v_target
32992
+ AND EXISTS (SELECT 1 FROM public.logistics_tasks lt WHERE lt.order_ref = ord.order_number AND lt.task_type = 'pick.execute'
32993
+ AND lt.status NOT IN ('done','cancelled','superseded','skipped'))
32994
+ )
32995
+ SELECT min(k.order_number) AS order_number,
32996
+ (array_agg(k.id ORDER BY k.order_number))[1] AS lead_id,
32997
+ array_agg(k.order_number ORDER BY k.order_number) AS refs,
32998
+ array_agg(k.id ORDER BY k.order_number) AS ids,
32999
+ (array_agg(k.buffer_id ORDER BY k.order_number) FILTER (WHERE k.buffer_id IS NOT NULL))[1] AS buffer_id,
33000
+ count(*)::int AS leden,
33001
+ (array_agg(k.customer_name ORDER BY k.order_number))[1] AS customer_name,
33002
+ (array_agg(k.delivery_date ORDER BY k.order_number))[1] AS delivery_date,
33003
+ (array_agg(k.custom_fields ORDER BY k.order_number))[1] AS custom_fields
33004
+ FROM kandidaat k GROUP BY COALESCE(k.buffer_id::text, k.id::text) ORDER BY 1
33005
+ LOOP
33006
+ IF v_pickmode = 'cart' AND public.logistics_pakbon_old_order_row(r_unit.ids) THEN CONTINUE; END IF;
33007
+ SELECT min(lt.planned_start), max(COALESCE(lt.due_at, lt.planned_end)) INTO v_start, v_due
33008
+ FROM public.logistics_tasks lt WHERE lt.order_ref = ANY(r_unit.refs) AND lt.task_type IN ('print.packing_slip','pick.execute');
33009
+ SELECT count(DISTINCT c.id)::integer INTO v_carts FROM logistics_active.order_carts c
33010
+ WHERE c.order_id = ANY(r_unit.ids) OR (r_unit.buffer_id IS NOT NULL AND r_unit.leden > 1 AND c.buffer_id = r_unit.buffer_id AND c.order_id IS NULL);
33011
+ SELECT count(*) INTO v_lines FROM logistics_active.order_items oi WHERE oi.order_id = ANY(r_unit.ids);
33012
+ v_breakdown := public.pick_cart_breakdown(r_unit.lead_id);
33013
+
33014
+ IF v_pickmode = 'cart' THEN
33015
+ v_cart_rows := 0;
33016
+ FOR r_cart IN
33017
+ SELECT DISTINCT c.id AS cart_id, c.cart_number FROM logistics_active.order_carts c
33018
+ WHERE c.order_id = ANY(r_unit.ids)
33019
+ OR (r_unit.buffer_id IS NOT NULL AND r_unit.leden > 1 AND c.buffer_id = r_unit.buffer_id AND c.order_id IS NULL)
33020
+ OR EXISTS (SELECT 1 FROM logistics_active.order_cart_items ci JOIN logistics_active.order_items oi ON oi.id = ci.order_item_id
33021
+ WHERE ci.cart_id = c.id AND oi.order_id = ANY(r_unit.ids))
33022
+ ORDER BY c.cart_number
33023
+ LOOP
33024
+ -- Kar al gescand: niets nieuws aanmaken.
33025
+ IF public.logistics_cart_scanned(r_cart.cart_id) THEN CONTINUE; END IF;
33026
+ v_ok := v_forced IS NOT NULL OR public.logistics_cart_released(r_cart.cart_id);
33027
+ v_reason := CASE WHEN v_ok THEN NULL ELSE public.logistics_cart_release_reason(r_cart.cart_id) END;
33028
+ v_wait := CASE WHEN v_ok THEN NULL ELSE public.logistics_cart_wait_until(r_cart.cart_id) END;
33029
+
33030
+ v_barcode := public.logistics_ensure_cart_barcode(r_cart.cart_id);
33031
+ v_fix := public.logistics_cart_fixed_numbering(r_cart.cart_id);
33032
+ SELECT count(*) INTO v_cart_lines FROM logistics_active.order_cart_items ci WHERE ci.cart_id = r_cart.cart_id;
33033
+
33034
+ v_cart_payload := jsonb_build_object(
33035
+ 'source','logistiek-engine','orderId', r_unit.lead_id, 'documentType','pakbon',
33036
+ 'pickDate', v_target, 'perCart', true, 'pickMode', 'cart',
33037
+ 'cartRowFor', r_unit.order_number, 'cartId', r_cart.cart_id,
33038
+ 'isFullCart', public.logistics_cart_is_full(r_cart.cart_id),
33039
+ 'barcode', v_barcode, 'cartCount', v_fix->'cartTotal', 'totalCarts', v_fix->'cartTotal',
33040
+ 'bufferId', r_unit.buffer_id, 'isBuffer', r_unit.leden > 1,
33041
+ 'bufferOrders', to_jsonb(r_unit.refs), 'orderIds', to_jsonb(r_unit.ids),
33042
+ 'printbaar', v_ok, 'wachtTot', v_wait, 'wachtReden', v_reason)
33043
+ || COALESCE(v_breakdown, '{}'::jsonb) || v_fix;
33044
+
33045
+ IF EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.reference = r_cart.cart_id::text
33046
+ AND q.document_type_id = v_type_id AND q.cancelled_at IS NULL) THEN
33047
+ UPDATE public.pick_queue q
33048
+ SET screen_id = COALESCE(v_screen, q.screen_id), station_id = COALESCE(v_station, q.station_id),
33049
+ customer_name = r_unit.customer_name, order_number = r_unit.order_number,
33050
+ document_number = 'PB-' || r_unit.order_number || '-K' || (v_fix->>'cartSeq'),
33051
+ line_count = v_cart_lines, cart_count = 1,
33052
+ due_at = COALESCE(v_wait, v_due, q.due_at),
33053
+ payload = COALESCE(q.payload, '{}'::jsonb) || v_cart_payload, updated_at = now()
33054
+ WHERE q.reference = r_cart.cart_id::text AND q.document_type_id = v_type_id AND q.cancelled_at IS NULL
33055
+ AND (q.screen_id, q.station_id, q.customer_name, q.order_number, q.document_number, q.line_count, q.cart_count, q.due_at, q.payload)
33056
+ IS DISTINCT FROM
33057
+ (COALESCE(v_screen, q.screen_id), COALESCE(v_station, q.station_id), r_unit.customer_name, r_unit.order_number,
33058
+ 'PB-' || r_unit.order_number || '-K' || (v_fix->>'cartSeq'), v_cart_lines, 1,
33059
+ COALESCE(v_wait, v_due, q.due_at), COALESCE(q.payload, '{}'::jsonb) || v_cart_payload);
33060
+ IF FOUND THEN v_updated := v_updated + 1; END IF;
33061
+ ELSE
33062
+ INSERT INTO public.pick_queue (station_id, screen_id, document_type_id, status_id, document_number, order_number, reference,
33063
+ customer_name, delivery_address, line_count, cart_count, priority, announced_at, due_at, payload)
33064
+ VALUES (v_station, v_screen, v_type_id, v_status,
33065
+ 'PB-' || r_unit.order_number || '-K' || (v_fix->>'cartSeq'), r_unit.order_number, r_cart.cart_id::text,
33066
+ r_unit.customer_name, NULLIF(r_unit.custom_fields ->> 'delivery_address', ''),
33067
+ v_cart_lines, 1, 30, now(), COALESCE(v_wait, v_due, (r_unit.delivery_date)::timestamptz), v_cart_payload)
33068
+ ON CONFLICT DO NOTHING;
33069
+ v_inserted := v_inserted + 1;
33070
+ END IF;
33071
+ v_cart_rows := v_cart_rows + 1;
33072
+ END LOOP;
33073
+
33074
+ IF v_cart_rows > 0 THEN
33075
+ v_closed := v_closed + public.logistics_pick_queue_close(ARRAY(
33076
+ SELECT q.id FROM public.pick_queue q
33077
+ WHERE q.order_number = ANY(r_unit.refs) AND q.document_type_id = v_type_id
33078
+ AND (q.payload->>'cartRowFor') IS NULL AND q.printed_at IS NULL
33079
+ AND q.cancelled_at IS NULL AND q.completed_at IS NULL),
33080
+ 'cancel', 'Orderregel vervangen door één regel per kar');
33081
+ END IF;
33082
+ -- Bij picken per kar nooit terugvallen op een orderregel.
33083
+ CONTINUE;
33084
+ END IF;
33085
+
33086
+ IF EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.order_number = r_unit.order_number AND q.document_type_id = v_type_id
33087
+ AND (q.payload ->> 'cartRowFor') IS NULL AND q.cancelled_at IS NULL) THEN
33088
+ UPDATE public.pick_queue q
33089
+ SET screen_id = COALESCE(v_screen, q.screen_id), station_id = COALESCE(v_station, q.station_id),
33090
+ customer_name = r_unit.customer_name, document_number = 'PB-' || r_unit.order_number,
33091
+ line_count = v_lines, cart_count = COALESCE(v_carts, 0), due_at = COALESCE(v_due, q.due_at),
33092
+ payload = COALESCE(q.payload, '{}'::jsonb)
33093
+ || jsonb_build_object('source','logistiek-engine','orderId', r_unit.lead_id, 'documentType','pakbon',
33094
+ 'pickDate', v_target, 'perCart', v_percart, 'cartCount', COALESCE(v_carts, 0),
33095
+ 'bufferId', r_unit.buffer_id, 'isBuffer', r_unit.leden > 1,
33096
+ 'bufferOrders', to_jsonb(r_unit.refs), 'orderIds', to_jsonb(r_unit.ids))
33097
+ || COALESCE(v_breakdown, '{}'::jsonb),
33098
+ updated_at = now()
33099
+ WHERE q.order_number = r_unit.order_number AND q.document_type_id = v_type_id
33100
+ AND (q.payload ->> 'cartRowFor') IS NULL AND q.cancelled_at IS NULL
33101
+ AND (q.screen_id, q.station_id, q.customer_name, q.document_number, q.line_count, q.cart_count, q.due_at, q.payload)
33102
+ IS DISTINCT FROM
33103
+ (COALESCE(v_screen, q.screen_id), COALESCE(v_station, q.station_id), r_unit.customer_name, 'PB-' || r_unit.order_number,
33104
+ v_lines, COALESCE(v_carts, 0), COALESCE(v_due, q.due_at),
33105
+ COALESCE(q.payload, '{}'::jsonb)
33106
+ || jsonb_build_object('source','logistiek-engine','orderId', r_unit.lead_id, 'documentType','pakbon',
33107
+ 'pickDate', v_target, 'perCart', v_percart, 'cartCount', COALESCE(v_carts, 0),
33108
+ 'bufferId', r_unit.buffer_id, 'isBuffer', r_unit.leden > 1,
33109
+ 'bufferOrders', to_jsonb(r_unit.refs), 'orderIds', to_jsonb(r_unit.ids))
33110
+ || COALESCE(v_breakdown, '{}'::jsonb));
33111
+ IF FOUND THEN v_updated := v_updated + 1; END IF;
33112
+ CONTINUE;
33113
+ END IF;
33114
+
33115
+ INSERT INTO public.pick_queue (station_id, screen_id, document_type_id, status_id, document_number, order_number, reference,
33116
+ customer_name, delivery_address, line_count, cart_count, priority, announced_at, due_at, payload)
33117
+ VALUES (v_station, v_screen, v_type_id, v_status, 'PB-' || r_unit.order_number, r_unit.order_number, r_unit.order_number,
33118
+ r_unit.customer_name, NULLIF(r_unit.custom_fields ->> 'delivery_address', ''), v_lines, COALESCE(v_carts, 0), 30, now(),
33119
+ COALESCE(v_due, (r_unit.delivery_date)::timestamptz),
33120
+ jsonb_build_object('source','logistiek-engine','orderId', r_unit.lead_id, 'documentType','pakbon', 'pickDate', v_target,
33121
+ 'perCart', v_percart, 'cartCount', COALESCE(v_carts, 0), 'bufferId', r_unit.buffer_id,
33122
+ 'isBuffer', r_unit.leden > 1, 'bufferOrders', to_jsonb(r_unit.refs), 'orderIds', to_jsonb(r_unit.ids))
33123
+ || COALESCE(v_breakdown, '{}'::jsonb))
33124
+ ON CONFLICT DO NOTHING;
33125
+ v_inserted := v_inserted + 1;
33126
+ END LOOP;
33127
+
33128
+ RETURN jsonb_build_object('station', v_station, 'screen', v_screen, 'pickdag', v_pickday,
33129
+ 'kandidaten', v_candidates, 'per_kar', v_pickmode = 'cart', 'kleinsteTelt', v_percart, 'pickmethode', v_pickmode,
33130
+ 'toegevoegd', v_inserted, 'bijgewerkt', v_updated, 'afgesloten', v_closed);
33131
+ END;
33132
+ $function$
33133
+ ;
33134
+
33135
+ CREATE OR REPLACE FUNCTION public.logistics_sync_pick_queue_backlog()
33136
+ RETURNS jsonb
33137
+ LANGUAGE plpgsql
33138
+ SECURITY DEFINER
33139
+ SET search_path TO 'public'
33140
+ AS $function$
33141
+ DECLARE
33142
+ v_max integer := public.logistics_pick_backlog_max_days();
33143
+ v_type_id uuid; v_status uuid; v_station uuid; v_screen uuid; v_pickmode text;
33144
+ r_unit record; r_cart record; v_barcode text; v_seq integer; v_total integer;
33145
+ v_lines integer; v_carts integer; v_payload jsonb; v_breakdown jsonb;
33146
+ v_ins integer := 0; v_upd integer := 0; v_orders integer := 0;
33147
+ BEGIN
33148
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_sync_pick_queue_backlog');
33149
+ IF v_max IS NULL THEN RETURN jsonb_build_object('achterstand', 'uit'); END IF;
33150
+ v_pickmode := COALESCE(public.logistics_pick_mode()->>'pickMode', '');
33151
+ IF v_pickmode <> 'cart' THEN
33152
+ RETURN jsonb_build_object('achterstand', 'overgeslagen', 'reden', 'alleen bij picken per kar');
33153
+ END IF;
33154
+ SELECT id INTO v_type_id FROM public.pick_document_types WHERE code = 'pakbon' LIMIT 1;
33155
+ SELECT id INTO v_status FROM public.pick_document_statuses WHERE code = 'queued' LIMIT 1;
33156
+ IF v_type_id IS NULL THEN RETURN jsonb_build_object('achterstand', 'fout', 'reden', 'documenttype pakbon ontbreekt'); END IF;
33157
+ v_station := public.logistics_document_station('pakbon');
33158
+ IF v_station IS NULL THEN
33159
+ SELECT s.station_id INTO v_station FROM public.pick_station_screens s WHERE s.name ILIKE '%pakbon%' ORDER BY s.created_at LIMIT 1;
33160
+ END IF;
33161
+ IF v_station IS NULL THEN SELECT id INTO v_station FROM public.pick_stations ORDER BY created_at LIMIT 1; END IF;
33162
+ SELECT s.id INTO v_screen FROM public.pick_station_screens s
33163
+ WHERE s.station_id = v_station
33164
+ AND (s.name ILIKE '%pakbon%' OR EXISTS (SELECT 1 FROM public.pick_screen_document_types d WHERE d.screen_id = s.id AND d.document_type_id = v_type_id))
33165
+ ORDER BY s.created_at LIMIT 1;
33166
+
33167
+ FOR r_unit IN
33168
+ WITH k AS (
33169
+ SELECT o.id, o.order_number, o.customer_name, o.delivery_date, o.buffer_id, o.custom_fields
33170
+ FROM public.orders o WHERE o.order_number IS NOT NULL AND public.logistics_order_is_backlog(o.id))
33171
+ SELECT min(k.order_number) AS order_number,
33172
+ (array_agg(k.id ORDER BY k.order_number))[1] AS lead_id,
33173
+ array_agg(k.order_number ORDER BY k.order_number) AS refs,
33174
+ array_agg(k.id ORDER BY k.order_number) AS ids,
33175
+ (array_agg(k.buffer_id ORDER BY k.order_number) FILTER (WHERE k.buffer_id IS NOT NULL))[1] AS buffer_id,
33176
+ count(*)::int AS leden,
33177
+ (array_agg(k.customer_name ORDER BY k.order_number))[1] AS customer_name,
33178
+ min((k.delivery_date::text)::date) AS delivery_date,
33179
+ (array_agg(k.custom_fields ORDER BY k.order_number))[1] AS custom_fields
33180
+ FROM k GROUP BY COALESCE(k.buffer_id::text, k.id::text) ORDER BY 1
33181
+ LOOP
33182
+ IF public.logistics_pakbon_old_order_row(r_unit.ids) THEN CONTINUE; END IF;
33183
+ v_orders := v_orders + 1;
33184
+ SELECT count(DISTINCT c.id)::int INTO v_carts FROM logistics_active.order_carts c
33185
+ WHERE c.order_id = ANY(r_unit.ids) OR (r_unit.buffer_id IS NOT NULL AND c.buffer_id = r_unit.buffer_id);
33186
+ v_breakdown := public.pick_cart_breakdown(r_unit.lead_id);
33187
+ FOR r_cart IN
33188
+ SELECT DISTINCT c.id AS cart_id, c.cart_number, c.total_carts FROM logistics_active.order_carts c
33189
+ WHERE c.order_id = ANY(r_unit.ids) OR (r_unit.buffer_id IS NOT NULL AND c.buffer_id = r_unit.buffer_id)
33190
+ ORDER BY c.cart_number
33191
+ LOOP
33192
+ IF NOT public.logistics_cart_released(r_cart.cart_id) THEN CONTINUE; END IF;
33193
+ v_barcode := public.logistics_ensure_cart_barcode(r_cart.cart_id);
33194
+ v_seq := NULL; v_total := NULL;
33195
+ SELECT s.cart_seq, s.cart_total INTO v_seq, v_total FROM public.logistics_cart_seq(r_unit.lead_id) s WHERE s.cart_id = r_cart.cart_id;
33196
+ v_seq := COALESCE(v_seq, r_cart.cart_number);
33197
+ v_total := COALESCE(v_total, r_cart.total_carts, v_carts);
33198
+ SELECT count(*) INTO v_lines FROM logistics_active.order_cart_items ci WHERE ci.cart_id = r_cart.cart_id;
33199
+ v_payload := jsonb_build_object(
33200
+ 'source','logistiek-engine','orderId', r_unit.lead_id, 'documentType','pakbon',
33201
+ 'pickDate', r_unit.delivery_date, 'perCart', true, 'pickMode', 'cart',
33202
+ 'cartRowFor', r_unit.order_number, 'cartId', r_cart.cart_id,
33203
+ 'cartSeq', v_seq, 'cartIndex', r_cart.cart_number, 'cartTotal', v_total,
33204
+ 'cartLabel', 'Kar ' || v_seq || ' van ' || COALESCE(v_total, v_seq),
33205
+ 'isFullCart', public.logistics_cart_is_full(r_cart.cart_id),
33206
+ 'barcode', v_barcode, 'cartCount', COALESCE(v_carts, 0),
33207
+ 'bufferId', r_unit.buffer_id, 'isBuffer', r_unit.leden > 1,
33208
+ 'bufferOrders', to_jsonb(r_unit.refs), 'orderIds', to_jsonb(r_unit.ids),
33209
+ 'backlog', true,
33210
+ 'backlogDays', (now() AT TIME ZONE public.logistics_tz())::date - r_unit.delivery_date,
33211
+ 'backlogLabel', 'Achterstand, leverdatum ' || to_char(r_unit.delivery_date, 'DD-MM'))
33212
+ || COALESCE(v_breakdown, '{}'::jsonb);
33213
+ IF EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.reference = r_cart.cart_id::text AND q.document_type_id = v_type_id AND q.cancelled_at IS NULL) THEN
33214
+ UPDATE public.pick_queue q
33215
+ SET customer_name = r_unit.customer_name, order_number = r_unit.order_number,
33216
+ document_number = 'PB-' || r_unit.order_number || '-K' || v_seq,
33217
+ line_count = v_lines, cart_count = 1, is_urgent = true,
33218
+ payload = COALESCE(q.payload, '{}'::jsonb) || v_payload, updated_at = now()
33219
+ WHERE q.reference = r_cart.cart_id::text AND q.document_type_id = v_type_id AND q.cancelled_at IS NULL
33220
+ AND q.printed_at IS NULL AND q.completed_at IS NULL
33221
+ AND (q.customer_name, q.order_number, q.document_number, q.line_count, q.cart_count, q.is_urgent, q.payload)
33222
+ IS DISTINCT FROM
33223
+ (r_unit.customer_name, r_unit.order_number, 'PB-' || r_unit.order_number || '-K' || v_seq, v_lines, 1, true,
33224
+ COALESCE(q.payload, '{}'::jsonb) || v_payload);
33225
+ IF FOUND THEN v_upd := v_upd + 1; END IF;
33226
+ ELSE
33227
+ INSERT INTO public.pick_queue (
33228
+ station_id, screen_id, document_type_id, status_id, document_number, order_number, reference,
33229
+ customer_name, delivery_address, line_count, cart_count, priority, is_urgent, announced_at, due_at, payload
33230
+ ) VALUES (
33231
+ v_station, v_screen, v_type_id, v_status,
33232
+ 'PB-' || r_unit.order_number || '-K' || v_seq, r_unit.order_number, r_cart.cart_id::text,
33233
+ r_unit.customer_name, NULLIF(r_unit.custom_fields ->> 'delivery_address', ''),
33234
+ v_lines, 1, 30, true, now(), (r_unit.delivery_date)::timestamptz, v_payload
33235
+ ) ON CONFLICT DO NOTHING;
33236
+ v_ins := v_ins + 1;
33237
+ END IF;
33238
+ END LOOP;
33239
+ END LOOP;
33240
+ RETURN jsonb_build_object('achterstand', 'aan', 'maxDagen', v_max, 'eenheden', v_orders, 'toegevoegd', v_ins, 'bijgewerkt', v_upd);
33241
+ END $function$
33242
+ ;
33243
+
33244
+ CREATE OR REPLACE FUNCTION public.logistics_order_frozen(p_order_id uuid)
33245
+ RETURNS boolean
33246
+ LANGUAGE plpgsql
33247
+ STABLE SECURITY DEFINER
33248
+ SET search_path TO 'public'
33249
+ AS $function$
33250
+ DECLARE v_number text;
33251
+ BEGIN
33252
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_frozen');
33253
+ SELECT o.order_number INTO v_number FROM public.orders o WHERE o.id = p_order_id;
33254
+ IF v_number IS NULL THEN RETURN false; END IF;
33255
+ IF EXISTS (SELECT 1 FROM public.logistics_order_plan_exclusion x WHERE x.order_id = p_order_id) THEN RETURN true; END IF;
33256
+ IF public.logistics_order_afgemeld(p_order_id) THEN RETURN true; END IF;
33257
+ IF EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.order_number = v_number AND q.cancelled_at IS NULL
33258
+ AND (q.printed_at IS NOT NULL OR q.completed_at IS NOT NULL)) THEN RETURN true; END IF;
33259
+ IF EXISTS (SELECT 1 FROM public.cart_control_items ci WHERE ci.order_number = v_number) THEN RETURN true; END IF;
33260
+ IF public.logistics_order_print_actief(v_number) THEN RETURN true; END IF;
33261
+ IF public.logistics_order_print_actief(v_number) THEN RETURN true; END IF;
33262
+ RETURN false;
33263
+ END;
33264
+ $function$
33265
+ ;
33266
+
33267
+ CREATE OR REPLACE FUNCTION public.logistics_order_mergeable(p_order_id uuid)
33268
+ RETURNS boolean
33269
+ LANGUAGE plpgsql
33270
+ STABLE SECURITY DEFINER
33271
+ SET search_path TO 'public'
33272
+ AS $function$
33273
+ DECLARE
33274
+ v_ok boolean;
33275
+ BEGIN
33276
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_mergeable');
33277
+ SELECT (o.deleted_at IS NULL
33278
+ AND o.cancelled_at IS NULL
33279
+ AND o.plan_locked_at IS NULL
33280
+ AND COALESCE(o.is_reservation, false) = false
33281
+ AND o.order_source = 'floriday'
33282
+ AND o.floriday_order_group_key IS NOT NULL
33283
+ AND NOT EXISTS (
33284
+ SELECT 1
33285
+ FROM public.pick_queue q
33286
+ WHERE q.order_number = o.order_number
33287
+ AND (q.printed_at IS NOT NULL OR q.completed_at IS NOT NULL)
33288
+ )
33289
+ AND NOT public.logistics_order_print_actief(o.order_number))
33290
+ INTO v_ok
33291
+ FROM public.orders o
33292
+ WHERE o.id = p_order_id;
33293
+
33294
+ RETURN COALESCE(v_ok, false);
33295
+ EXCEPTION WHEN OTHERS THEN
33296
+ RETURN false;
33297
+ END $function$
33298
+ ;
33299
+
33300
+ CREATE OR REPLACE FUNCTION public.logistics_merge_partial_carts(p_limit integer DEFAULT 50)
33301
+ RETURNS jsonb
33302
+ LANGUAGE plpgsql
33303
+ SECURITY DEFINER
33304
+ SET search_path TO 'public'
33305
+ AS $function$
33306
+ declare r record; v_count integer := 0;
33307
+ begin
33308
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_merge_partial_carts');
33309
+ if coalesce(public.logistics_pick_mode()->>'pickMode','') <> 'cart' then
33310
+ return jsonb_build_object('overgeslagen', 'pickmethode is niet per kar');
33311
+ end if;
33312
+ if public.logistics_pick_partial_mode() <> 'after_checkin' then
33313
+ return jsonb_build_object('overgeslagen', 'deelkarren mogen ook voor het aanmeldmoment');
33314
+ end if;
33315
+
33316
+ for r in
33317
+ select distinct o.id as order_id
33318
+ from logistics_active.order_carts c
33319
+ join lateral unnest(public.logistics_cart_order_refs(c.id)) ref on true
33320
+ join public.orders o on o.order_number = ref
33321
+ where not public.logistics_cart_is_full(c.id)
33322
+ and not exists (select 1 from public.pick_queue q
33323
+ where q.reference = c.id::text and q.printed_at is not null)
33324
+ and not public.logistics_order_print_actief(o.order_number)
33325
+ and not public.logistics_order_print_actief(o.order_number)
33326
+ and public.logistics_cart_checkin_at(c.id) is not null
33327
+ limit greatest(coalesce(p_limit, 50), 1)
33328
+ loop
33329
+ begin
33330
+ perform public.enqueue_cart_replan_if_unlocked(r.order_id);
33331
+ v_count := v_count + 1;
33332
+ exception when others then null;
33333
+ end;
33334
+ end loop;
33335
+
33336
+ return jsonb_build_object('opnieuw_ingedeeld', v_count);
33337
+ end; $function$
33338
+ ;
33339
+
33340
+ CREATE OR REPLACE FUNCTION public.logistics_order_is_frozen(p_order_id uuid)
33341
+ RETURNS boolean
33342
+ LANGUAGE sql
33343
+ STABLE SECURITY DEFINER
33344
+ SET search_path TO 'public'
33345
+ AS $function$
33346
+ SELECT public.logistics_acc_guard('ingelogd', 'logistics_order_is_frozen');
33347
+ SELECT EXISTS (
33348
+ SELECT 1 FROM public.pick_queue p
33349
+ WHERE p.cancelled_at IS NULL
33350
+ AND (p.printed_at IS NOT NULL OR p.completed_at IS NOT NULL
33351
+ OR jsonb_array_length(CASE WHEN jsonb_typeof(p.printed_cart_ids) = 'array' THEN p.printed_cart_ids ELSE '[]'::jsonb END) > 0)
33352
+ AND (p.payload->>'orderId' = p_order_id::text
33353
+ OR COALESCE(p.payload->'orderIds', '[]'::jsonb) ? p_order_id::text)
33354
+ )
33355
+ OR EXISTS (
33356
+ SELECT 1 FROM logistics_active.order_carts oc
33357
+ WHERE oc.order_id = p_order_id AND public.logistics_cart_is_frozen(oc.id)
33358
+ )
33359
+ OR public.logistics_order_print_actief((SELECT o.order_number FROM public.orders o WHERE o.id = p_order_id))
33360
+ OR public.logistics_order_print_actief((SELECT o.order_number FROM public.orders o WHERE o.id = p_order_id))
33361
+ OR EXISTS (
33362
+ SELECT 1 FROM logistics_active.order_carts oc
33363
+ JOIN logistics_active.order_cart_items oci ON oci.cart_id = oc.id
33364
+ JOIN public.order_items oi ON oi.id = oci.order_item_id
33365
+ WHERE oc.order_id IS NULL AND oi.order_id = p_order_id
33366
+ AND public.logistics_cart_is_frozen(oc.id)
33367
+ )
33368
+ $function$
33369
+ ;
33370
+
33371
+ CREATE OR REPLACE FUNCTION public.logistics_order_on_frozen_shared_cart(p_order_id uuid)
33372
+ RETURNS boolean
33373
+ LANGUAGE sql
33374
+ STABLE SECURITY DEFINER
33375
+ SET search_path TO 'public'
33376
+ AS $function$
33377
+ SELECT public.logistics_acc_guard('server', 'logistics_order_on_frozen_shared_cart');
33378
+ SELECT EXISTS (
33379
+ SELECT 1 FROM public.logistics_shared_cart_peers(p_order_id) p
33380
+ JOIN logistics_active.order_carts c ON c.id = p.cart_id
33381
+ LEFT JOIN public.orders po ON po.id = p.peer_order_id
33382
+ WHERE c.pickup_locked IS TRUE OR c.is_complete IS TRUE
33383
+ OR public.logistics_cart_handled(COALESCE(po.order_number, p.peer_order_id::text), p.cart_number)
33384
+ OR public.logistics_order_afgemeld(p.peer_order_id)
33385
+ OR public.logistics_order_print_actief(po.order_number)
33386
+ OR public.logistics_order_print_actief(po.order_number)
33387
+ OR public.logistics_cart_is_frozen(p.cart_id)
33388
+ OR EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.cancelled_at IS NULL
33389
+ AND q.payload->>'cartId' = p.cart_id::text
33390
+ AND (q.printed_at IS NOT NULL OR q.completed_at IS NOT NULL)))
33391
+ $function$
33392
+ ;
33393
+
33394
+ CREATE OR REPLACE FUNCTION public.logistics_tick()
33395
+ RETURNS jsonb
33396
+ LANGUAGE plpgsql
33397
+ SECURITY DEFINER
33398
+ SET search_path TO 'public'
33399
+ AS $function$
33400
+ DECLARE v_result jsonb := '{}'::jsonb; v_start timestamptz := clock_timestamp(); v_t timestamptz; v_err boolean := false;
33401
+ BEGIN
33402
+ PERFORM public.logistics_acc_guard('server', 'logistics_tick');
33403
+ v_t := clock_timestamp();
33404
+ BEGIN
33405
+ PERFORM set_config('statement_timeout', '10000', true);
33406
+ v_result := v_result || jsonb_build_object('vervoerder', public.logistics_carrier_recalc_run(200));
33407
+ EXCEPTION WHEN OTHERS THEN
33408
+ v_err := true; v_result := v_result || jsonb_build_object('vervoerder', left(SQLERRM, 200));
33409
+ END;
33410
+ v_result := v_result || jsonb_build_object('vervoerder_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33411
+
33412
+ v_t := clock_timestamp();
33413
+ BEGIN
33414
+ PERFORM set_config('statement_timeout', '15000', true);
33415
+ PERFORM public.logistics_rebuffer_pending();
33416
+ v_result := v_result || jsonb_build_object('rebuffer', 'ok');
33417
+ EXCEPTION WHEN OTHERS THEN
33418
+ v_err := true;
33419
+ IF SQLERRM ILIKE '%tijdzone%' THEN
33420
+ v_result := v_result || jsonb_build_object('rebuffer', 'overgeslagen: tijdzone ontbreekt in Werktijden');
33421
+ BEGIN
33422
+ UPDATE public.logistics_rebuffer_state SET last_error = 'Herindelen overgeslagen: de tijdzone ontbreekt in Werktijden.', updated_at = now() WHERE id;
33423
+ EXCEPTION WHEN OTHERS THEN NULL; END;
33424
+ ELSE
33425
+ v_result := v_result || jsonb_build_object('rebuffer', left(SQLERRM, 200));
33426
+ END IF;
33427
+ END;
33428
+ v_result := v_result || jsonb_build_object('rebuffer_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33429
+
33430
+ v_t := clock_timestamp();
33431
+ BEGIN
33432
+ PERFORM set_config('statement_timeout', '20000', true);
33433
+ PERFORM public.run_logistics_tasks_dispatch();
33434
+ v_result := v_result || jsonb_build_object('taken', 'ok');
33435
+ EXCEPTION WHEN OTHERS THEN
33436
+ v_err := true; v_result := v_result || jsonb_build_object('taken', left(SQLERRM, 200));
33437
+ END;
33438
+ v_result := v_result || jsonb_build_object('taken_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33439
+
33440
+ v_t := clock_timestamp();
33441
+ BEGIN
33442
+ PERFORM set_config('statement_timeout', '20000', true);
33443
+ v_result := v_result || jsonb_build_object('afgerond', public.logistics_order_finalize_run(25, 120));
33444
+ EXCEPTION WHEN OTHERS THEN
33445
+ v_err := true; v_result := v_result || jsonb_build_object('afgerond', left(SQLERRM, 200));
33446
+ END;
33447
+ v_result := v_result || jsonb_build_object('afgerond_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33448
+
33449
+ -- 1.0.194: lichte ronde — pickwachtrij en laadtoestand in SQL, niet meer via HTTP.
33450
+ v_t := clock_timestamp();
33451
+ BEGIN
33452
+ PERFORM set_config('statement_timeout', '15000', true);
33453
+ v_result := v_result || jsonb_build_object('pickwachtrij', public.logistics_sync_pick_queue());
33454
+ EXCEPTION WHEN OTHERS THEN
33455
+ v_err := true; v_result := v_result || jsonb_build_object('pickwachtrij', left(SQLERRM, 200));
33456
+ END;
33457
+ v_result := v_result || jsonb_build_object('pickwachtrij_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33458
+
33459
+ v_t := clock_timestamp();
33460
+ BEGIN
33461
+ PERFORM set_config('statement_timeout', '10000', true);
33462
+ v_result := v_result || jsonb_build_object('laadtoestand', public.logistics_refresh_plan_state_als_nodig());
33463
+ EXCEPTION WHEN OTHERS THEN
33464
+ v_err := true; v_result := v_result || jsonb_build_object('laadtoestand', left(SQLERRM, 200));
33465
+ END;
33466
+ v_result := v_result || jsonb_build_object('laadtoestand_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33467
+
33468
+ v_t := clock_timestamp();
33469
+ BEGIN
33470
+ PERFORM set_config('statement_timeout', '10000', true);
33471
+ v_result := v_result || jsonb_build_object('instellingen', public.logistics_ensure_periodiek());
33472
+ EXCEPTION WHEN OTHERS THEN
33473
+ v_err := true; v_result := v_result || jsonb_build_object('instellingen', left(SQLERRM, 200));
33474
+ END;
33475
+ v_result := v_result || jsonb_build_object('instellingen_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33476
+
33477
+ -- 1.0.194: lichte ronde — pickwachtrij en laadtoestand in SQL, niet meer via HTTP.
33478
+ v_t := clock_timestamp();
33479
+ BEGIN
33480
+ PERFORM set_config('statement_timeout', '15000', true);
33481
+ v_result := v_result || jsonb_build_object('pickwachtrij', public.logistics_sync_pick_queue());
33482
+ EXCEPTION WHEN OTHERS THEN
33483
+ v_err := true; v_result := v_result || jsonb_build_object('pickwachtrij', left(SQLERRM, 200));
33484
+ END;
33485
+ v_result := v_result || jsonb_build_object('pickwachtrij_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33486
+
33487
+ v_t := clock_timestamp();
33488
+ BEGIN
33489
+ PERFORM set_config('statement_timeout', '10000', true);
33490
+ v_result := v_result || jsonb_build_object('laadtoestand', public.logistics_refresh_plan_state_als_nodig());
33491
+ EXCEPTION WHEN OTHERS THEN
33492
+ v_err := true; v_result := v_result || jsonb_build_object('laadtoestand', left(SQLERRM, 200));
33493
+ END;
33494
+ v_result := v_result || jsonb_build_object('laadtoestand_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33495
+
33496
+ v_t := clock_timestamp();
33497
+ BEGIN
33498
+ PERFORM set_config('statement_timeout', '10000', true);
33499
+ v_result := v_result || jsonb_build_object('instellingen', public.logistics_ensure_periodiek());
33500
+ EXCEPTION WHEN OTHERS THEN
33501
+ v_err := true; v_result := v_result || jsonb_build_object('instellingen', left(SQLERRM, 200));
33502
+ END;
33503
+ v_result := v_result || jsonb_build_object('instellingen_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33504
+
33505
+ BEGIN
33506
+ INSERT INTO public.logistics_tick_runs(started_at, finished_at, duration_ms, has_error, steps)
33507
+ VALUES (v_start, clock_timestamp(), (extract(epoch from clock_timestamp()-v_start)*1000)::int, v_err, v_result);
33508
+ EXCEPTION WHEN OTHERS THEN NULL;
33509
+ END;
33510
+ PERFORM set_config('statement_timeout', '30000', true);
33511
+ RETURN v_result;
33512
+ END $function$
33513
+ ;
33514
+
33515
+ CREATE OR REPLACE FUNCTION public.logistics_install_check()
33516
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
33517
+ LANGUAGE plpgsql
33518
+ SECURITY DEFINER
33519
+ SET search_path TO 'public'
33520
+ AS $function$
33521
+ DECLARE c text; f text; t text; v_fav text; n bigint; s record;
33522
+ BEGIN
33523
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_install_check');
33524
+ FOREACH f IN ARRAY ARRAY[
33525
+ 'f:logistics_try_uuid','f:logistics_tz','f:logistics_is_beheer','f:logistics_buffer_payload','f:logistics_apply_order_buffer',
33526
+ 'f:logistics_apply_buffer_decision','f:logistics_rebuffer_pending','f:logistics_departure_calc','f:logistics_own_transport_departure',
33527
+ 'f:logistics_order_departure','f:logistics_brief_transport','f:logistics_order_frozen','f:logistics_order_is_frozen',
33528
+ 'f:logistics_order_finalize_run','f:run_logistics_tasks_dispatch','f:enqueue_cart_replan_if_unlocked','f:logistics_tick',
33529
+ 't:logistics_tick_runs',
33530
+ 'f:logistics_belading_bevestig','f:logistics_cart_seq_dubbel','f:logistics_plan_retry_later','f:logistics_own_traysticker_sync',
33531
+ 't:logistics_belading_bevestigingen',
33532
+ 'f:logistics_sticker_copies','f:logistics_sticker_etiket_status','f:logistics_sticker_order_counts','f:logistics_sticker_sync',
33533
+ 'c:logistics_sticker_prints.kopieen_bron',
33534
+ 'f:logistics_carrier_settings_ensure','t:logistics_carrier_settings','t:logistics_carrier_recalc_queue','t:logistics_carrier_events',
33535
+ 'f:logistics_carrier_status','f:logistics_carrier_set_backfill','f:logistics_carrier_preview_existing','c:control_get_todays_carts.carrier_name',
33536
+ 'f:logistics_plan_fail_log'] LOOP
33537
+ onderdeel := 'Engine: ' || substr(f, 3);
33538
+ ok := CASE left(f, 1)
33539
+ WHEN 'f' THEN EXISTS (SELECT 1 FROM pg_proc p WHERE p.pronamespace = 'public'::regnamespace AND p.proname = substr(f, 3))
33540
+ WHEN 't' THEN to_regclass('public.' || substr(f, 3)) IS NOT NULL
33541
+ ELSE CASE WHEN split_part(substr(f, 3), '.', 1) = 'control_get_todays_carts'
33542
+ THEN coalesce(pg_get_function_result(to_regprocedure('public.control_get_todays_carts()')), '') LIKE '%carrier_name%'
33543
+ ELSE EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public'
33544
+ AND table_name = split_part(substr(f, 3), '.', 1) AND column_name = split_part(substr(f, 3), '.', 2)) END
33545
+ END;
33546
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie (1.0.182 t/m 1.0.188) in dev uitvoeren en via schemasync verspreiden' END;
33547
+ RETURN NEXT;
33548
+ END LOOP;
33549
+ FOREACH c IN ARRAY ARRAY['carrier_id','carrier_source','carrier_reason','carrier_resolved_at','carrier_set_by','carrier_set_at'] LOOP
33550
+ onderdeel := 'orders.' || c;
33551
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='orders' AND column_name=c);
33552
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
33553
+ END LOOP;
33554
+ FOREACH f IN ARRAY ARRAY['logistics_resolve_carrier(uuid)','logistics_resolve_carrier_many(uuid[])',
33555
+ 'logistics_resolve_carrier_for_location(uuid,uuid)','logistics_resolve_carrier_for_location_many(uuid[],uuid)',
33556
+ 'logistics_resolve_carrier_for_location_many(jsonb)',
33557
+ 'logistics_set_order_carrier(uuid,uuid)','logistics_carrier_recalc_run(integer)'] LOOP
33558
+ onderdeel := 'functie ' || f; ok := to_regprocedure('public.' || f) IS NOT NULL;
33559
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
33560
+ END LOOP;
33561
+ FOREACH t IN ARRAY ARRAY['orders:trg_logistics_orders_carrier_fill','orders:trg_logistics_orders_carrier_changed',
33562
+ 'carriers:trg_logistics_carriers_enqueue','customer_delivery_locations:trg_logistics_locations_enqueue'] LOOP
33563
+ onderdeel := 'trigger ' || split_part(t, ':', 2);
33564
+ ok := EXISTS (SELECT 1 FROM pg_trigger g WHERE g.tgname = split_part(t, ':', 2)
33565
+ AND g.tgrelid = to_regclass('public.' || split_part(t, ':', 1)));
33566
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Engine-migratie' END; RETURN NEXT;
33567
+ END LOOP;
33568
+ onderdeel := 'minuutronde rekent vervoerders';
33569
+ ok := EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'logistics_tick' AND prosrc LIKE '%logistics_carrier_recalc_run%');
33570
+ toelichting := CASE WHEN ok THEN 'ja' ELSE 'nee' END; RETURN NEXT;
33571
+ FOREACH c IN ARRAY ARRAY['carrier_kind','is_favorite','archived_at','delivery_location_glns'] LOOP
33572
+ onderdeel := 'Afhandeling: carriers.' || c;
33573
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='carriers' AND column_name=c);
33574
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Afhandeling' END; RETURN NEXT;
33575
+ END LOOP;
33576
+ FOREACH f IN ARRAY ARRAY['afh_favorite_carrier','afh_city_code','afh_normalize_country','afh_normalize_nl_province'] LOOP
33577
+ onderdeel := 'Afhandeling: ' || f || '()';
33578
+ ok := EXISTS (SELECT 1 FROM pg_proc p WHERE p.pronamespace = 'public'::regnamespace AND p.proname = f);
33579
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Afhandeling' END; RETURN NEXT;
33580
+ END LOOP;
33581
+ onderdeel := 'Favoriete vervoerder ingesteld en bruikbaar';
33582
+ IF to_regprocedure('public.afh_favorite_carrier()') IS NULL THEN ok := false; toelichting := 'functie ontbreekt';
33583
+ ELSE
33584
+ BEGIN v_fav := public.logistics_carrier_favorite_id()::text; EXCEPTION WHEN OTHERS THEN v_fav := NULL; END;
33585
+ ok := public.logistics_try_uuid(v_fav) IS NOT NULL AND public.logistics_carrier_skip_reason(public.logistics_try_uuid(v_fav)) IS NULL;
33586
+ toelichting := CASE WHEN ok THEN 'ja' WHEN v_fav IS NULL THEN 'geen favoriet ingesteld (Afhandeling → Vervoerders)'
33587
+ ELSE public.logistics_carrier_skip_reason(public.logistics_try_uuid(v_fav)) END;
33588
+ END IF;
33589
+ RETURN NEXT;
33590
+ onderdeel := 'Klanten: customers.default_carrier_id';
33591
+ ok := EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='customers' AND column_name='default_carrier_id');
33592
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: Klanten' END; RETURN NEXT;
33593
+ IF to_regclass('public.logistics_carrier_settings') IS NOT NULL AND to_regclass('public.orders') IS NOT NULL
33594
+ AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='orders' AND column_name='carrier_source') THEN
33595
+ SELECT * INTO s FROM public.logistics_carrier_settings_ensure();
33596
+ EXECUTE $q$SELECT count(*) FROM public.orders o
33597
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
33598
+ AND coalesce(o.delivery_date, '') ~ '^\d{4}-\d{2}-\d{2}' AND left(o.delivery_date, 10)::date >= current_date
33599
+ AND (o.carrier_source = 'geen' OR (o.carrier_source IS NULL AND o.created_at >= $1))$q$ INTO n USING s.installed_at;
33600
+ onderdeel := 'Open orders zonder vervoerder'; ok := n = 0; toelichting := n || ' order(s)'; RETURN NEXT;
33601
+ onderdeel := 'Schakelaar bestaande orders'; ok := true;
33602
+ toelichting := CASE WHEN coalesce(s.backfill_existing, false) THEN 'aan' ELSE 'uit (bestaande orders wachten op akkoord)' END; RETURN NEXT;
33603
+ END IF;
33604
+ -- 1.0.188: mislukte aanmelding voor (her)planning vanuit de ordertriggers.
33605
+ SELECT count(*) INTO n FROM public.logistics_trace_events e
33606
+ WHERE e.actor IN ('trg_logistics_plan_order_items', 'trg_logistics_plan_orders')
33607
+ AND e.status = 'mislukt' AND e.created_at > now() - interval '24 hours';
33608
+ onderdeel := 'Mislukte herplanning (laatste 24 uur)'; ok := n = 0;
33609
+ toelichting := CASE WHEN n = 0 THEN 'geen' ELSE n || ' keer; laatste: ' || coalesce((SELECT coalesce(e.order_ref, '?') || ': ' || coalesce(e.error, '') FROM public.logistics_trace_events e
33610
+ WHERE e.actor IN ('trg_logistics_plan_order_items', 'trg_logistics_plan_orders') AND e.status = 'mislukt'
33611
+ ORDER BY e.created_at DESC LIMIT 1), '') END;
33612
+ RETURN NEXT;
33613
+ RETURN QUERY SELECT * FROM public.logistics_install_check_194();
33614
+ RETURN QUERY SELECT * FROM public.logistics_install_check_196();
33615
+ RETURN QUERY SELECT * FROM public.logistics_access_check();
33616
+ RETURN QUERY SELECT * FROM public.logistics_overload_check();
33617
+ RETURN QUERY SELECT * FROM public.logistics_check_190();
33618
+ END $function$
33619
+ ;
33620
+
33621
+ -- Bij installeren: instellingen aanvullen (idempotent).
33622
+ DO $e$ BEGIN
33623
+ PERFORM public.logistics_ensure_tick_settings();
33624
+ PERFORM public.logistics_ensure_rebuffer_setting();
33625
+ EXCEPTION WHEN OTHERS THEN
33626
+ RAISE NOTICE 'logistiek-engine 1.0.194: instellingen aanvullen overgeslagen (%); de minuutronde doet dit alsnog', SQLERRM;
33627
+ END $e$;
33628
+
33629
+ GRANT EXECUTE ON FUNCTION public.logistics_order_date(text) TO authenticated, service_role;
33630
+ GRANT EXECUTE ON FUNCTION public.logistics_order_date(date) TO authenticated, service_role;
33631
+ GRANT EXECUTE ON FUNCTION public.logistics_print_status(uuid[]) TO authenticated, service_role;
33632
+ GRANT EXECUTE ON FUNCTION public.logistics_kick_metingen(integer) TO authenticated, service_role;
33633
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_194() TO authenticated, service_role;
33634
+
33635
+ -- ============================================================================
33636
+ -- logistiek-engine 1.0.196 — direct wekken, achterstand in één aanroep, gegevens
33637
+ -- voor karindeling, sticker opnieuw proberen, rebuffer-slot (additief, idempotent)
33638
+ -- ============================================================================
33639
+ -- 1.0.196 — direct wekken, achterstand in één aanroep, gegevens voor karindeling, sticker opnieuw, rebuffer-slot
33640
+
33641
+ CREATE OR REPLACE FUNCTION public.logistics_plan_now_gate(p_order_ids uuid[])
33642
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
33643
+ DECLARE
33644
+ v_running boolean; v_last timestamptz;
33645
+ v_debounce int := public.logistics_plan_setting('kick_debounce_seconds', 5)::int;
33646
+ v_n int := coalesce(cardinality(p_order_ids), 0);
33647
+ BEGIN
33648
+ PERFORM public.logistics_acc_guard('server', 'logistics_plan_now_gate');
33649
+ IF v_n > 1000 THEN
33650
+ RAISE EXCEPTION 'Maximaal 1000 orders per aanroep (ontvangen: %).', v_n USING ERRCODE = '22023';
33651
+ END IF;
33652
+ SELECT EXISTS (SELECT 1 FROM public.logistics_plan_locks WHERE lock_key = 'tick:ronde' AND locked_until > now())
33653
+ INTO v_running;
33654
+ IF v_running THEN
33655
+ INSERT INTO public.logistics_plan_settings(key, value, updated_at)
33656
+ VALUES ('kick_pending', clock_timestamp()::text, now())
33657
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
33658
+ BEGIN INSERT INTO public.logistics_kick_log(skipped_reason) VALUES ('plan_now_vlag_gezet'); EXCEPTION WHEN OTHERS THEN NULL; END;
33659
+ RETURN jsonb_build_object('actie', 'vlag_gezet', 'orders', v_n);
33660
+ END IF;
33661
+ SELECT NULLIF(value,'')::timestamptz INTO v_last FROM public.logistics_plan_settings WHERE key = 'last_kick_at';
33662
+ IF v_last IS NOT NULL AND v_last > now() - make_interval(secs => v_debounce) THEN
33663
+ -- Binnen de wachttijd: niet laten vallen, de eerstvolgende ronde pakt de vlag op.
33664
+ INSERT INTO public.logistics_plan_settings(key, value, updated_at)
33665
+ VALUES ('kick_pending', clock_timestamp()::text, now())
33666
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
33667
+ BEGIN INSERT INTO public.logistics_kick_log(skipped_reason) VALUES ('plan_now_debounce'); EXCEPTION WHEN OTHERS THEN NULL; END;
33668
+ RETURN jsonb_build_object('actie', 'debounce', 'orders', v_n);
33669
+ END IF;
33670
+ INSERT INTO public.logistics_plan_settings(key, value, updated_at)
33671
+ VALUES ('last_kick_at', now()::text, now())
33672
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
33673
+ BEGIN INSERT INTO public.logistics_kick_log(skipped_reason) VALUES ('plan_now_start'); EXCEPTION WHEN OTHERS THEN NULL; END;
33674
+ RETURN jsonb_build_object('actie', 'start', 'orders', v_n);
33675
+ END $$;
33676
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_now_gate(uuid[]) TO service_role;
33677
+
33678
+ CREATE OR REPLACE FUNCTION public.logistics_order_backlog_info_many(p_order_ids uuid[])
33679
+ RETURNS TABLE(order_id uuid, info jsonb, achterstand boolean, leverdatum date, dagen integer)
33680
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
33681
+ BEGIN
33682
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_backlog_info_many');
33683
+ IF coalesce(cardinality(p_order_ids), 0) > 1000 THEN
33684
+ RAISE EXCEPTION 'Maximaal 1000 orders per aanroep (ontvangen: %).', cardinality(p_order_ids) USING ERRCODE = '22023';
33685
+ END IF;
33686
+ RETURN QUERY
33687
+ WITH ids AS (SELECT DISTINCT u AS id FROM unnest(coalesce(p_order_ids, '{}'::uuid[])) u WHERE u IS NOT NULL),
33688
+ today AS (SELECT (now() AT TIME ZONE public.logistics_tz())::date AS d),
33689
+ hit AS (
33690
+ SELECT o.id, (o.delivery_date::text)::date AS ld
33691
+ FROM public.orders o JOIN ids ON ids.id = o.id, today
33692
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
33693
+ AND COALESCE(o.is_reservation, false) = false
33694
+ AND lower(coalesce(o.status, '')) = 'nog te picken'
33695
+ AND NULLIF(o.delivery_date::text, '') IS NOT NULL
33696
+ AND (o.delivery_date::text)::date < today.d
33697
+ AND NOT public.logistics_order_handled(o.id)
33698
+ AND NOT EXISTS (SELECT 1 FROM public.floriday_salesorders_cache fc
33699
+ WHERE o.floriday_sales_order_id IS NOT NULL AND fc.floriday_id = o.floriday_sales_order_id AND fc.controlled_at IS NOT NULL)
33700
+ )
33701
+ SELECT ids.id,
33702
+ CASE WHEN h.id IS NULL THEN NULL
33703
+ ELSE jsonb_build_object('achterstand', true, 'leverdatum', h.ld, 'dagen', t.d - h.ld) END,
33704
+ h.id IS NOT NULL, h.ld, CASE WHEN h.id IS NULL THEN NULL ELSE t.d - h.ld END
33705
+ FROM ids CROSS JOIN today t LEFT JOIN hit h ON h.id = ids.id;
33706
+ END $$;
33707
+ GRANT EXECUTE ON FUNCTION public.logistics_order_backlog_info_many(uuid[]) TO authenticated, service_role;
33708
+
33709
+ -- E4/E5: gegevens voor karindeling
33710
+ CREATE OR REPLACE FUNCTION public.logistics_data_gereedheid(p_product_ids uuid[] DEFAULT NULL)
33711
+ RETURNS TABLE(product_id uuid, product text, ladingdrager text, methode text, in_open_order boolean,
33712
+ ontbrekend text[], velden jsonb, voorstel jsonb, uitleg text)
33713
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
33714
+ DECLARE v_methode text; v_carrier text; c record;
33715
+ BEGIN
33716
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_data_gereedheid');
33717
+ SELECT CASE WHEN coalesce(s.settings->'karrenindeling'->'fields'->>'cart_layout_method', s.settings->'karrenindeling'->>'cart_layout_method') = 'afmetingen'
33718
+ THEN 'afmetingen' ELSE 'belading' END,
33719
+ nullif(btrim(coalesce(s.settings->'karrenindeling'->'fields'->>'default_carrier_type', s.settings->'karrenindeling'->>'default_carrier_type')), '')
33720
+ INTO v_methode, v_carrier
33721
+ FROM public.logistics_rule_configs s WHERE s.is_active ORDER BY s.updated_at DESC LIMIT 1;
33722
+ v_methode := coalesce(v_methode, 'belading');
33723
+ SELECT t.code, t.name, t.layer_length_cm, t.layer_width_cm, t.max_height_cm INTO c
33724
+ FROM public.load_carrier_types t
33725
+ WHERE t.is_active AND (t.code = v_carrier OR (v_carrier IS NULL AND t.is_default))
33726
+ ORDER BY (t.code = v_carrier) DESC NULLS LAST LIMIT 1;
33727
+
33728
+ RETURN QUERY
33729
+ WITH open_p AS (
33730
+ SELECT DISTINCT oi.product_id FROM public.orders o JOIN public.order_items oi ON oi.order_id = o.id
33731
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.is_reservation,false) = false
33732
+ AND lower(coalesce(o.status,'')) = 'nog te picken' AND oi.product_id IS NOT NULL
33733
+ ),
33734
+ p AS (
33735
+ SELECT pr.id, pr.product, pr.plants_per_crate, pr.crates_per_layer, pr.layers_per_cart,
33736
+ NULLIF(regexp_replace(coalesce(pr.weight::text,''), '[^0-9.,]', '', 'g'), '') AS weight_txt,
33737
+ pr.plant_height_cm, pr.fust_type_id, pr.floriday_packing_source,
33738
+ ft.length_mm, ft.width_mm, ft.height_mm,
33739
+ (op.product_id IS NOT NULL) AS in_open
33740
+ FROM public.products pr
33741
+ LEFT JOIN public.fust_types ft ON ft.id = pr.fust_type_id
33742
+ LEFT JOIN open_p op ON op.product_id = pr.id
33743
+ WHERE (p_product_ids IS NULL AND op.product_id IS NOT NULL) OR pr.id = ANY(p_product_ids)
33744
+ ),
33745
+ m AS (
33746
+ SELECT p.*,
33747
+ array_remove(ARRAY[
33748
+ CASE WHEN coalesce(p.plants_per_crate,0) <= 0 THEN 'plants_per_crate' END,
33749
+ CASE WHEN v_methode = 'belading' AND coalesce(p.crates_per_layer,0) <= 0 THEN 'crates_per_layer' END,
33750
+ CASE WHEN v_methode = 'belading' AND coalesce(p.layers_per_cart,0) <= 0 THEN 'layers_per_cart' END,
33751
+ CASE WHEN v_methode = 'afmetingen' AND p.fust_type_id IS NULL THEN 'fust_type_id' END,
33752
+ CASE WHEN v_methode = 'afmetingen' AND coalesce(p.plant_height_cm,0) <= 0 THEN 'plant_height_cm' END,
33753
+ CASE WHEN p.weight_txt IS NULL THEN 'weight' END
33754
+ ], NULL) AS mist,
33755
+ -- Fusten per laag uit fustmaat × laagmaat (beide oriëntaties, raster).
33756
+ CASE WHEN p.length_mm > 0 AND p.width_mm > 0 AND c.layer_length_cm > 0 AND c.layer_width_cm > 0 THEN
33757
+ greatest(floor(c.layer_length_cm*10 / p.length_mm) * floor(c.layer_width_cm*10 / p.width_mm),
33758
+ floor(c.layer_length_cm*10 / p.width_mm) * floor(c.layer_width_cm*10 / p.length_mm))::int END AS calc_cpl,
33759
+ -- Lagen per kar uit planthoogte + fusthoogte tegen hoogte ladingdrager.
33760
+ CASE WHEN coalesce(p.plant_height_cm,0) > 0 AND c.max_height_cm > 0 THEN
33761
+ floor(c.max_height_cm / (p.plant_height_cm + coalesce(p.height_mm,0)/10.0))::int END AS calc_lpc
33762
+ FROM p
33763
+ )
33764
+ SELECT m.id, m.product, c.code, v_methode, m.in_open, m.mist,
33765
+ jsonb_build_object(
33766
+ 'plants_per_crate', jsonb_build_object('waarde', m.plants_per_crate, 'bron', CASE WHEN m.plants_per_crate > 0 THEN coalesce(m.floriday_packing_source, 'product') END),
33767
+ 'crates_per_layer', jsonb_build_object('waarde', m.crates_per_layer, 'bron', CASE WHEN m.crates_per_layer > 0 THEN coalesce(m.floriday_packing_source, 'product') END),
33768
+ 'layers_per_cart', jsonb_build_object('waarde', m.layers_per_cart, 'bron', CASE WHEN m.layers_per_cart > 0 THEN coalesce(m.floriday_packing_source, 'product') END),
33769
+ 'weight', jsonb_build_object('waarde', m.weight_txt, 'bron', CASE WHEN m.weight_txt IS NOT NULL THEN 'product' END),
33770
+ 'plant_height_cm', jsonb_build_object('waarde', m.plant_height_cm, 'bron', CASE WHEN m.plant_height_cm > 0 THEN 'product' END),
33771
+ 'fust_type_id', jsonb_build_object('waarde', m.fust_type_id, 'bron', CASE WHEN m.fust_type_id IS NOT NULL THEN 'product' END)),
33772
+ jsonb_strip_nulls(jsonb_build_object(
33773
+ 'crates_per_layer', CASE WHEN m.calc_cpl > 0 THEN jsonb_build_object('waarde', m.calc_cpl, 'bron', 'berekend',
33774
+ 'berekening', format('fust %s×%s mm op laag %s×%s cm (%s)', m.length_mm, m.width_mm, c.layer_length_cm, c.layer_width_cm, c.name))
33775
+ ELSE (SELECT jsonb_build_object('waarde', x.v, 'bron', 'vergelijkbaar', 'berekening', format('meest gebruikt bij %s product(en) met dezelfde fust', x.n))
33776
+ FROM (SELECT p2.crates_per_layer v, count(*) n FROM public.products p2
33777
+ WHERE m.fust_type_id IS NOT NULL AND p2.fust_type_id = m.fust_type_id AND p2.crates_per_layer > 0 AND p2.id <> m.id
33778
+ GROUP BY 1 ORDER BY 2 DESC LIMIT 1) x) END,
33779
+ 'layers_per_cart', CASE WHEN m.calc_lpc > 0 THEN jsonb_build_object('waarde', m.calc_lpc, 'bron', 'berekend',
33780
+ 'berekening', format('hoogte %s cm / (plant %s cm + fust %s cm)', c.max_height_cm, m.plant_height_cm, round(coalesce(m.height_mm,0)/10.0,1)))
33781
+ ELSE (SELECT jsonb_build_object('waarde', x.v, 'bron', 'vergelijkbaar', 'berekening', format('meest gebruikt bij %s product(en) met dezelfde fust', x.n))
33782
+ FROM (SELECT p2.layers_per_cart v, count(*) n FROM public.products p2
33783
+ WHERE m.fust_type_id IS NOT NULL AND p2.fust_type_id = m.fust_type_id AND p2.layers_per_cart > 0 AND p2.id <> m.id
33784
+ GROUP BY 1 ORDER BY 2 DESC LIMIT 1) x) END)),
33785
+ CASE WHEN cardinality(m.mist) = 0 THEN 'Compleet.'
33786
+ ELSE format('Bij "%s" ontbreekt: %s. Vul dit in bij het product (Voorraad) of hier met "Invullen".', m.product,
33787
+ array_to_string(ARRAY(SELECT CASE f WHEN 'plants_per_crate' THEN 'planten per fust' WHEN 'crates_per_layer' THEN 'fusten per laag'
33788
+ WHEN 'layers_per_cart' THEN 'lagen per kar' WHEN 'fust_type_id' THEN 'fustsoort' WHEN 'plant_height_cm' THEN 'planthoogte'
33789
+ WHEN 'weight' THEN 'gewicht per fust' ELSE f END FROM unnest(m.mist) f), ', ')) END
33790
+ FROM m
33791
+ ORDER BY cardinality(m.mist) DESC, m.in_open DESC, m.product
33792
+ LIMIT 2000;
33793
+ END $$;
33794
+ GRANT EXECUTE ON FUNCTION public.logistics_data_gereedheid(uuid[]) TO authenticated, service_role;
33795
+
33796
+ CREATE OR REPLACE FUNCTION public.logistics_data_vul_in(p_product_id uuid, p_veld text, p_waarde numeric)
33797
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
33798
+ BEGIN
33799
+ PERFORM public.logistics_acc_guard('beheer', 'logistics_data_vul_in');
33800
+ IF p_waarde IS NULL OR p_waarde <= 0 THEN
33801
+ RAISE EXCEPTION 'Vul een getal groter dan 0 in.' USING ERRCODE = '22023';
33802
+ END IF;
33803
+ IF p_veld = 'plants_per_crate' THEN UPDATE public.products SET plants_per_crate = p_waarde, updated_at = now() WHERE id = p_product_id;
33804
+ ELSIF p_veld = 'crates_per_layer' THEN UPDATE public.products SET crates_per_layer = p_waarde, updated_at = now() WHERE id = p_product_id;
33805
+ ELSIF p_veld = 'layers_per_cart' THEN UPDATE public.products SET layers_per_cart = p_waarde, updated_at = now() WHERE id = p_product_id;
33806
+ ELSIF p_veld = 'plant_height_cm' THEN UPDATE public.products SET plant_height_cm = p_waarde, updated_at = now() WHERE id = p_product_id;
33807
+ ELSIF p_veld = 'weight' THEN UPDATE public.products SET weight = p_waarde, updated_at = now() WHERE id = p_product_id;
33808
+ ELSE RAISE EXCEPTION 'Dit veld kan hier niet worden ingevuld: %', p_veld USING ERRCODE = '22023';
33809
+ END IF;
33810
+ IF NOT FOUND THEN RAISE EXCEPTION 'Product niet gevonden.' USING ERRCODE = 'P0002'; END IF;
33811
+ RETURN jsonb_build_object('ok', true, 'productId', p_product_id, 'veld', p_veld, 'waarde', p_waarde);
33812
+ END $$;
33813
+ GRANT EXECUTE ON FUNCTION public.logistics_data_vul_in(uuid, text, numeric) TO authenticated, service_role;
33814
+
33815
+ -- 4: geweigerde sticker opnieuw proberen (status blijft binnen bestaande CHECK)
33816
+ CREATE OR REPLACE FUNCTION public.logistics_sticker_retry(p_id uuid)
33817
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
33818
+ BEGIN
33819
+ PERFORM public.logistics_acc_guard('beheer', 'logistics_sticker_retry');
33820
+ UPDATE public.logistics_sticker_prints
33821
+ SET status = 'klaar', reden = NULL, print_job_id = NULL, attempts = 0, next_attempt_at = NULL, updated_at = now()
33822
+ WHERE id = p_id AND status IN ('geblokkeerd', 'mislukt');
33823
+ IF NOT FOUND THEN
33824
+ RAISE EXCEPTION 'Deze sticker is niet geblokkeerd of mislukt; opnieuw proberen is niet nodig.' USING ERRCODE = '22023';
33825
+ END IF;
33826
+ RETURN jsonb_build_object('ok', true, 'id', p_id);
33827
+ END $$;
33828
+ GRANT EXECUTE ON FUNCTION public.logistics_sticker_retry(uuid) TO authenticated, service_role;
33829
+
33830
+ -- 6: rebuffer-instelling onder advisory lock (geen deadlock meer bij gelijktijdige aanroepen)
33831
+ CREATE OR REPLACE FUNCTION public.logistics_ensure_rebuffer_setting()
33832
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
33833
+ DECLARE v_n integer := 0;
33834
+ BEGIN
33835
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_ensure_rebuffer_setting');
33836
+ -- 1.0.196: één tegelijk; bezet = overslaan zonder fout. Daarna rijen in vaste volgorde vergrendelen.
33837
+ IF NOT pg_try_advisory_xact_lock(hashtext('logistics_ensure_rebuffer_setting')) THEN
33838
+ RETURN 0;
33839
+ END IF;
33840
+ PERFORM 1 FROM public.logistics_rule_configs c
33841
+ WHERE NOT coalesce(c.settings->'order_verwerking'->'fields' ? 'rebuffer_on_change', false)
33842
+ AND NOT coalesce(c.settings->'order_verwerking' ? 'rebuffer_on_change', false)
33843
+ ORDER BY c.id FOR UPDATE SKIP LOCKED;
33844
+ UPDATE public.logistics_rule_configs c
33845
+ SET settings = jsonb_set(coalesce(c.settings, '{}'::jsonb), '{order_verwerking}',
33846
+ coalesce(c.settings->'order_verwerking', '{}'::jsonb)
33847
+ || jsonb_build_object('fields', coalesce(c.settings->'order_verwerking'->'fields', '{}'::jsonb)
33848
+ || jsonb_build_object('rebuffer_on_change', true)), true)
33849
+ WHERE c.id IS NOT NULL
33850
+ AND NOT coalesce(c.settings->'order_verwerking'->'fields' ? 'rebuffer_on_change', false)
33851
+ AND NOT coalesce(c.settings->'order_verwerking' ? 'rebuffer_on_change', false);
33852
+ GET DIAGNOSTICS v_n = ROW_COUNT;
33853
+ RETURN v_n;
33854
+ END $$;
33855
+
33856
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_196()
33857
+ RETURNS TABLE(controle text, ok boolean, toelichting text)
33858
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
33859
+ BEGIN
33860
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_install_check_196');
33861
+ RETURN QUERY SELECT 'Engine: direct wekken (plan-now) – sleutel ingesteld'::text,
33862
+ EXISTS (SELECT 1 FROM public.module_runtime_config WHERE key = 'logistics_tick_apikey' AND nullif(btrim(value),'') IS NOT NULL),
33863
+ 'module_runtime_config logistics_tick_apikey; zonder sleutel antwoordt de route 500.'::text;
33864
+ RETURN QUERY SELECT 'Engine: poort direct wekken aanwezig', to_regprocedure('public.logistics_plan_now_gate(uuid[])') IS NOT NULL, 'logistics_plan_now_gate';
33865
+ RETURN QUERY SELECT 'Engine: achterstand in één aanroep', to_regprocedure('public.logistics_order_backlog_info_many(uuid[])') IS NOT NULL, 'logistics_order_backlog_info_many';
33866
+ RETURN QUERY SELECT 'Engine: gegevens voor karindeling', to_regprocedure('public.logistics_data_gereedheid(uuid[])') IS NOT NULL, 'logistics_data_gereedheid';
33867
+ RETURN QUERY SELECT 'Engine: sticker opnieuw proberen', to_regprocedure('public.logistics_sticker_retry(uuid)') IS NOT NULL, 'logistics_sticker_retry';
33868
+ RETURN QUERY SELECT 'Engine: rebuffer-instelling onder slot',
33869
+ coalesce(pg_get_functiondef('public.logistics_ensure_rebuffer_setting()'::regprocedure) LIKE '%pg_try_advisory_xact_lock%', false),
33870
+ 'voorkomt deadlock bij gelijktijdige aanroep';
33871
+ END $$;
33872
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_196() TO authenticated, service_role;
33873
+ DO $do$ DECLARE d text;
33874
+ BEGIN
33875
+ d := pg_get_functiondef('public.logistics_install_check()'::regprocedure);
33876
+ IF position('logistics_install_check_196' in d) = 0 THEN
33877
+ d := replace(d, $r$ RETURN QUERY SELECT * FROM public.logistics_access_check();$r$,
33878
+ $r$ RETURN QUERY SELECT * FROM public.logistics_install_check_196();
33879
+ RETURN QUERY SELECT * FROM public.logistics_access_check();$r$);
33880
+ EXECUTE d;
33881
+ END IF;
33882
+ EXCEPTION WHEN undefined_function THEN NULL;
33883
+ END $do$;
33884
+
33885
+ -- ===== 1.0.197 =====
33886
+ -- ============================================================================
33887
+ -- logistiek-engine 1.0.197 — unieke karnummering per buffer (incl. bevroren
33888
+ -- karren uit eerdere buffers), "van N" over alle karren, orderscherm telt via de
33889
+ -- buffer, fysieke beladingscontrole in de installatiecontrole (additief, idempotent)
33890
+ -- ============================================================================
33891
+
33892
+ -- Kern zonder guard en zonder SECURITY DEFINER: RLS van de aanroeper blijft gelden.
33893
+ CREATE OR REPLACE FUNCTION public.logistics_order_unit_carts_core(p_order_ids uuid[])
33894
+ RETURNS TABLE(cart_id uuid, cart_number integer, buffer_id uuid, order_id uuid, display_seq integer, display_total integer)
33895
+ LANGUAGE sql STABLE SET search_path TO 'public' AS $$
33896
+ WITH ords AS (
33897
+ SELECT o.id, o.buffer_id FROM public.orders o WHERE o.id = ANY(coalesce(p_order_ids, '{}'::uuid[]))
33898
+ ),
33899
+ bufs AS (SELECT DISTINCT b.buffer_id FROM ords b WHERE b.buffer_id IS NOT NULL),
33900
+ ords2 AS (
33901
+ SELECT o.id FROM ords o
33902
+ UNION
33903
+ SELECT o.id FROM public.orders o
33904
+ WHERE o.buffer_id IN (SELECT bb.buffer_id FROM bufs bb) AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
33905
+ ),
33906
+ k AS (
33907
+ SELECT c.id FROM logistics_active.order_carts c WHERE c.order_id IN (SELECT x.id FROM ords2 x)
33908
+ UNION
33909
+ SELECT ci.cart_id FROM logistics_active.order_cart_items ci
33910
+ JOIN public.order_items oi ON oi.id = ci.order_item_id
33911
+ WHERE oi.order_id IN (SELECT x.id FROM ords2 x)
33912
+ UNION
33913
+ SELECT c.id FROM logistics_active.order_carts c
33914
+ WHERE c.order_id IS NULL AND c.buffer_id IN (SELECT bb.buffer_id FROM bufs bb)
33915
+ ),
33916
+ base AS (
33917
+ SELECT c.id, c.cart_number, c.buffer_id, c.order_id, c.created_at,
33918
+ min(c.created_at) OVER (PARTITION BY coalesce(c.buffer_id, c.id)) AS grp_at
33919
+ FROM logistics_active.order_carts c WHERE c.id IN (SELECT k.id FROM k)
33920
+ ),
33921
+ f AS (
33922
+ SELECT count(*)::int AS n, count(DISTINCT b.cart_number)::int AS nd,
33923
+ coalesce(max(b.cart_number), 0)::int AS mx,
33924
+ count(*) FILTER (WHERE b.cart_number IS NULL)::int AS leeg
33925
+ FROM base b
33926
+ )
33927
+ SELECT b.id, b.cart_number, b.buffer_id, b.order_id,
33928
+ CASE WHEN f.nd = f.n AND f.leeg = 0 THEN b.cart_number
33929
+ ELSE row_number() OVER (ORDER BY b.grp_at, coalesce(b.buffer_id, b.id), b.cart_number NULLS LAST, b.id)::int END,
33930
+ CASE WHEN f.nd = f.n AND f.leeg = 0 THEN greatest(f.n, f.mx) ELSE f.n END
33931
+ FROM base b CROSS JOIN f
33932
+ $$;
33933
+
33934
+ CREATE OR REPLACE FUNCTION public.logistics_order_unit_carts(p_order_ids uuid[])
33935
+ RETURNS TABLE(cart_id uuid, cart_number integer, buffer_id uuid, order_id uuid, display_seq integer, display_total integer)
33936
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
33937
+ BEGIN
33938
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_unit_carts');
33939
+ IF coalesce(cardinality(p_order_ids), 0) > 1000 THEN
33940
+ RAISE EXCEPTION 'Maximaal 1000 orders per aanroep (ontvangen: %).', cardinality(p_order_ids) USING ERRCODE = '22023';
33941
+ END IF;
33942
+ RETURN QUERY SELECT * FROM public.logistics_order_unit_carts_core(p_order_ids);
33943
+ END $$;
33944
+ GRANT EXECUTE ON FUNCTION public.logistics_order_unit_carts(uuid[]) TO authenticated, service_role;
33945
+
33946
+ -- Bestaande handtekening blijft; weergave nu uniek binnen de hele eenheid.
33947
+ CREATE OR REPLACE FUNCTION public.logistics_cart_fixed_numbering(p_cart_id uuid)
33948
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
33949
+ DECLARE c record; v_ids uuid[]; u record; v_total int;
33950
+ BEGIN
33951
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_cart_fixed_numbering');
33952
+ SELECT * INTO c FROM public.order_carts WHERE id = p_cart_id;
33953
+ IF NOT FOUND THEN RETURN NULL; END IF;
33954
+ IF c.archived_at IS NULL THEN
33955
+ SELECT array_agg(DISTINCT s.x) INTO v_ids FROM (
33956
+ SELECT c.order_id AS x
33957
+ UNION SELECT oi.order_id FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id WHERE ci.cart_id = p_cart_id
33958
+ UNION SELECT o.id FROM public.orders o WHERE c.buffer_id IS NOT NULL AND o.buffer_id = c.buffer_id AND o.deleted_at IS NULL
33959
+ ) s WHERE s.x IS NOT NULL;
33960
+ IF v_ids IS NOT NULL THEN
33961
+ SELECT * INTO u FROM public.logistics_order_unit_carts_core(v_ids) z WHERE z.cart_id = p_cart_id;
33962
+ IF FOUND AND u.display_seq IS NOT NULL THEN
33963
+ RETURN jsonb_build_object('cartSeq', u.display_seq, 'cartIndex', u.display_seq, 'cartTotal', u.display_total,
33964
+ 'cartLabel', 'Kar ' || u.display_seq || ' van ' || u.display_total, 'storedCartNumber', c.cart_number);
33965
+ END IF;
33966
+ END IF;
33967
+ END IF;
33968
+ -- Terugval (gearchiveerde kar of geen orders): oude berekening.
33969
+ SELECT greatest(coalesce(max(c2.total_carts), 0), count(*)::int, coalesce(c.cart_number, 0)) INTO v_total
33970
+ FROM public.order_carts c2
33971
+ WHERE (c.order_id IS NULL AND c.buffer_id IS NOT NULL AND c2.order_id IS NULL AND c2.buffer_id = c.buffer_id)
33972
+ OR (c.order_id IS NOT NULL AND c2.order_id = c.order_id)
33973
+ OR (c.order_id IS NULL AND c.buffer_id IS NULL AND c2.id = c.id);
33974
+ RETURN jsonb_build_object('cartSeq', c.cart_number, 'cartIndex', c.cart_number, 'cartTotal', v_total,
33975
+ 'cartLabel', 'Kar ' || c.cart_number || ' van ' || v_total);
33976
+ END $$;
33977
+
33978
+ -- Orderscherm: tel alle karren van de eenheid (buffer + karren met goederen van de order).
33979
+ CREATE OR REPLACE FUNCTION public.logistics_live_cart_count(p_order_ref text)
33980
+ RETURNS integer LANGUAGE sql STABLE SET search_path TO 'public' AS $$
33981
+ SELECT COALESCE(
33982
+ NULLIF((SELECT count(*)::int FROM public.logistics_order_unit_carts_core(
33983
+ ARRAY(SELECT o.id FROM public.orders o WHERE o.order_number = p_order_ref AND o.deleted_at IS NULL))), 0),
33984
+ public.logistics_order_cart_count(p_order_ref),
33985
+ 0);
33986
+ $$;
33987
+
33988
+ -- Controle: actieve pakbonnen met dezelfde naam (alleen melden, niets wijzigen).
33989
+ CREATE OR REPLACE FUNCTION public.logistics_dubbele_pakbonnamen()
33990
+ RETURNS TABLE(document_number text, aantal integer, karren text[])
33991
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
33992
+ BEGIN
33993
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_dubbele_pakbonnamen');
33994
+ RETURN QUERY
33995
+ SELECT q.document_number, count(*)::int, array_agg(q.reference ORDER BY q.reference)
33996
+ FROM public.pick_queue q
33997
+ WHERE q.cancelled_at IS NULL AND q.document_number LIKE 'PB-%-K%'
33998
+ GROUP BY q.document_number HAVING count(*) > 1;
33999
+ END $$;
34000
+ GRANT EXECUTE ON FUNCTION public.logistics_dubbele_pakbonnamen() TO authenticated, service_role;
34001
+
34002
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_197()
34003
+ RETURNS TABLE(controle text, ok boolean, toelichting text)
34004
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34005
+ DECLARE v_dub int;
34006
+ BEGIN
34007
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_install_check_197');
34008
+ RETURN QUERY SELECT 'Engine: karnummering per eenheid'::text,
34009
+ to_regprocedure('public.logistics_order_unit_carts(uuid[])') IS NOT NULL
34010
+ AND coalesce(pg_get_functiondef('public.logistics_cart_fixed_numbering(uuid)'::regprocedure) LIKE '%logistics_order_unit_carts_core%', false),
34011
+ 'logistics_order_unit_carts + logistics_cart_fixed_numbering'::text;
34012
+ RETURN QUERY SELECT 'Engine: orderscherm telt karren via de buffer',
34013
+ coalesce(pg_get_functiondef('public.logistics_live_cart_count(text)'::regprocedure) LIKE '%logistics_order_unit_carts_core%', false),
34014
+ 'logistics_live_cart_count';
34015
+ BEGIN
34016
+ SELECT count(*) INTO v_dub FROM public.logistics_dubbele_pakbonnamen();
34017
+ RETURN QUERY SELECT 'Engine: geen dubbele pakbonnamen', v_dub = 0,
34018
+ CASE WHEN v_dub = 0 THEN 'geen' ELSE v_dub || ' naam/namen dubbel; de eerstvolgende pakbonronde zet het unieke nummer' END;
34019
+ EXCEPTION WHEN undefined_table THEN
34020
+ RETURN QUERY SELECT 'Engine: geen dubbele pakbonnamen', true, 'pick_queue ontbreekt in deze Shell';
34021
+ END;
34022
+ END $$;
34023
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_197() TO authenticated, service_role;
34024
+
34025
+ DO $do$ DECLARE d text;
34026
+ BEGIN
34027
+ d := pg_get_functiondef('public.logistics_install_check()'::regprocedure);
34028
+ IF position('logistics_install_check_197' in d) = 0 THEN
34029
+ d := replace(d, $r$ RETURN QUERY SELECT * FROM public.logistics_access_check();$r$,
34030
+ $r$ RETURN QUERY SELECT * FROM public.logistics_install_check_197();
34031
+ RETURN QUERY SELECT * FROM public.logistics_access_check();$r$);
34032
+ EXECUTE d;
34033
+ END IF;
34034
+ EXCEPTION WHEN undefined_function THEN NULL;
34035
+ END $do$;
34036
+
34037
+ -- ============================================================================
34038
+ -- 1.0.198: maten hergebruiken uit Floricode/Floriday/Voorraad; niets stil invullen
34039
+ -- ============================================================================
34040
+
34041
+ -- Bronregistratie van overgenomen of bevestigde waarden (Engine-eigendom).
34042
+ CREATE TABLE IF NOT EXISTS public.logistics_gegevens_bron (
34043
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
34044
+ onderwerp text NOT NULL,
34045
+ sleutel text NOT NULL,
34046
+ veld text NOT NULL,
34047
+ waarde text,
34048
+ bron text NOT NULL,
34049
+ toelichting text,
34050
+ vastgelegd_door uuid,
34051
+ vastgelegd_op timestamptz NOT NULL DEFAULT now(),
34052
+ UNIQUE (onderwerp, sleutel, veld)
34053
+ );
34054
+ GRANT SELECT ON public.logistics_gegevens_bron TO authenticated;
34055
+ GRANT ALL ON public.logistics_gegevens_bron TO service_role;
34056
+ ALTER TABLE public.logistics_gegevens_bron ENABLE ROW LEVEL SECURITY;
34057
+ DO $do$ BEGIN
34058
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'logistics_gegevens_bron' AND policyname = 'logistics_gegevens_bron_lezen') THEN
34059
+ CREATE POLICY logistics_gegevens_bron_lezen ON public.logistics_gegevens_bron FOR SELECT TO authenticated USING (true);
34060
+ END IF;
34061
+ END $do$;
34062
+
34063
+ -- Positief getal uit tekst, anders NULL. Nooit een standaardwaarde.
34064
+ CREATE OR REPLACE FUNCTION public.logistics_pos_num(p text)
34065
+ RETURNS numeric LANGUAGE sql IMMUTABLE SET search_path TO 'public' AS $$
34066
+ SELECT CASE WHEN replace(btrim(coalesce(p, '')), ',', '.') ~ '^[0-9]+(\.[0-9]+)?$'
34067
+ THEN NULLIF(replace(btrim(p), ',', '.')::numeric, 0) END
34068
+ $$;
34069
+ GRANT EXECUTE ON FUNCTION public.logistics_pos_num(text) TO authenticated, service_role;
34070
+
34071
+ -- VBN-kenmerkwaarde in cm. Met Floricode: alleen een omschrijving "N cm" telt;
34072
+ -- een bereik of andere tekst = onbekend. Zonder Floricode-tabel: de code zelf (cm).
34073
+ CREATE OR REPLACE FUNCTION public.logistics_vbn_cm(p_feature text, p_code text)
34074
+ RETURNS numeric LANGUAGE plpgsql STABLE SET search_path TO 'public' AS $$
34075
+ DECLARE v_desc text; v_found boolean := false;
34076
+ BEGIN
34077
+ IF p_code IS NULL OR btrim(p_code) = '' THEN RETURN NULL; END IF;
34078
+ IF to_regclass('public.floricode_records') IS NOT NULL THEN
34079
+ EXECUTE $q$SELECT r.data->>'description' FROM public.floricode_records r
34080
+ WHERE r.resource = 'FeatureValue' AND r.data->>'feature_type_id' = $1 AND r.data->>'code' = $2
34081
+ ORDER BY (r.data->>'expiry_date') IS NULL DESC LIMIT 1$q$
34082
+ INTO v_desc USING p_feature, p_code;
34083
+ v_found := v_desc IS NOT NULL;
34084
+ END IF;
34085
+ IF v_found THEN
34086
+ IF v_desc ~* '^\s*[0-9]+([.,][0-9]+)?\s*cm\s*$' THEN
34087
+ RETURN public.logistics_pos_num(regexp_replace(v_desc, '[^0-9.,]', '', 'g'));
34088
+ END IF;
34089
+ RETURN NULL;
34090
+ END IF;
34091
+ RETURN public.logistics_pos_num(p_code);
34092
+ END $$;
34093
+ GRANT EXECUTE ON FUNCTION public.logistics_vbn_cm(text, text) TO authenticated, service_role;
34094
+
34095
+ -- 1: eigen terugval voor fustmaten. Per veld: Floricode -> Floriday custom package -> handmatig -> onbekend.
34096
+ CREATE OR REPLACE FUNCTION public.logistics_fust_maten_terugval()
34097
+ RETURNS TABLE(fust_type_id uuid, fust_key text, vbn_code integer, name text,
34098
+ length_mm numeric, width_mm numeric, height_mm numeric,
34099
+ lengte_bron text, breedte_bron text, hoogte_bron text)
34100
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34101
+ #variable_conflict use_column
34102
+ DECLARE v_fc text; v_fd text;
34103
+ BEGIN
34104
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_fust_maten_terugval');
34105
+ IF to_regprocedure('public.floricode_packaging_types()') IS NOT NULL THEN
34106
+ v_fc := $q$SELECT DISTINCT ON (c.code::text)
34107
+ CASE WHEN c.code::text ~ '^[0-9]+$' THEN c.code::text::int END AS code,
34108
+ public.logistics_pos_num(c.length_mm::text) AS l, public.logistics_pos_num(c.width_mm::text) AS w,
34109
+ public.logistics_pos_num(c.height_mm::text) AS h
34110
+ FROM public.floricode_packaging_types() c ORDER BY c.code::text, c.expiry_date DESC NULLS FIRST$q$;
34111
+ ELSIF to_regclass('public.floricode_records') IS NOT NULL THEN
34112
+ v_fc := $q$SELECT DISTINCT ON (r.data->>'code')
34113
+ CASE WHEN (r.data->>'code') ~ '^[0-9]+$' THEN (r.data->>'code')::int END AS code,
34114
+ public.logistics_pos_num(r.data->>'length') AS l, public.logistics_pos_num(r.data->>'width') AS w,
34115
+ public.logistics_pos_num(r.data->>'height') AS h
34116
+ FROM public.floricode_records r WHERE r.resource = 'LoadCarrier'
34117
+ ORDER BY r.data->>'code', (r.data->>'expiry_date') DESC NULLS FIRST$q$;
34118
+ ELSE
34119
+ v_fc := $q$SELECT NULL::int AS code, NULL::numeric AS l, NULL::numeric AS w, NULL::numeric AS h WHERE false$q$;
34120
+ END IF;
34121
+ IF to_regclass('public.floriday_packings_cache') IS NOT NULL THEN
34122
+ v_fd := $q$SELECT DISTINCT ON (p.floriday_id) p.floriday_id AS id, p.data->>'customPackageId' AS id2,
34123
+ public.logistics_pos_num(coalesce(p.data->'dimension'->>'length', p.data->'dimensions'->>'length', p.data->>'lengthInMm', p.data->>'length')) AS l,
34124
+ public.logistics_pos_num(coalesce(p.data->'dimension'->>'width', p.data->'dimensions'->>'width', p.data->>'widthInMm', p.data->>'width')) AS w,
34125
+ public.logistics_pos_num(coalesce(p.data->'dimension'->>'height', p.data->'dimensions'->>'height', p.data->>'heightInMm', p.data->>'height')) AS h
34126
+ FROM public.floriday_packings_cache p WHERE coalesce(p.is_deleted, false) = false
34127
+ ORDER BY p.floriday_id, p.updated_at DESC NULLS LAST$q$;
34128
+ ELSE
34129
+ v_fd := $q$SELECT NULL::text AS id, NULL::text AS id2, NULL::numeric AS l, NULL::numeric AS w, NULL::numeric AS h WHERE false$q$;
34130
+ END IF;
34131
+ RETURN QUERY EXECUTE format($q$
34132
+ WITH fc AS (%s), fd AS (%s)
34133
+ SELECT f.id, f.fust_key, f.vbn_code, f.name,
34134
+ coalesce(fc.l, fdx.l, public.logistics_pos_num(f.length_mm::text)),
34135
+ coalesce(fc.w, fdx.w, public.logistics_pos_num(f.width_mm::text)),
34136
+ coalesce(fc.h, fdx.h, public.logistics_pos_num(f.height_mm::text)),
34137
+ CASE WHEN fc.l IS NOT NULL THEN 'floricode' WHEN fdx.l IS NOT NULL THEN 'floriday' WHEN public.logistics_pos_num(f.length_mm::text) IS NOT NULL THEN 'handmatig' ELSE 'onbekend' END,
34138
+ CASE WHEN fc.w IS NOT NULL THEN 'floricode' WHEN fdx.w IS NOT NULL THEN 'floriday' WHEN public.logistics_pos_num(f.width_mm::text) IS NOT NULL THEN 'handmatig' ELSE 'onbekend' END,
34139
+ CASE WHEN fc.h IS NOT NULL THEN 'floricode' WHEN fdx.h IS NOT NULL THEN 'floriday' WHEN public.logistics_pos_num(f.height_mm::text) IS NOT NULL THEN 'handmatig' ELSE 'onbekend' END
34140
+ FROM public.fust_types f
34141
+ LEFT JOIN fc ON fc.code = coalesce(f.vbn_code, CASE WHEN f.fust_key ~ '^vbn:[0-9]+$' THEN substr(f.fust_key, 5)::int END)
34142
+ LEFT JOIN LATERAL (SELECT fd.l, fd.w, fd.h FROM fd
34143
+ WHERE nullif(f.custom_package_id, '') IS NOT NULL AND (fd.id = f.custom_package_id OR fd.id2 = f.custom_package_id)
34144
+ LIMIT 1) fdx ON true
34145
+ $q$, v_fc, v_fd);
34146
+ END $$;
34147
+ GRANT EXECUTE ON FUNCTION public.logistics_fust_maten_terugval() TO authenticated, service_role;
34148
+
34149
+ -- 1: fustmaten. Voorraad (voorraad_fust_dimensions) als die bestaat, anders de eigen terugval.
34150
+ CREATE OR REPLACE FUNCTION public.logistics_fust_maten()
34151
+ RETURNS TABLE(fust_type_id uuid, fust_key text, vbn_code integer, name text,
34152
+ length_mm numeric, width_mm numeric, height_mm numeric,
34153
+ lengte_bron text, breedte_bron text, hoogte_bron text, pad text)
34154
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34155
+ #variable_conflict use_column
34156
+ DECLARE j jsonb; v_src text;
34157
+ BEGIN
34158
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_fust_maten');
34159
+ IF to_regprocedure('public.voorraad_fust_dimensions()') IS NOT NULL THEN
34160
+ FOR j IN EXECUTE 'SELECT to_jsonb(x) FROM public.voorraad_fust_dimensions() x' LOOP
34161
+ fust_type_id := (j->>'fust_type_id')::uuid;
34162
+ fust_key := j->>'fust_key';
34163
+ vbn_code := CASE WHEN (j->>'vbn_code') ~ '^[0-9]+$' THEN (j->>'vbn_code')::int END;
34164
+ name := j->>'name';
34165
+ length_mm := public.logistics_pos_num(j->>'length_mm');
34166
+ width_mm := public.logistics_pos_num(j->>'width_mm');
34167
+ height_mm := public.logistics_pos_num(j->>'height_mm');
34168
+ v_src := nullif(j->>'dimension_source', 'onbekend');
34169
+ lengte_bron := CASE WHEN length_mm IS NULL THEN 'onbekend' ELSE coalesce(j->>'length_source', j->>'lengte_bron', v_src, 'voorraad') END;
34170
+ breedte_bron := CASE WHEN width_mm IS NULL THEN 'onbekend' ELSE coalesce(j->>'width_source', j->>'breedte_bron', v_src, 'voorraad') END;
34171
+ hoogte_bron := CASE WHEN height_mm IS NULL THEN 'onbekend' ELSE coalesce(j->>'height_source', j->>'hoogte_bron', v_src, 'voorraad') END;
34172
+ pad := 'voorraad';
34173
+ RETURN NEXT;
34174
+ END LOOP;
34175
+ RETURN;
34176
+ END IF;
34177
+ RETURN QUERY SELECT t.fust_type_id, t.fust_key, t.vbn_code, t.name, t.length_mm, t.width_mm, t.height_mm,
34178
+ t.lengte_bron, t.breedte_bron, t.hoogte_bron, 'terugval'::text
34179
+ FROM public.logistics_fust_maten_terugval() t;
34180
+ END $$;
34181
+ GRANT EXECUTE ON FUNCTION public.logistics_fust_maten() TO authenticated, service_role;
34182
+
34183
+ -- 2: eigen terugval voor artikelkenmerken (S15, S02, fust) uit het Floriday-handelsitem.
34184
+ -- Leest nooit products.custom_fields.
34185
+ CREATE OR REPLACE FUNCTION public.logistics_artikel_vbn_terugval(p_product_id uuid)
34186
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34187
+ DECLARE v_ti text; v jsonb;
34188
+ BEGIN
34189
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_artikel_vbn_terugval');
34190
+ SELECT coalesce(nullif(p.floriday_trade_item_id, ''), nullif(p.trade_item_key, '')) INTO v_ti FROM public.products p WHERE p.id = p_product_id;
34191
+ IF v_ti IS NULL OR to_regclass('public.floriday_trade_items_cache') IS NULL THEN
34192
+ RETURN jsonb_build_object('bron', 'geen', 's02_cm', NULL, 's15_cm', NULL, 'vbn_package_code', NULL, 'custom_package_id', NULL);
34193
+ END IF;
34194
+ EXECUTE $q$
34195
+ SELECT jsonb_build_object(
34196
+ 'bron', 'floriday_handelsitem',
34197
+ 's15_cm', public.logistics_vbn_cm('S15', (SELECT x->>'vbnValueCode' FROM jsonb_array_elements(coalesce(c.data->'characteristics', '[]'::jsonb)) x WHERE x->>'vbnCode' = 'S15' LIMIT 1)),
34198
+ 's02_cm', public.logistics_vbn_cm('S02', (SELECT x->>'vbnValueCode' FROM jsonb_array_elements(coalesce(c.data->'characteristics', '[]'::jsonb)) x WHERE x->>'vbnCode' = 'S02' LIMIT 1)),
34199
+ 'vbn_package_code', (SELECT CASE WHEN (pc->'package'->>'vbnPackageCode') ~ '^[0-9]+$' THEN (pc->'package'->>'vbnPackageCode')::int END
34200
+ FROM jsonb_array_elements(coalesce(c.data->'packingConfigurations', '[]'::jsonb)) pc
34201
+ ORDER BY (pc->>'primary')::boolean DESC NULLS LAST LIMIT 1),
34202
+ 'custom_package_id', (SELECT nullif(pc->'package'->>'customPackageId', '')
34203
+ FROM jsonb_array_elements(coalesce(c.data->'packingConfigurations', '[]'::jsonb)) pc
34204
+ ORDER BY (pc->>'primary')::boolean DESC NULLS LAST LIMIT 1))
34205
+ FROM public.floriday_trade_items_cache c
34206
+ WHERE (c.floriday_id = $1 OR c.data->>'tradeItemId' = $1) AND coalesce(c.is_deleted, false) = false
34207
+ LIMIT 1$q$ INTO v USING v_ti;
34208
+ RETURN coalesce(v, jsonb_build_object('bron', 'geen', 's02_cm', NULL, 's15_cm', NULL, 'vbn_package_code', NULL, 'custom_package_id', NULL));
34209
+ END $$;
34210
+ GRANT EXECUTE ON FUNCTION public.logistics_artikel_vbn_terugval(uuid) TO authenticated, service_role;
34211
+
34212
+ -- 2: artikelkenmerken. Voorraad (voorraad_article_vbn) als die bestaat, anders de eigen terugval.
34213
+ CREATE OR REPLACE FUNCTION public.logistics_artikel_vbn(p_product_id uuid)
34214
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34215
+ DECLARE j jsonb;
34216
+ BEGIN
34217
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_artikel_vbn');
34218
+ IF to_regprocedure('public.voorraad_article_vbn(uuid)') IS NOT NULL THEN
34219
+ EXECUTE 'SELECT to_jsonb(x) FROM public.voorraad_article_vbn($1) x LIMIT 1' INTO j USING p_product_id;
34220
+ IF j ? 'voorraad_article_vbn' AND jsonb_typeof(j->'voorraad_article_vbn') = 'object' THEN j := j->'voorraad_article_vbn'; END IF;
34221
+ j := coalesce(j, '{}'::jsonb);
34222
+ RETURN jsonb_build_object(
34223
+ 'pad', 'voorraad',
34224
+ 'bron', coalesce(j->>'bron', j->>'source', 'voorraad'),
34225
+ 's15_cm', coalesce(public.logistics_pos_num(j->>'s15_cm'), public.logistics_vbn_cm('S15', j->'features'->>'S15')),
34226
+ 's02_cm', coalesce(public.logistics_pos_num(j->>'s02_cm'),
34227
+ CASE WHEN j->>'s02_source' = 'handmatig' THEN public.logistics_pos_num(j->>'s02')
34228
+ ELSE public.logistics_vbn_cm('S02', coalesce(j->>'s02', j->'features'->>'S02')) END),
34229
+ 'vbn_package_code', CASE WHEN coalesce(j->>'vbn_package_code', j->'fust'->>'vbn_code') ~ '^[0-9]+$' THEN coalesce(j->>'vbn_package_code', j->'fust'->>'vbn_code')::int END,
34230
+ 'custom_package_id', nullif(coalesce(j->>'custom_package_id', j->'fust'->>'custom_package_id'), ''));
34231
+ END IF;
34232
+ RETURN public.logistics_artikel_vbn_terugval(p_product_id) || jsonb_build_object('pad', 'terugval');
34233
+ END $$;
34234
+ GRANT EXECUTE ON FUNCTION public.logistics_artikel_vbn(uuid) TO authenticated, service_role;
34235
+
34236
+ -- 1+2: maten per product in één bron. Fustkeuze: handmatig -> VBN-code (rij met de meeste maten) -> custom package.
34237
+ -- Maten per veld; ontbreekt een veld bij de gekozen rij, dan uit een andere rij met dezelfde fust_key.
34238
+ -- Planthoogte geldt altijd INCLUSIEF pot: handmatig -> S15 (transporthoogte) -> S02 (min. planthoogte incl. pot).
34239
+ CREATE OR REPLACE FUNCTION public.logistics_product_maten(p_product_ids uuid[])
34240
+ RETURNS TABLE(product_id uuid, product_name text, plants_per_crate integer, fust_type_id uuid, fust_name text, fust_bron text,
34241
+ fust_length_mm numeric, fust_width_mm numeric, fust_height_mm numeric,
34242
+ lengte_bron text, breedte_bron text, hoogte_bron text,
34243
+ plant_height_cm numeric, height_source text, weight_kg numeric,
34244
+ pad_fust text, pad_artikel text, missing_reason text)
34245
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34246
+ #variable_conflict use_column
34247
+ BEGIN
34248
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_product_maten');
34249
+ IF p_product_ids IS NOT NULL AND cardinality(p_product_ids) > 2000 THEN
34250
+ RAISE EXCEPTION 'Maximaal 2000 producten per aanvraag.' USING ERRCODE = '22023';
34251
+ END IF;
34252
+ RETURN QUERY
34253
+ WITH fm AS (SELECT * FROM public.logistics_fust_maten()),
34254
+ pr AS (
34255
+ SELECT p.id, p.product, p.plants_per_crate, p.fust_type_id AS hand_fid, p.plant_height_cm AS hand_h, p.weight,
34256
+ public.logistics_artikel_vbn(p.id) AS a
34257
+ FROM public.products p
34258
+ WHERE p_product_ids IS NULL OR p.id = ANY(p_product_ids)
34259
+ ),
34260
+ keuze AS (
34261
+ SELECT pr.*,
34262
+ (SELECT m.fust_type_id FROM fm m
34263
+ WHERE (pr.a->>'vbn_package_code') ~ '^[0-9]+$'
34264
+ AND (m.vbn_code = (pr.a->>'vbn_package_code')::int OR m.fust_key = 'vbn:' || (pr.a->>'vbn_package_code'))
34265
+ ORDER BY ((m.length_mm IS NOT NULL)::int + (m.width_mm IS NOT NULL)::int + (m.height_mm IS NOT NULL)::int) DESC, m.fust_type_id
34266
+ LIMIT 1) AS vbn_fid,
34267
+ (SELECT f.id FROM public.fust_types f
34268
+ WHERE nullif(pr.a->>'custom_package_id', '') IS NOT NULL AND f.custom_package_id = pr.a->>'custom_package_id'
34269
+ ORDER BY f.created_at, f.id LIMIT 1) AS cp_fid
34270
+ FROM pr
34271
+ ),
34272
+ k2 AS (
34273
+ SELECT k.*, coalesce(k.hand_fid, k.vbn_fid, k.cp_fid) AS fid,
34274
+ CASE WHEN k.hand_fid IS NOT NULL THEN 'handmatig' WHEN k.vbn_fid IS NOT NULL THEN 'vbn' WHEN k.cp_fid IS NOT NULL THEN 'floriday' END AS fbron
34275
+ FROM keuze k
34276
+ ),
34277
+ maat AS (
34278
+ SELECT k.*, m.name AS mname, m.pad AS mpad,
34279
+ coalesce(m.length_mm, lx.length_mm) AS l, CASE WHEN m.length_mm IS NOT NULL THEN m.lengte_bron ELSE coalesce(lx.lengte_bron, 'onbekend') END AS lb,
34280
+ coalesce(m.width_mm, wx.width_mm) AS w, CASE WHEN m.width_mm IS NOT NULL THEN m.breedte_bron ELSE coalesce(wx.breedte_bron, 'onbekend') END AS wb,
34281
+ coalesce(m.height_mm, hx.height_mm) AS h, CASE WHEN m.height_mm IS NOT NULL THEN m.hoogte_bron ELSE coalesce(hx.hoogte_bron, 'onbekend') END AS hb
34282
+ FROM k2 k
34283
+ LEFT JOIN fm m ON m.fust_type_id = k.fid
34284
+ LEFT JOIN LATERAL (SELECT m2.length_mm, m2.lengte_bron FROM fm m2 WHERE m.fust_key IS NOT NULL AND m2.fust_key = m.fust_key AND m2.length_mm IS NOT NULL ORDER BY m2.fust_type_id LIMIT 1) lx ON true
34285
+ LEFT JOIN LATERAL (SELECT m2.width_mm, m2.breedte_bron FROM fm m2 WHERE m.fust_key IS NOT NULL AND m2.fust_key = m.fust_key AND m2.width_mm IS NOT NULL ORDER BY m2.fust_type_id LIMIT 1) wx ON true
34286
+ LEFT JOIN LATERAL (SELECT m2.height_mm, m2.hoogte_bron FROM fm m2 WHERE m.fust_key IS NOT NULL AND m2.fust_key = m.fust_key AND m2.height_mm IS NOT NULL ORDER BY m2.fust_type_id LIMIT 1) hx ON true
34287
+ ),
34288
+ hoogte AS (
34289
+ SELECT x.*,
34290
+ public.logistics_pos_num(x.hand_h::text) AS hh,
34291
+ public.logistics_pos_num(x.a->>'s15_cm') AS s15,
34292
+ public.logistics_pos_num(x.a->>'s02_cm') AS s02,
34293
+ (SELECT b.bron FROM public.logistics_gegevens_bron b
34294
+ WHERE b.onderwerp = 'product' AND b.sleutel = x.id::text AND b.veld = 'plant_height_cm'
34295
+ AND public.logistics_pos_num(b.waarde) = public.logistics_pos_num(x.hand_h::text) LIMIT 1) AS hand_bron
34296
+ FROM maat x
34297
+ )
34298
+ SELECT h.id, h.product, h.plants_per_crate::int, h.fid, h.mname, h.fbron,
34299
+ h.l, h.w, h.h, h.lb, h.wb, h.hb,
34300
+ coalesce(h.hh, h.s15, h.s02),
34301
+ CASE WHEN h.hh IS NOT NULL THEN coalesce(h.hand_bron, 'handmatig')
34302
+ WHEN h.s15 IS NOT NULL THEN 'floricode_s15'
34303
+ WHEN h.s02 IS NOT NULL THEN 'floricode_s02' END,
34304
+ public.logistics_pos_num(regexp_replace(coalesce(h.weight::text, ''), '[^0-9.,]', '', 'g')),
34305
+ coalesce(h.mpad, (SELECT fm.pad FROM fm LIMIT 1)), h.a->>'pad',
34306
+ CASE
34307
+ WHEN h.fid IS NULL AND h.a->>'vbn_package_code' IS NULL AND h.a->>'custom_package_id' IS NULL
34308
+ THEN format('Bij "%s" is geen fustsoort bekend: kies de fustsoort bij het product in Voorraad.', h.product)
34309
+ WHEN h.fid IS NULL
34310
+ THEN format('Bij "%s" bestaat de fustsoort (VBN %s) niet in Voorraad → Fusten.', h.product, coalesce(h.a->>'vbn_package_code', h.a->>'custom_package_id'))
34311
+ WHEN h.l IS NULL OR h.w IS NULL OR h.h IS NULL
34312
+ THEN format('Fust %s heeft geen volledige maten in Floricode, Floriday of Voorraad → Fusten (ontbreekt: %s).', coalesce(h.mname, 'onbekend'),
34313
+ array_to_string(array_remove(ARRAY[CASE WHEN h.l IS NULL THEN 'lengte' END, CASE WHEN h.w IS NULL THEN 'breedte' END, CASE WHEN h.h IS NULL THEN 'hoogte' END], NULL), ', '))
34314
+ WHEN coalesce(h.hh, h.s15, h.s02) IS NULL
34315
+ THEN format('Bij "%s" is de planthoogte inclusief pot onbekend (geen S15/S02 in Floricode of Floriday, niet handmatig ingevuld).', h.product)
34316
+ END
34317
+ FROM hoogte h;
34318
+ END $$;
34319
+ GRANT EXECUTE ON FUNCTION public.logistics_product_maten(uuid[]) TO authenticated, service_role;
34320
+
34321
+ -- Bestaande view blijft met dezelfde kolommen werken; de bron is nu logistics_product_maten.
34322
+ DO $fs$ BEGIN
34323
+ EXECUTE $v$CREATE OR REPLACE VIEW public.logistics_product_dimensions AS
34324
+ SELECT m.product_id, m.product_name, m.plants_per_crate, m.fust_type_id,
34325
+ CASE m.fust_bron WHEN 'vbn' THEN 'floriday' ELSE m.fust_bron END AS fust_source,
34326
+ m.fust_name, m.fust_length_mm, m.fust_width_mm, m.fust_height_mm,
34327
+ m.plant_height_cm, m.height_source,
34328
+ CASE WHEN m.missing_reason IS NULL THEN coalesce(nullif(m.fust_bron, 'vbn'), 'floriday') ELSE 'onbekend' END AS dimension_source,
34329
+ m.missing_reason
34330
+ FROM public.logistics_product_maten(NULL) m$v$;
34331
+ EXCEPTION WHEN undefined_table OR undefined_column OR undefined_function OR invalid_table_definition THEN
34332
+ RAISE NOTICE 'logistiek-engine: view logistics_product_dimensions niet vervangen: %', SQLERRM;
34333
+ END $fs$;
34334
+
34335
+ -- 3+E4: gegevens voor karindeling, nu met bronnen uit logistics_product_maten.
34336
+ -- Gewicht per fust alleen gevraagd als de gebruikte ladingdrager een kg-grens heeft.
34337
+ -- Lagen per kar: laaghoogte = max(fusthoogte, planthoogte inclusief pot).
34338
+ CREATE OR REPLACE FUNCTION public.logistics_data_gereedheid(p_product_ids uuid[] DEFAULT NULL)
34339
+ RETURNS TABLE(product_id uuid, product text, ladingdrager text, methode text, in_open_order boolean,
34340
+ ontbrekend text[], velden jsonb, voorstel jsonb, uitleg text)
34341
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34342
+ #variable_conflict use_column
34343
+ DECLARE v_methode text; v_carrier text; c record; v_ids uuid[];
34344
+ BEGIN
34345
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_data_gereedheid');
34346
+ SELECT CASE WHEN coalesce(s.settings->'karrenindeling'->'fields'->>'cart_layout_method', s.settings->'karrenindeling'->>'cart_layout_method') = 'afmetingen'
34347
+ THEN 'afmetingen' ELSE 'belading' END,
34348
+ nullif(btrim(coalesce(s.settings->'karrenindeling'->'fields'->>'default_carrier_type', s.settings->'karrenindeling'->>'default_carrier_type')), '')
34349
+ INTO v_methode, v_carrier
34350
+ FROM public.logistics_rule_configs s WHERE s.is_active ORDER BY s.updated_at DESC LIMIT 1;
34351
+ v_methode := coalesce(v_methode, 'belading');
34352
+ SELECT t.code, t.name, t.layer_length_cm, t.layer_width_cm, t.max_height_cm, t.max_weight_kg INTO c
34353
+ FROM public.load_carrier_types t
34354
+ WHERE t.is_active AND (t.code = v_carrier OR (v_carrier IS NULL AND t.is_default))
34355
+ ORDER BY (t.code = v_carrier) DESC NULLS LAST LIMIT 1;
34356
+
34357
+ SELECT array_agg(DISTINCT x) INTO v_ids FROM (
34358
+ SELECT oi.product_id AS x FROM public.orders o JOIN public.order_items oi ON oi.order_id = o.id
34359
+ WHERE p_product_ids IS NULL AND o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.is_reservation, false) = false
34360
+ AND lower(coalesce(o.status, '')) = 'nog te picken' AND oi.product_id IS NOT NULL
34361
+ UNION SELECT unnest(p_product_ids)) q WHERE x IS NOT NULL;
34362
+ IF v_ids IS NULL THEN RETURN; END IF;
34363
+
34364
+ RETURN QUERY
34365
+ WITH open_p AS (
34366
+ SELECT DISTINCT oi.product_id FROM public.orders o JOIN public.order_items oi ON oi.order_id = o.id
34367
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.is_reservation, false) = false
34368
+ AND lower(coalesce(o.status, '')) = 'nog te picken' AND oi.product_id = ANY(v_ids)
34369
+ ),
34370
+ p AS (
34371
+ SELECT pr.id, pr.product, pr.plants_per_crate, pr.crates_per_layer, pr.layers_per_cart, pr.floriday_packing_source,
34372
+ mt.fust_type_id, mt.fust_name, mt.fust_bron, mt.fust_length_mm AS length_mm, mt.fust_width_mm AS width_mm, mt.fust_height_mm AS height_mm,
34373
+ mt.lengte_bron, mt.breedte_bron, mt.hoogte_bron, mt.plant_height_cm, mt.height_source, mt.weight_kg,
34374
+ (op.product_id IS NOT NULL) AS in_open
34375
+ FROM public.products pr
34376
+ JOIN public.logistics_product_maten(v_ids) mt ON mt.product_id = pr.id
34377
+ LEFT JOIN open_p op ON op.product_id = pr.id
34378
+ ),
34379
+ m AS (
34380
+ SELECT p.*,
34381
+ array_remove(ARRAY[
34382
+ CASE WHEN coalesce(p.plants_per_crate, 0) <= 0 THEN 'plants_per_crate' END,
34383
+ CASE WHEN v_methode = 'belading' AND coalesce(p.crates_per_layer, 0) <= 0 THEN 'crates_per_layer' END,
34384
+ CASE WHEN v_methode = 'belading' AND coalesce(p.layers_per_cart, 0) <= 0 THEN 'layers_per_cart' END,
34385
+ CASE WHEN v_methode = 'afmetingen' AND p.fust_type_id IS NULL THEN 'fust_type_id' END,
34386
+ CASE WHEN v_methode = 'afmetingen' AND p.fust_type_id IS NOT NULL AND (p.length_mm IS NULL OR p.width_mm IS NULL OR p.height_mm IS NULL) THEN 'fust_maten' END,
34387
+ CASE WHEN v_methode = 'afmetingen' AND p.plant_height_cm IS NULL THEN 'plant_height_cm' END,
34388
+ CASE WHEN coalesce(c.max_weight_kg, 0) > 0 AND p.weight_kg IS NULL THEN 'weight' END
34389
+ ], NULL) AS mist,
34390
+ CASE WHEN p.length_mm > 0 AND p.width_mm > 0 AND c.layer_length_cm > 0 AND c.layer_width_cm > 0 THEN
34391
+ greatest(floor(c.layer_length_cm * 10 / p.length_mm) * floor(c.layer_width_cm * 10 / p.width_mm),
34392
+ floor(c.layer_length_cm * 10 / p.width_mm) * floor(c.layer_width_cm * 10 / p.length_mm))::int END AS calc_cpl,
34393
+ CASE WHEN p.plant_height_cm > 0 AND c.max_height_cm > 0 THEN
34394
+ floor(c.max_height_cm / greatest(p.plant_height_cm, coalesce(p.height_mm, 0) / 10.0))::int END AS calc_lpc
34395
+ FROM p
34396
+ )
34397
+ SELECT m.id, m.product, c.code, v_methode, m.in_open, m.mist,
34398
+ jsonb_build_object(
34399
+ 'plants_per_crate', jsonb_build_object('waarde', m.plants_per_crate, 'bron', CASE WHEN m.plants_per_crate > 0 THEN coalesce(m.floriday_packing_source, 'product') END),
34400
+ 'crates_per_layer', jsonb_build_object('waarde', m.crates_per_layer, 'bron', CASE WHEN m.crates_per_layer > 0 THEN coalesce(m.floriday_packing_source, 'product') END),
34401
+ 'layers_per_cart', jsonb_build_object('waarde', m.layers_per_cart, 'bron', CASE WHEN m.layers_per_cart > 0 THEN coalesce(m.floriday_packing_source, 'product') END),
34402
+ 'weight', jsonb_build_object('waarde', m.weight_kg, 'bron', CASE WHEN m.weight_kg IS NOT NULL THEN 'product' END,
34403
+ 'nodig', coalesce(c.max_weight_kg, 0) > 0, 'grens_kg', c.max_weight_kg),
34404
+ 'plant_height_cm', jsonb_build_object('waarde', m.plant_height_cm, 'bron', m.height_source, 'inclusief_pot', true),
34405
+ 'fust_type_id', jsonb_build_object('waarde', m.fust_type_id, 'bron', m.fust_bron, 'naam', m.fust_name),
34406
+ 'fust_length_mm', jsonb_build_object('waarde', m.length_mm, 'bron', m.lengte_bron),
34407
+ 'fust_width_mm', jsonb_build_object('waarde', m.width_mm, 'bron', m.breedte_bron),
34408
+ 'fust_height_mm', jsonb_build_object('waarde', m.height_mm, 'bron', m.hoogte_bron)),
34409
+ jsonb_strip_nulls(jsonb_build_object(
34410
+ 'crates_per_layer', CASE WHEN m.calc_cpl > 0 THEN jsonb_build_object('waarde', m.calc_cpl, 'bron', 'berekend',
34411
+ 'berekening', format('fust %s×%s mm op laag %s×%s cm (%s)', m.length_mm, m.width_mm, c.layer_length_cm, c.layer_width_cm, c.name)) END,
34412
+ 'layers_per_cart', CASE WHEN m.calc_lpc > 0 THEN jsonb_build_object('waarde', m.calc_lpc, 'bron', 'berekend',
34413
+ 'berekening', format('hoogte %s cm / grootste van (plant incl. pot %s cm, fust %s cm)', c.max_height_cm, m.plant_height_cm, round(coalesce(m.height_mm, 0) / 10.0, 1))) END)),
34414
+ CASE WHEN cardinality(m.mist) = 0 THEN 'Compleet.'
34415
+ ELSE format('Bij "%s" ontbreekt: %s. Vul dit in bij het product (Voorraad) of hier met "Invullen".', m.product,
34416
+ array_to_string(ARRAY(SELECT CASE f WHEN 'plants_per_crate' THEN 'planten per fust' WHEN 'crates_per_layer' THEN 'fusten per laag'
34417
+ WHEN 'layers_per_cart' THEN 'lagen per kar' WHEN 'fust_type_id' THEN 'fustsoort' WHEN 'fust_maten' THEN 'fustmaten'
34418
+ WHEN 'plant_height_cm' THEN 'planthoogte inclusief pot'
34419
+ WHEN 'weight' THEN format('gewicht per fust (%s heeft een grens van %s kg)', c.name, c.max_weight_kg) ELSE f END FROM unnest(m.mist) f), ', ')) END
34420
+ FROM m
34421
+ ORDER BY cardinality(m.mist) DESC, m.in_open DESC, m.product
34422
+ LIMIT 2000;
34423
+ END $$;
34424
+ GRANT EXECUTE ON FUNCTION public.logistics_data_gereedheid(uuid[]) TO authenticated, service_role;
34425
+
34426
+ -- 5: "Alles overnemen uit Floricode/Floriday": alleen lege velden, bron vastgelegd.
34427
+ -- p_uitvoeren = false geeft alleen de voorvertoning.
34428
+ CREATE OR REPLACE FUNCTION public.logistics_data_overnemen(p_uitvoeren boolean)
34429
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
34430
+ #variable_conflict use_column
34431
+ DECLARE v_lijst jsonb := '[]'::jsonb; r record; v_n int := 0; v_ids uuid[];
34432
+ BEGIN
34433
+ PERFORM public.logistics_acc_guard(CASE WHEN p_uitvoeren THEN 'beheer' ELSE 'ingelogd' END, 'logistics_data_overnemen');
34434
+ SELECT array_agg(DISTINCT oi.product_id) INTO v_ids FROM public.orders o JOIN public.order_items oi ON oi.order_id = o.id
34435
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.is_reservation, false) = false
34436
+ AND lower(coalesce(o.status, '')) = 'nog te picken' AND oi.product_id IS NOT NULL;
34437
+ IF v_ids IS NULL THEN RETURN jsonb_build_object('uitgevoerd', false, 'velden', v_lijst, 'aantal', 0); END IF;
34438
+ FOR r IN
34439
+ SELECT m.*, p.fust_type_id AS hand_fid, p.plant_height_cm AS hand_h, f.length_mm AS f_l, f.width_mm AS f_w, f.height_mm AS f_h
34440
+ FROM public.logistics_product_maten(v_ids) m
34441
+ JOIN public.products p ON p.id = m.product_id
34442
+ LEFT JOIN public.fust_types f ON f.id = m.fust_type_id
34443
+ LOOP
34444
+ IF r.hand_h IS NULL AND r.height_source IN ('floricode_s15', 'floricode_s02') THEN
34445
+ v_lijst := v_lijst || jsonb_build_object('onderwerp', 'product', 'sleutel', r.product_id, 'naam', r.product_name, 'veld', 'plant_height_cm', 'waarde', r.plant_height_cm, 'bron', r.height_source);
34446
+ IF p_uitvoeren THEN
34447
+ UPDATE public.products SET plant_height_cm = r.plant_height_cm, updated_at = now() WHERE id = r.product_id AND plant_height_cm IS NULL;
34448
+ END IF;
34449
+ END IF;
34450
+ IF r.hand_fid IS NULL AND r.fust_type_id IS NOT NULL THEN
34451
+ v_lijst := v_lijst || jsonb_build_object('onderwerp', 'product', 'sleutel', r.product_id, 'naam', r.product_name, 'veld', 'fust_type_id', 'waarde', r.fust_type_id,
34452
+ 'bron', CASE r.fust_bron WHEN 'vbn' THEN 'floriday_vbn' ELSE r.fust_bron END);
34453
+ IF p_uitvoeren THEN
34454
+ UPDATE public.products SET fust_type_id = r.fust_type_id, updated_at = now() WHERE id = r.product_id AND fust_type_id IS NULL;
34455
+ END IF;
34456
+ END IF;
34457
+ IF r.fust_type_id IS NOT NULL AND r.f_l IS NULL AND r.fust_length_mm IS NOT NULL AND r.lengte_bron IN ('floricode', 'floriday') THEN
34458
+ v_lijst := v_lijst || jsonb_build_object('onderwerp', 'fust', 'sleutel', r.fust_type_id, 'naam', r.fust_name, 'veld', 'length_mm', 'waarde', r.fust_length_mm, 'bron', r.lengte_bron);
34459
+ IF p_uitvoeren THEN UPDATE public.fust_types SET length_mm = round(r.fust_length_mm)::int, updated_at = now() WHERE id = r.fust_type_id AND length_mm IS NULL; END IF;
34460
+ END IF;
34461
+ IF r.fust_type_id IS NOT NULL AND r.f_w IS NULL AND r.fust_width_mm IS NOT NULL AND r.breedte_bron IN ('floricode', 'floriday') THEN
34462
+ v_lijst := v_lijst || jsonb_build_object('onderwerp', 'fust', 'sleutel', r.fust_type_id, 'naam', r.fust_name, 'veld', 'width_mm', 'waarde', r.fust_width_mm, 'bron', r.breedte_bron);
34463
+ IF p_uitvoeren THEN UPDATE public.fust_types SET width_mm = round(r.fust_width_mm)::int, updated_at = now() WHERE id = r.fust_type_id AND width_mm IS NULL; END IF;
34464
+ END IF;
34465
+ IF r.fust_type_id IS NOT NULL AND r.f_h IS NULL AND r.fust_height_mm IS NOT NULL AND r.hoogte_bron IN ('floricode', 'floriday') THEN
34466
+ v_lijst := v_lijst || jsonb_build_object('onderwerp', 'fust', 'sleutel', r.fust_type_id, 'naam', r.fust_name, 'veld', 'height_mm', 'waarde', r.fust_height_mm, 'bron', r.hoogte_bron);
34467
+ IF p_uitvoeren THEN UPDATE public.fust_types SET height_mm = round(r.fust_height_mm)::int, updated_at = now() WHERE id = r.fust_type_id AND height_mm IS NULL; END IF;
34468
+ END IF;
34469
+ END LOOP;
34470
+ -- Ontdubbelen (meerdere producten met hetzelfde fust).
34471
+ SELECT coalesce(jsonb_agg(DISTINCT e), '[]'::jsonb) INTO v_lijst FROM jsonb_array_elements(v_lijst) e;
34472
+ IF p_uitvoeren THEN
34473
+ INSERT INTO public.logistics_gegevens_bron(onderwerp, sleutel, veld, waarde, bron, toelichting, vastgelegd_door)
34474
+ SELECT e->>'onderwerp', e->>'sleutel', e->>'veld', e->>'waarde', e->>'bron', 'Overgenomen via de startlijst', auth.uid()
34475
+ FROM jsonb_array_elements(v_lijst) e
34476
+ ON CONFLICT (onderwerp, sleutel, veld) DO UPDATE SET waarde = EXCLUDED.waarde, bron = EXCLUDED.bron, toelichting = EXCLUDED.toelichting,
34477
+ vastgelegd_door = EXCLUDED.vastgelegd_door, vastgelegd_op = now();
34478
+ END IF;
34479
+ v_n := jsonb_array_length(v_lijst);
34480
+ RETURN jsonb_build_object('uitgevoerd', p_uitvoeren, 'aantal', v_n, 'velden', v_lijst,
34481
+ 'per_bron', (SELECT coalesce(jsonb_object_agg(b, n), '{}'::jsonb) FROM (SELECT e->>'bron' b, count(*) n FROM jsonb_array_elements(v_lijst) e GROUP BY 1) q));
34482
+ END $$;
34483
+ GRANT EXECUTE ON FUNCTION public.logistics_data_overnemen(boolean) TO authenticated, service_role;
34484
+
34485
+ -- 4: algemene standaard-laagmaten per Floriday-ladingdragertype (vaste, klant-onafhankelijke referentie).
34486
+ -- Alleen lengte en breedte; hoogte en gewichtsgrens hebben geen algemene standaard en vult de beheerder zelf in.
34487
+ CREATE OR REPLACE FUNCTION public.logistics_ladingdrager_referentie()
34488
+ RETURNS TABLE(floriday_code text, lengte_cm integer, breedte_cm integer, bron text)
34489
+ LANGUAGE sql IMMUTABLE SET search_path TO 'public' AS $$
34490
+ VALUES ('DANISH_TROLLEY', 135, 56, 'Standaard Deense container (CC), 1350×565 mm'),
34491
+ ('DANISH_TROLLEY_NON_CC_RFID', 135, 56, 'Standaard Deense container, 1350×565 mm'),
34492
+ ('DANISH_TROLLEY_WATER_TRAY', 135, 56, 'Standaard Deense container met waterbak, 1350×565 mm'),
34493
+ ('AUCTION_TROLLEY', 135, 56, 'Standaard veilingstapelwagen, 1350×565 mm'),
34494
+ ('EURO_PALLET', 120, 80, 'Europallet (EPAL), 1200×800 mm'),
34495
+ ('HT_EURO_PALLET', 120, 80, 'Europallet (EPAL, HT), 1200×800 mm'),
34496
+ ('BLOCK_PALLET', 120, 100, 'Blokpallet, 1200×1000 mm')
34497
+ $$;
34498
+ GRANT EXECUTE ON FUNCTION public.logistics_ladingdrager_referentie() TO authenticated, service_role;
34499
+
34500
+ CREATE OR REPLACE FUNCTION public.logistics_ladingdrager_voorstellen()
34501
+ RETURNS TABLE(code text, naam text, floriday_code text, veld text, huidig numeric, voorstel numeric, bron text, actie text)
34502
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34503
+ #variable_conflict use_column
34504
+ BEGIN
34505
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_ladingdrager_voorstellen');
34506
+ RETURN QUERY
34507
+ WITH k AS (
34508
+ SELECT DISTINCT ON (t.code) t.code, t.name, m.floriday_code, t.layer_length_cm, t.layer_width_cm, r.lengte_cm, r.breedte_cm, r.bron
34509
+ FROM public.load_carrier_types t
34510
+ JOIN public.logistics_floriday_carrier_map m ON m.carrier_code = t.code
34511
+ JOIN public.logistics_ladingdrager_referentie() r ON r.floriday_code = m.floriday_code
34512
+ WHERE t.is_active
34513
+ ORDER BY t.code, m.floriday_code
34514
+ )
34515
+ SELECT k.code, k.name, k.floriday_code, v.veld, v.huidig, v.voorstel, k.bron,
34516
+ CASE WHEN coalesce(v.huidig, 0) <= 0 THEN 'invullen' WHEN v.huidig = v.voorstel THEN 'gelijk' ELSE 'afwijkend' END
34517
+ FROM k CROSS JOIN LATERAL (VALUES ('layer_length_cm', k.layer_length_cm::numeric, k.lengte_cm::numeric),
34518
+ ('layer_width_cm', k.layer_width_cm::numeric, k.breedte_cm::numeric)) v(veld, huidig, voorstel)
34519
+ ORDER BY k.code, v.veld;
34520
+ END $$;
34521
+ GRANT EXECUTE ON FUNCTION public.logistics_ladingdrager_voorstellen() TO authenticated, service_role;
34522
+
34523
+ -- Bevestigen per ladingdrager: schrijft alleen lege velden en legt de bron vast. Overschrijft nooit.
34524
+ CREATE OR REPLACE FUNCTION public.logistics_ladingdrager_voorstel_bevestigen(p_code text)
34525
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
34526
+ #variable_conflict use_column
34527
+ DECLARE r record; v_n int := 0;
34528
+ BEGIN
34529
+ PERFORM public.logistics_acc_guard('beheer', 'logistics_ladingdrager_voorstel_bevestigen');
34530
+ FOR r IN SELECT * FROM public.logistics_ladingdrager_voorstellen() v WHERE v.code = p_code AND v.actie = 'invullen' LOOP
34531
+ IF r.veld = 'layer_length_cm' THEN
34532
+ UPDATE public.load_carrier_types SET layer_length_cm = r.voorstel::int, updated_at = now() WHERE code = p_code AND coalesce(layer_length_cm, 0) <= 0;
34533
+ ELSIF r.veld = 'layer_width_cm' THEN
34534
+ UPDATE public.load_carrier_types SET layer_width_cm = r.voorstel::int, updated_at = now() WHERE code = p_code AND coalesce(layer_width_cm, 0) <= 0;
34535
+ END IF;
34536
+ IF FOUND THEN
34537
+ v_n := v_n + 1;
34538
+ INSERT INTO public.logistics_gegevens_bron(onderwerp, sleutel, veld, waarde, bron, toelichting, vastgelegd_door)
34539
+ VALUES ('ladingdrager', p_code, r.veld, r.voorstel::text, 'standaard:' || r.floriday_code, r.bron, auth.uid())
34540
+ ON CONFLICT (onderwerp, sleutel, veld) DO UPDATE SET waarde = EXCLUDED.waarde, bron = EXCLUDED.bron, toelichting = EXCLUDED.toelichting,
34541
+ vastgelegd_door = EXCLUDED.vastgelegd_door, vastgelegd_op = now();
34542
+ END IF;
34543
+ END LOOP;
34544
+ RETURN jsonb_build_object('ok', true, 'code', p_code, 'ingevuld', v_n);
34545
+ END $$;
34546
+ GRANT EXECUTE ON FUNCTION public.logistics_ladingdrager_voorstel_bevestigen(text) TO authenticated, service_role;
34547
+
34548
+ -- 5: startlijst voor een nieuwe shell.
34549
+ CREATE OR REPLACE FUNCTION public.logistics_startlijst()
34550
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34551
+ DECLARE v_drag int; v_drag_mist int; v_werk boolean; v_prod int; v_prod_mist int;
34552
+ BEGIN
34553
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_startlijst');
34554
+ SELECT count(*), count(*) FILTER (WHERE coalesce(t.layer_length_cm, 0) <= 0 OR coalesce(t.layer_width_cm, 0) <= 0 OR coalesce(t.max_height_cm, 0) <= 0)
34555
+ INTO v_drag, v_drag_mist
34556
+ FROM public.load_carrier_types t
34557
+ WHERE t.is_active AND NOT EXISTS (SELECT 1 FROM public.logistics_floriday_carrier_map m WHERE m.carrier_code = t.code AND m.floriday_code = 'NONE');
34558
+ SELECT EXISTS (SELECT 1 FROM public.logistics_rule_configs s WHERE s.is_active AND s.settings ? 'karrenindeling') INTO v_werk;
34559
+ SELECT count(*), count(*) FILTER (WHERE cardinality(g.ontbrekend) > 0) INTO v_prod, v_prod_mist FROM public.logistics_data_gereedheid(NULL) g;
34560
+ RETURN jsonb_build_object(
34561
+ 'ladingdragers', jsonb_build_object('klaar', v_drag > 0 AND v_drag_mist = 0, 'aantal', v_drag, 'onvolledig', v_drag_mist),
34562
+ 'werkwijze', jsonb_build_object('klaar', v_werk),
34563
+ 'gereedheid', jsonb_build_object('klaar', v_prod_mist = 0, 'producten', v_prod, 'onvolledig', v_prod_mist));
34564
+ END $$;
34565
+ GRANT EXECUTE ON FUNCTION public.logistics_startlijst() TO authenticated, service_role;
34566
+
34567
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_198()
34568
+ RETURNS TABLE(controle text, ok boolean, toelichting text)
34569
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34570
+ BEGIN
34571
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_install_check_198');
34572
+ RETURN QUERY SELECT 'Engine: fustmaten uit één bron'::text,
34573
+ to_regprocedure('public.logistics_fust_maten()') IS NOT NULL,
34574
+ CASE WHEN to_regprocedure('public.voorraad_fust_dimensions()') IS NOT NULL THEN 'via Voorraad (voorraad_fust_dimensions)'
34575
+ ELSE 'eigen terugval: Floricode → Floriday → handmatig' END;
34576
+ RETURN QUERY SELECT 'Engine: artikelkenmerken (S15/S02, fust)',
34577
+ to_regprocedure('public.logistics_artikel_vbn(uuid)') IS NOT NULL,
34578
+ CASE WHEN to_regprocedure('public.voorraad_article_vbn(uuid)') IS NOT NULL THEN 'via Voorraad (voorraad_article_vbn)'
34579
+ ELSE 'eigen terugval: Floriday-handelsitem' END;
34580
+ RETURN QUERY SELECT 'Engine: view logistics_product_dimensions gebruikt logistics_product_maten',
34581
+ coalesce(pg_get_viewdef('public.logistics_product_dimensions'::regclass) LIKE '%logistics_product_maten%', false), 'view';
34582
+ RETURN QUERY SELECT 'Engine: gereedheid vraagt gewicht alleen bij kg-grens',
34583
+ coalesce(pg_get_functiondef('public.logistics_data_gereedheid(uuid[])'::regprocedure) LIKE '%max_weight_kg%', false), 'logistics_data_gereedheid';
34584
+ RETURN QUERY SELECT 'Engine: startlijst en standaardmaten ladingdragers',
34585
+ to_regprocedure('public.logistics_startlijst()') IS NOT NULL AND to_regprocedure('public.logistics_ladingdrager_voorstel_bevestigen(text)') IS NOT NULL,
34586
+ 'logistics_startlijst + logistics_ladingdrager_voorstel_bevestigen';
34587
+ RETURN QUERY SELECT 'Engine: bronregistratie', to_regclass('public.logistics_gegevens_bron') IS NOT NULL, 'logistics_gegevens_bron';
34588
+ EXCEPTION WHEN undefined_table OR undefined_function THEN
34589
+ RETURN QUERY SELECT 'Engine: maten 1.0.198'::text, false, SQLERRM;
34590
+ END $$;
34591
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_198() TO authenticated, service_role;
34592
+
34593
+ DO $do$ DECLARE d text;
34594
+ BEGIN
34595
+ d := pg_get_functiondef('public.logistics_install_check()'::regprocedure);
34596
+ IF position('logistics_install_check_198' in d) = 0 THEN
34597
+ d := replace(d, $r$ RETURN QUERY SELECT * FROM public.logistics_access_check();$r$,
34598
+ $r$ RETURN QUERY SELECT * FROM public.logistics_install_check_198();
34599
+ RETURN QUERY SELECT * FROM public.logistics_access_check();$r$);
34600
+ EXECUTE d;
34601
+ END IF;
34602
+ EXCEPTION WHEN undefined_function THEN NULL;
34603
+ END $do$;