@flowselections/logistiek-engine 1.0.219 → 1.0.224

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.
@@ -37647,4 +37647,1328 @@ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY D
37647
37647
  AND position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_order_transport_group(jsonb)'::regprocedure)) > 0,
37648
37648
  'Aanroeper wordt intern gecontroleerd'
37649
37649
  $$;
37650
- GRANT EXECUTE ON FUNCTION public.logistics_install_check_218() TO authenticated, service_role;
37650
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_218() TO authenticated, service_role;
37651
+ -- ===== 1.0.220 =====
37652
+ -- ===== 1.0.220 reden per verouderde kar, één buffertelling, eigen vervoer in Dagdetails =====
37653
+
37654
+ -- Kar gemaakt voor een eerdere gecombineerde order (de order zit nu in een andere buffer). Alleen melding.
37655
+ CREATE OR REPLACE FUNCTION public.logistics_cart_eerdere_buffer(p_cart_id uuid)
37656
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path TO 'public'
37657
+ AS $function$
37658
+ WITH k AS (SELECT c.id, c.order_id, c.buffer_id FROM public.order_carts c WHERE c.id = p_cart_id),
37659
+ o AS (
37660
+ SELECT x.order_number, x.buffer_id FROM k
37661
+ JOIN public.order_cart_items ci ON ci.cart_id = k.id
37662
+ JOIN public.order_items oi ON oi.id = ci.order_item_id
37663
+ JOIN public.orders x ON x.id = oi.order_id
37664
+ WHERE x.deleted_at IS NULL
37665
+ UNION
37666
+ SELECT x.order_number, x.buffer_id FROM k JOIN public.orders x ON x.id = k.order_id WHERE x.deleted_at IS NULL
37667
+ ),
37668
+ a AS (SELECT DISTINCT o.order_number FROM o, k
37669
+ WHERE o.buffer_id IS DISTINCT FROM k.buffer_id AND (o.buffer_id IS NOT NULL OR k.order_id IS NULL))
37670
+ SELECT CASE WHEN count(*) > 0 THEN jsonb_build_object('orders', jsonb_agg(order_number ORDER BY order_number),
37671
+ 'tekst', 'Deze kar is ingedeeld voor een eerdere gecombineerde order; '
37672
+ || string_agg(order_number, ', ' ORDER BY order_number)
37673
+ || CASE WHEN count(*) > 1 THEN ' zitten' ELSE ' zit' END || ' nu in een andere gecombineerde order.') END
37674
+ FROM a
37675
+ $function$;
37676
+
37677
+ -- Alle redenen waarom een kar verouderd is, in gewone taal, in vaste volgorde (eerste = hoofdreden).
37678
+ CREATE OR REPLACE FUNCTION public.logistics_cart_verouderd_redenen(p_cart_id uuid)
37679
+ RETURNS jsonb LANGUAGE plpgsql STABLE SET search_path TO 'public'
37680
+ AS $function$
37681
+ DECLARE v_nr text; v_n int; v_live int; v_afw text[]; v_r jsonb; v_out jsonb := '[]'::jsonb;
37682
+ BEGIN
37683
+ IF p_cart_id IS NULL THEN RETURN v_out; END IF;
37684
+ SELECT coalesce(cart_number::text, '?') INTO v_nr FROM public.order_carts WHERE id = p_cart_id;
37685
+ IF NOT FOUND THEN RETURN v_out; END IF;
37686
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'order_items' AND column_name = 'quarantined_at') THEN
37687
+ SELECT count(*) INTO v_n FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
37688
+ WHERE ci.cart_id = p_cart_id AND oi.quarantined_at IS NOT NULL;
37689
+ IF coalesce(v_n, 0) > 0 THEN
37690
+ v_out := v_out || jsonb_build_array(jsonb_build_object('code', 'quarantaine',
37691
+ 'tekst', format('%s regel(s) op kar %s staan in quarantaine bij Verkoop.', v_n, v_nr),
37692
+ 'extra', jsonb_build_object('regelsInQuarantaine', v_n),
37693
+ 'blokkade', jsonb_build_object('code', 'kar_verouderd',
37694
+ 'tekst', format('Kar %s heeft %s regel(s) die Verkoop in quarantaine heeft gezet. Deel de niet-geprinte karren opnieuw in.', v_nr, v_n))));
37695
+ END IF;
37696
+ SELECT count(*) INTO v_live FROM public.order_cart_items ci LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
37697
+ WHERE ci.cart_id = p_cart_id AND coalesce(ci.crates, 0) > 0 AND oi.quarantined_at IS NULL;
37698
+ ELSE
37699
+ SELECT count(*) INTO v_live FROM public.order_cart_items ci WHERE ci.cart_id = p_cart_id AND coalesce(ci.crates, 0) > 0;
37700
+ END IF;
37701
+ IF coalesce(v_live, 0) = 0 THEN
37702
+ RETURN v_out || jsonb_build_array(jsonb_build_object('code', 'leeg',
37703
+ 'tekst', format('Kar %s heeft geen regels meer.', v_nr),
37704
+ 'blokkade', jsonb_build_object('code', 'kar_leeg',
37705
+ 'tekst', format('Kar %s heeft geen regels meer. Deel de niet-geprinte karren opnieuw in.', v_nr))));
37706
+ END IF;
37707
+ v_afw := public.logistics_cart_aantal_afwijking_core(p_cart_id);
37708
+ IF coalesce(cardinality(v_afw), 0) > 0 THEN
37709
+ v_out := v_out || jsonb_build_array(jsonb_build_object('code', 'aantal',
37710
+ 'tekst', format('De aantallen op de kar wijken af van de order (%s).', array_to_string(v_afw, ', ')),
37711
+ 'extra', jsonb_build_object('producten', to_jsonb(v_afw)),
37712
+ 'blokkade', jsonb_build_object('code', 'kar_verouderd',
37713
+ 'tekst', format('Kar %s: de aantallen wijken af van de order (%s). Deel de niet-geprinte karren opnieuw in.', v_nr, array_to_string(v_afw, ', ')))));
37714
+ END IF;
37715
+ v_r := public.logistics_cart_vervoer_gemengd(p_cart_id);
37716
+ IF v_r IS NOT NULL THEN
37717
+ v_out := v_out || jsonb_build_array(jsonb_build_object('code', 'vervoer_gemengd', 'tekst', v_r->>'tekst', 'blokkade', NULL));
37718
+ END IF;
37719
+ v_r := public.logistics_cart_eerdere_buffer(p_cart_id);
37720
+ IF v_r IS NOT NULL THEN
37721
+ v_out := v_out || jsonb_build_array(jsonb_build_object('code', 'eerdere_buffer', 'tekst', v_r->>'tekst', 'blokkade', NULL));
37722
+ END IF;
37723
+ v_r := public.logistics_cart_regels_verouderd_core(p_cart_id);
37724
+ IF v_r IS NOT NULL THEN
37725
+ v_out := v_out || jsonb_build_array(jsonb_build_object('code', v_r->>'reden', 'tekst', v_r->>'tekst', 'blokkade', NULL));
37726
+ END IF;
37727
+ RETURN v_out;
37728
+ END $function$;
37729
+
37730
+ -- Hoofdreden blijft gelijk aan vóór 1.0.220 (zelfde volgorde: quarantaine, leeg, aantal,
37731
+ -- logistics_cart_vervoer_gemengd, regels); nieuw: eerdere buffer en de volledige lijst 'redenen'.
37732
+ CREATE OR REPLACE FUNCTION public.logistics_cart_verouderd_core(p_cart_id uuid)
37733
+ RETURNS jsonb LANGUAGE plpgsql STABLE SET search_path TO 'public'
37734
+ AS $function$
37735
+ DECLARE v_lijst jsonb; v_r jsonb;
37736
+ BEGIN
37737
+ IF p_cart_id IS NULL THEN RETURN NULL; END IF;
37738
+ v_lijst := public.logistics_cart_verouderd_redenen(p_cart_id);
37739
+ IF coalesce(jsonb_array_length(v_lijst), 0) = 0 THEN RETURN NULL; END IF;
37740
+ v_r := v_lijst->0;
37741
+ RETURN jsonb_build_object('verouderd', true, 'reden', v_r->>'code', 'tekst', v_r->>'tekst',
37742
+ 'blokkade', v_r->'blokkade',
37743
+ 'redenen', (SELECT jsonb_agg(e - 'extra' - 'blokkade') FROM jsonb_array_elements(v_lijst) e))
37744
+ || coalesce(v_r->'extra', '{}'::jsonb);
37745
+ END $function$;
37746
+
37747
+ -- Kaartgegevens krijgen naast verouderdReden ook de volledige lijst verouderdRedenen.
37748
+ DO $fs$
37749
+ DECLARE v_def text;
37750
+ BEGIN
37751
+ v_def := pg_get_functiondef('public.logistics_cart_platen_verrijk'::regproc);
37752
+ IF position('verouderdRedenen' IN v_def) = 0 THEN
37753
+ IF position('''verouderdReden'', public.logistics_cart_verouderd_core(v_id)->>''tekst'')' IN v_def) = 0 THEN
37754
+ RAISE EXCEPTION 'logistics_cart_platen_verrijk: verwachte tekst niet gevonden; niet aangepast';
37755
+ END IF;
37756
+ v_def := replace(v_def, '''verouderdReden'', public.logistics_cart_verouderd_core(v_id)->>''tekst'')',
37757
+ '''verouderdReden'', public.logistics_cart_verouderd_core(v_id)->>''tekst'', ''verouderdRedenen'', public.logistics_cart_verouderd_core(v_id)->''redenen'')');
37758
+ EXECUTE v_def;
37759
+ END IF;
37760
+ END $fs$;
37761
+
37762
+ -- Eén bron voor de karren van een gecombineerde order: kop, karindeling en pickdocumenten.
37763
+ CREATE OR REPLACE FUNCTION public.logistics_buffer_carts(p_buffer_id uuid)
37764
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
37765
+ AS $function$
37766
+ DECLARE v_ids uuid[]; v_refs text[]; v_carts jsonb; v_types jsonb; v_dubbel jsonb;
37767
+ BEGIN
37768
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_buffer_carts');
37769
+ IF p_buffer_id IS NULL THEN RETURN NULL; END IF;
37770
+ SELECT array_agg(o.id ORDER BY o.order_number), array_agg(o.order_number ORDER BY o.order_number) INTO v_ids, v_refs
37771
+ FROM public.orders o
37772
+ WHERE o.buffer_id = p_buffer_id AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
37773
+ AND coalesce(o.status, '') <> 'geannuleerd';
37774
+ IF v_ids IS NULL THEN
37775
+ RETURN jsonb_build_object('bufferId', p_buffer_id, 'orders', '[]'::jsonb, 'karren', 0, 'ladingdragers', '[]'::jsonb,
37776
+ 'carts', '[]'::jsonb, 'dubbeleDocumenten', '[]'::jsonb);
37777
+ END IF;
37778
+ WITH u AS (SELECT * FROM public.logistics_order_unit_carts_core(v_ids)),
37779
+ d AS (
37780
+ SELECT DISTINCT ON (q.reference) q.reference, q.id, q.document_number, q.printed_at
37781
+ FROM public.pick_queue q
37782
+ WHERE q.cancelled_at IS NULL AND q.reference IN (SELECT u.cart_id::text FROM u)
37783
+ ORDER BY q.reference, q.created_at DESC
37784
+ )
37785
+ SELECT coalesce(jsonb_agg(jsonb_build_object(
37786
+ 'cartId', u.cart_id, 'seq', u.display_seq, 'total', u.display_total,
37787
+ 'storedCartNumber', u.cart_number, 'cartBufferId', u.buffer_id, 'orderId', u.order_id,
37788
+ 'carrierType', c.carrier_type,
37789
+ 'ladingdrager', coalesce((SELECT coalesce(t.name, t.code) FROM public.load_carrier_types t WHERE t.code = c.carrier_type LIMIT 1), c.carrier_type),
37790
+ 'verouderd', v.v IS NOT NULL, 'verouderdReden', v.v->>'tekst', 'verouderdRedenen', coalesce(v.v->'redenen', '[]'::jsonb),
37791
+ 'pickDocument', CASE WHEN d.id IS NULL THEN NULL ELSE jsonb_build_object('id', d.id, 'documentNumber', d.document_number, 'printedAt', d.printed_at) END)
37792
+ ORDER BY u.display_seq), '[]'::jsonb)
37793
+ INTO v_carts
37794
+ FROM u JOIN public.order_carts c ON c.id = u.cart_id
37795
+ LEFT JOIN LATERAL (SELECT public.logistics_cart_verouderd_core(u.cart_id) AS v) v ON true
37796
+ LEFT JOIN d ON d.reference = u.cart_id::text;
37797
+
37798
+ SELECT coalesce(jsonb_agg(jsonb_build_object('ladingdrager', x->>'ladingdrager', 'aantal', n) ORDER BY n DESC, x->>'ladingdrager'), '[]'::jsonb)
37799
+ INTO v_types
37800
+ FROM (SELECT jsonb_build_object('ladingdrager', e->>'ladingdrager') x, count(*) n FROM jsonb_array_elements(v_carts) e GROUP BY 1) s;
37801
+
37802
+ -- Open pickdocumenten van deze orders met hetzelfde nummer (alleen melden, niets wijzigen).
37803
+ SELECT coalesce(jsonb_agg(jsonb_build_object('documentNumber', g.document_number, 'documenten', g.docs)), '[]'::jsonb)
37804
+ INTO v_dubbel
37805
+ FROM (SELECT q.document_number,
37806
+ jsonb_agg(jsonb_build_object('id', q.id, 'cartId', q.reference, 'aangemaakt', q.created_at,
37807
+ 'hoortBijIndeling', EXISTS (SELECT 1 FROM jsonb_array_elements(v_carts) e WHERE e->>'cartId' = q.reference)) ORDER BY q.created_at) AS docs
37808
+ FROM public.pick_queue q
37809
+ WHERE q.cancelled_at IS NULL AND q.order_number = ANY(v_refs) AND q.document_number IS NOT NULL
37810
+ GROUP BY q.document_number HAVING count(DISTINCT q.reference) > 1) g;
37811
+
37812
+ RETURN jsonb_build_object('bufferId', p_buffer_id, 'orders', to_jsonb(v_refs),
37813
+ 'karren', jsonb_array_length(v_carts), 'ladingdragers', v_types, 'carts', v_carts, 'dubbeleDocumenten', v_dubbel);
37814
+ END $function$;
37815
+ REVOKE ALL ON FUNCTION public.logistics_buffer_carts(uuid) FROM PUBLIC, anon;
37816
+ GRANT EXECUTE ON FUNCTION public.logistics_buffer_carts(uuid) TO authenticated, service_role;
37817
+
37818
+ -- buffer_cart_count telt exact dezelfde karren als logistics_buffer_carts (logistics_order_unit_carts_core).
37819
+ CREATE OR REPLACE FUNCTION public.logistics_order_plan_coverage_list(p_order_ids uuid[])
37820
+ RETURNS TABLE(order_id uuid, order_number text, buffer_id uuid, state text, label text, progress_pct integer, reason text, cart_count integer, pick_row_count integer, pick_status text, picked_at timestamp with time zone, buffer_order_count integer, buffer_cart_count integer)
37821
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
37822
+ AS $function$
37823
+ SELECT public.logistics_acc_guard('ingelogd', 'logistics_order_plan_coverage_list');
37824
+ WITH b AS (SELECT * FROM public.logistics_order_plan_coverage_list_v152(p_order_ids)),
37825
+ mem AS (
37826
+ SELECT DISTINCT b.buffer_id AS buf, o.id AS oid FROM b
37827
+ JOIN public.orders o ON o.buffer_id = b.buffer_id
37828
+ WHERE b.buffer_id IS NOT NULL AND o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.status,'') <> 'geannuleerd'
37829
+ ),
37830
+ bc AS (
37831
+ SELECT m.buf, (SELECT count(*) FROM public.logistics_order_unit_carts_core(array_agg(m.oid)))::int AS n
37832
+ FROM mem m GROUP BY m.buf
37833
+ )
37834
+ SELECT b.order_id, b.order_number, b.buffer_id,
37835
+ CASE WHEN x.full_fulfilled THEN 'afgehandeld_extern' ELSE b.state END,
37836
+ CASE WHEN x.full_fulfilled THEN 'Afgehandeld in Floriday' ELSE b.label END,
37837
+ b.progress_pct,
37838
+ CASE WHEN x.full_fulfilled THEN 'De order is in Floriday afgehandeld; de Engine plant en print er niets meer voor.' ELSE b.reason END,
37839
+ b.cart_count, b.pick_row_count, b.pick_status, b.picked_at,
37840
+ CASE WHEN b.buffer_id IS NULL THEN 1 ELSE greatest((SELECT count(*) FROM mem WHERE mem.buf = b.buffer_id)::int, 1) END,
37841
+ CASE WHEN b.buffer_id IS NULL THEN b.cart_count ELSE coalesce((SELECT bc.n FROM bc WHERE bc.buf = b.buffer_id), 0) END
37842
+ FROM b LEFT JOIN public.logistics_external_fulfillment x ON x.order_id = b.order_id
37843
+ $function$;
37844
+
37845
+ -- Dagdag van een order: de pickdag, maar nooit later dan de leverdag. Een order waarvan de leverdag
37846
+ -- in het verleden is gezet (bijv. HO-2601467 op 28-10, gewijzigd op 29-10) blijft zo op zijn leverdag staan.
37847
+ CREATE OR REPLACE FUNCTION public.logistics_order_dag(p_planned_start timestamptz, p_delivery_date text)
37848
+ RETURNS date LANGUAGE sql STABLE SET search_path TO 'public'
37849
+ AS $function$
37850
+ SELECT CASE
37851
+ WHEN p_delivery_date ~ '^\d{4}-\d{2}-\d{2}' AND p_planned_start IS NOT NULL
37852
+ THEN least((p_planned_start AT TIME ZONE public.logistics_tz())::date, left(p_delivery_date, 10)::date)
37853
+ WHEN p_planned_start IS NOT NULL THEN (p_planned_start AT TIME ZONE public.logistics_tz())::date
37854
+ WHEN p_delivery_date ~ '^\d{4}-\d{2}-\d{2}' THEN left(p_delivery_date, 10)::date
37855
+ END
37856
+ $function$;
37857
+
37858
+ CREATE OR REPLACE FUNCTION public.logistics_capacity_day_load(p_from date, p_to date, p_user uuid, p_settings jsonb)
37859
+ RETURNS TABLE(d date, carts integer, required numeric, zonder_kar integer)
37860
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
37861
+ AS $function$
37862
+ WITH t0 AS (
37863
+ SELECT DISTINCT ON (t.order_ref) t.order_ref, t.planned_start
37864
+ FROM public.logistics_tasks t
37865
+ WHERE t.task_type = 'pick.execute' AND t.status NOT IN ('done','cancelled') AND NOT t.shadow
37866
+ AND t.planned_start IS NOT NULL AND (p_user IS NULL OR t.user_id = p_user)
37867
+ AND (t.planned_start AT TIME ZONE public.logistics_tz())::date >= p_from
37868
+ ORDER BY t.order_ref, t.planned_start
37869
+ ),
37870
+ t AS (
37871
+ SELECT t0.order_ref, public.logistics_order_dag(t0.planned_start, o.delivery_date) AS d, o.id
37872
+ FROM t0 JOIN public.orders o ON o.order_number = t0.order_ref AND o.deleted_at IS NULL
37873
+ ),
37874
+ o AS (SELECT t.d, t.order_ref, t.id FROM t WHERE t.d BETWEEN p_from AND p_to),
37875
+ k AS (
37876
+ SELECT DISTINCT ON (u.cart_id) u.cart_id, o.d
37877
+ FROM o, LATERAL public.logistics_order_unit_carts_core(ARRAY[o.id]) u
37878
+ ORDER BY u.cart_id, o.d
37879
+ ),
37880
+ kc AS (
37881
+ SELECT k.d, count(*)::int AS carts,
37882
+ SUM(public.logistics_carrier_minutes(COALESCE(c.carrier_type, 'trolley'), p_settings)) AS mins
37883
+ FROM k JOIN public.order_carts c ON c.id = k.cart_id GROUP BY k.d
37884
+ ),
37885
+ zk AS (
37886
+ SELECT o.d, count(*)::int AS n FROM o
37887
+ WHERE NOT EXISTS (SELECT 1 FROM public.logistics_order_unit_carts_core(ARRAY[o.id]) u)
37888
+ GROUP BY o.d
37889
+ ),
37890
+ dd AS (SELECT DISTINCT o.d FROM o)
37891
+ SELECT dd.d, COALESCE(kc.carts, 0), COALESCE(NULLIF(kc.mins, 0), COALESCE(kc.carts, 0) * 15)::numeric, COALESCE(zk.n, 0)
37892
+ FROM dd LEFT JOIN kc ON kc.d = dd.d LEFT JOIN zk ON zk.d = dd.d
37893
+ ORDER BY dd.d;
37894
+ $function$;
37895
+
37896
+ -- Alle open orders van één dag (ook eigen vervoer, klant haalt op en orders zonder kar), met reden.
37897
+ CREATE OR REPLACE FUNCTION public.logistics_capacity_day_orders(p_date date)
37898
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
37899
+ AS $function$
37900
+ DECLARE v jsonb;
37901
+ BEGIN
37902
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_capacity_day_orders');
37903
+ IF p_date IS NULL THEN RETURN '[]'::jsonb; END IF;
37904
+ WITH t AS (
37905
+ SELECT DISTINCT ON (t.order_ref) t.order_ref, t.planned_start
37906
+ FROM public.logistics_tasks t
37907
+ WHERE t.task_type = 'pick.execute' AND t.status NOT IN ('done','cancelled') AND NOT t.shadow AND t.planned_start IS NOT NULL
37908
+ ORDER BY t.order_ref, t.planned_start
37909
+ ),
37910
+ o AS (
37911
+ SELECT x.*, t.planned_start, public.logistics_order_dag(t.planned_start, x.delivery_date) AS dag
37912
+ FROM public.orders x LEFT JOIN t ON t.order_ref = x.order_number
37913
+ WHERE x.deleted_at IS NULL AND x.cancelled_at IS NULL
37914
+ AND lower(coalesce(x.status,'')) NOT IN ('cancelled','geannuleerd','deleted','verwijderd','afgehandeld','completed','delivered','geleverd')
37915
+ AND (t.order_ref IS NOT NULL OR x.delivery_date ~ '^\d{4}-\d{2}-\d{2}')
37916
+ ),
37917
+ r AS (
37918
+ SELECT o.*, (SELECT count(*) FROM public.logistics_order_unit_carts_core(ARRAY[o.id]))::int AS karren,
37919
+ public.logistics_order_transport_group(to_jsonb(o)) AS grp
37920
+ FROM o WHERE o.dag = p_date
37921
+ )
37922
+ SELECT coalesce(jsonb_agg(jsonb_build_object(
37923
+ 'orderId', r.id, 'orderNumber', r.order_number, 'bufferId', r.buffer_id,
37924
+ 'deliveryDate', left(r.delivery_date, 10), 'pickStart', r.planned_start,
37925
+ 'transportGroup', r.grp,
37926
+ 'vervoer', CASE r.grp WHEN 'eigen_vervoer' THEN 'Eigen vervoer (wij leveren)' WHEN 'klant_haalt_op' THEN 'Klant haalt op' ELSE 'Vervoerder' END,
37927
+ 'karren', r.karren, 'zonderKar', r.karren = 0,
37928
+ 'reden', CASE WHEN r.karren > 0 THEN NULL
37929
+ WHEN r.planned_start IS NULL THEN 'Nog niet ingepland: er staat geen picktaak voor deze order.'
37930
+ ELSE 'Ingepland, maar zonder kar: de karindeling ontbreekt. De bewaking deelt hem opnieuw in.' END)
37931
+ ORDER BY r.buffer_id NULLS LAST, r.order_number), '[]'::jsonb)
37932
+ INTO v FROM r;
37933
+ RETURN v;
37934
+ END $function$;
37935
+ REVOKE ALL ON FUNCTION public.logistics_capacity_day_orders(date) FROM PUBLIC, anon;
37936
+ GRANT EXECUTE ON FUNCTION public.logistics_capacity_day_orders(date) TO authenticated, service_role;
37937
+
37938
+ -- Bewaking zonder kar: ook een open order waarvan de leverdag net in het verleden is gezet
37939
+ -- (max. 7 dagen terug en in de laatste 24 uur gewijzigd), zoals HO-2601467.
37940
+ CREATE OR REPLACE FUNCTION public.logistics_open_orders_zonder_kar()
37941
+ RETURNS TABLE(order_id uuid, order_number text, delivery_date text, laatste_reden text)
37942
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
37943
+ AS $function$
37944
+ SELECT public.logistics_acc_guard('ingelogd', 'logistics_open_orders_zonder_kar');
37945
+ WITH lq AS (
37946
+ SELECT DISTINCT ON (q.order_id) q.order_id, q.status, q.reason_code, q.error
37947
+ FROM public.logistics_plan_queue q ORDER BY q.order_id, q.updated_at DESC
37948
+ )
37949
+ SELECT o.id, o.order_number, o.delivery_date, lq.error
37950
+ FROM public.orders o
37951
+ LEFT JOIN lq ON lq.order_id = o.id
37952
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
37953
+ AND lower(coalesce(o.status,'')) NOT IN ('cancelled','geannuleerd','deleted','verwijderd','afgehandeld','completed','delivered','geleverd')
37954
+ AND o.delivery_date ~ '^\d{4}-\d{2}-\d{2}'
37955
+ AND (left(o.delivery_date,10)::date >= public.logistics_local_today()
37956
+ OR (left(o.delivery_date,10)::date >= public.logistics_local_today() - 7 AND o.updated_at > now() - interval '24 hours'))
37957
+ AND EXISTS (SELECT 1 FROM public.order_items oi WHERE oi.order_id = o.id)
37958
+ AND NOT coalesce(public.logistics_order_buffer_excluded(o.id), false)
37959
+ AND NOT (coalesce(lq.status,'') IN ('geblokkeerd','pending','running'))
37960
+ AND NOT (coalesce(lq.status,'') = 'gearchiveerd'
37961
+ AND coalesce(lq.reason_code,'') IN ('belading_onwaarschijnlijk','belading_onvolledig','belading_onbekend','artikel_kiezen','ladingdrager_onbekend'))
37962
+ AND NOT EXISTS (SELECT 1 FROM logistics_active.order_carts c WHERE c.order_id = o.id)
37963
+ AND NOT EXISTS (SELECT 1 FROM public.order_items oi
37964
+ JOIN logistics_active.order_cart_items ci ON ci.order_item_id = oi.id
37965
+ JOIN logistics_active.order_carts c ON c.id = ci.cart_id
37966
+ WHERE oi.order_id = o.id);
37967
+ $function$;
37968
+
37969
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_220()
37970
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
37971
+ SELECT 'redenen per kar', position('logistics_cart_verouderd_redenen' IN pg_get_functiondef('public.logistics_cart_verouderd_core(uuid)'::regprocedure)) > 0,
37972
+ 'Verouderde kar geeft alle redenen in gewone taal'
37973
+ UNION ALL SELECT 'kaart toont redenen', position('verouderdRedenen' IN pg_get_functiondef('public.logistics_cart_platen_verrijk'::regproc)) > 0,
37974
+ 'Kaartgegevens bevatten verouderdRedenen'
37975
+ UNION ALL SELECT 'één buffertelling', position('logistics_order_unit_carts_core' IN pg_get_functiondef('public.logistics_order_plan_coverage_list(uuid[])'::regprocedure)) > 0
37976
+ AND to_regprocedure('public.logistics_buffer_carts(uuid)') IS NOT NULL,
37977
+ 'Kop, karindeling en pickdocumenten uit dezelfde karren'
37978
+ UNION ALL SELECT 'dagdag niet na leverdag', position('logistics_order_dag' IN pg_get_functiondef('public.logistics_capacity_day_load(date,date,uuid,jsonb)'::regprocedure)) > 0,
37979
+ 'Order staat op zijn pickdag, nooit later dan de leverdag'
37980
+ UNION ALL SELECT 'dagorders', to_regprocedure('public.logistics_capacity_day_orders(date)') IS NOT NULL,
37981
+ 'Alle open orders van een dag, ook eigen vervoer en zonder kar'
37982
+ UNION ALL SELECT 'bewaking recente datumwijziging', position('interval ''24 hours''' IN pg_get_functiondef('public.logistics_open_orders_zonder_kar()'::regprocedure)) > 0,
37983
+ 'Order met net in het verleden gezette leverdag wordt opnieuw ingedeeld'
37984
+ $$;
37985
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_220() TO authenticated, service_role;
37986
+
37987
+ -- ===== 1.0.221 bevroren verdelingen, karren uit eerdere buffer, status geannuleerd =====
37988
+
37989
+ -- ---------- 1+2. Bevroren verdeling ongeldig? (inactieve groep / niet-canonieke sleutel) ----------
37990
+ CREATE OR REPLACE FUNCTION public.logistics_allocation_stale_reden(p_order_item_id uuid)
37991
+ RETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
37992
+ AS $function$
37993
+ DECLARE v_pid uuid; v_qty numeric; v_items text[]; v_groups uuid[]; v_err boolean; r record;
37994
+ BEGIN
37995
+ IF p_order_item_id IS NULL THEN RETURN NULL; END IF;
37996
+ SELECT oi.product_id, COALESCE(oi.quantity, 0)::numeric INTO v_pid, v_qty
37997
+ FROM public.order_items oi WHERE oi.id = p_order_item_id;
37998
+ IF v_pid IS NULL OR v_qty <= 0 THEN RETURN NULL; END IF;
37999
+ IF NOT EXISTS (SELECT 1 FROM public.logistics_reservation_allocations a
38000
+ WHERE a.order_item_id = p_order_item_id AND a.source IN ('item', 'group')) THEN
38001
+ RETURN NULL;
38002
+ END IF;
38003
+ SELECT array_agg(public.logistics_reserved_key(e.ref)) FILTER (WHERE e.kind = 'item'),
38004
+ array_agg(e.ref::uuid) FILTER (WHERE e.kind = 'group'),
38005
+ bool_or(e.kind = 'error')
38006
+ INTO v_items, v_groups, v_err
38007
+ FROM public.logistics_expand_line(v_pid, v_qty) e;
38008
+ IF COALESCE(v_err, false) THEN RETURN NULL; END IF;
38009
+ FOR r IN SELECT a.source, a.group_id, a.trade_item_key FROM public.logistics_reservation_allocations a
38010
+ WHERE a.order_item_id = p_order_item_id AND a.source IN ('item', 'group') LOOP
38011
+ IF r.source = 'group' THEN
38012
+ IF r.group_id IS NULL OR NOT EXISTS (SELECT 1 FROM public.product_groups g WHERE g.id = r.group_id AND g.is_active) THEN
38013
+ RETURN 'Verdeeld via een groep die niet meer actief is';
38014
+ END IF;
38015
+ IF NOT (r.group_id = ANY (COALESCE(v_groups, '{}'::uuid[]))) THEN
38016
+ RETURN 'Verdeeld via een groep die niet meer bij dit artikel hoort';
38017
+ END IF;
38018
+ IF NOT EXISTS (SELECT 1 FROM public.product_group_items pgi
38019
+ WHERE pgi.product_group_id = r.group_id
38020
+ AND public.logistics_reserved_key(public.resolve_group_member_stock_key(pgi.trade_item_id)) = r.trade_item_key) THEN
38021
+ RETURN 'Groepsregel wijst naar een voorraadregel die geen lid (meer) is';
38022
+ END IF;
38023
+ ELSE
38024
+ IF NOT (r.trade_item_key = ANY (COALESCE(v_items, '{}'::text[]))) THEN
38025
+ RETURN 'Sleutel is niet de canonieke voorraadregel van het artikel';
38026
+ END IF;
38027
+ END IF;
38028
+ END LOOP;
38029
+ RETURN NULL;
38030
+ END $function$;
38031
+
38032
+ CREATE OR REPLACE FUNCTION public.logistics_allocation_stale(p_order_item_id uuid)
38033
+ RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
38034
+ AS $function$ SELECT public.logistics_allocation_stale_reden(p_order_item_id) IS NOT NULL $function$;
38035
+
38036
+ REVOKE ALL ON FUNCTION public.logistics_allocation_stale_reden(uuid) FROM PUBLIC, anon, authenticated;
38037
+ REVOKE ALL ON FUNCTION public.logistics_allocation_stale(uuid) FROM PUBLIC, anon, authenticated;
38038
+ GRANT EXECUTE ON FUNCTION public.logistics_allocation_stale_reden(uuid) TO service_role;
38039
+ GRANT EXECUTE ON FUNCTION public.logistics_allocation_stale(uuid) TO service_role;
38040
+
38041
+ -- Bestaande (bevroren) verdeling alleen teruggeven als hij nog geldig is.
38042
+ CREATE OR REPLACE FUNCTION public.logistics_order_line_allocation(p_order_item_id uuid)
38043
+ RETURNS TABLE(trade_item_key text, qty numeric, source text, group_id uuid, note text)
38044
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
38045
+ AS $function$
38046
+ DECLARE
38047
+ v_oi record; part record; member_rec record;
38048
+ v_key text; v_last text; v_rem numeric; v_av numeric; v_take numeric; v_err text;
38049
+ BEGIN
38050
+ PERFORM public.logistics_acc_guard('server', 'logistics_order_line_allocation');
38051
+ IF p_order_item_id IS NULL THEN RETURN; END IF;
38052
+
38053
+ IF EXISTS (SELECT 1 FROM public.logistics_double_group_lines(
38054
+ (SELECT oi.order_id FROM public.order_items oi WHERE oi.id = p_order_item_id)) d
38055
+ WHERE d.order_item_id = p_order_item_id) THEN
38056
+ RETURN;
38057
+ END IF;
38058
+
38059
+ IF EXISTS (SELECT 1 FROM public.logistics_reservation_allocations a WHERE a.order_item_id = p_order_item_id)
38060
+ AND NOT public.logistics_allocation_stale(p_order_item_id) THEN
38061
+ RETURN QUERY
38062
+ SELECT a.trade_item_key, a.qty, a.source, a.group_id, NULL::text
38063
+ FROM public.logistics_reservation_allocations a
38064
+ WHERE a.order_item_id = p_order_item_id
38065
+ ORDER BY a.source, a.trade_item_key;
38066
+ RETURN;
38067
+ END IF;
38068
+
38069
+ SELECT oi.id, oi.product_id, COALESCE(oi.quantity, 0)::numeric AS quantity
38070
+ INTO v_oi FROM public.order_items oi WHERE oi.id = p_order_item_id;
38071
+ IF v_oi.id IS NULL OR v_oi.product_id IS NULL OR v_oi.quantity <= 0 THEN RETURN; END IF;
38072
+
38073
+ SELECT string_agg(e.note, '; ') INTO v_err
38074
+ FROM public.logistics_expand_line(v_oi.product_id, v_oi.quantity) e WHERE e.kind = 'error';
38075
+ IF v_err IS NOT NULL THEN
38076
+ RETURN QUERY SELECT NULL::text, 0::numeric, 'error'::text, NULL::uuid, v_err;
38077
+ RETURN;
38078
+ END IF;
38079
+
38080
+ FOR part IN SELECT e.kind, e.ref, e.qty FROM public.logistics_expand_line(v_oi.product_id, v_oi.quantity) e LOOP
38081
+ IF part.kind = 'item' THEN
38082
+ v_key := public.logistics_reserved_key(part.ref);
38083
+ IF v_key IS NULL THEN
38084
+ RETURN QUERY SELECT part.ref, part.qty, 'error'::text, NULL::uuid, 'geen voorraadrij'::text;
38085
+ ELSE
38086
+ RETURN QUERY SELECT v_key, part.qty, 'item'::text, NULL::uuid, NULL::text;
38087
+ END IF;
38088
+ ELSE
38089
+ v_rem := part.qty;
38090
+ v_last := NULL;
38091
+ FOR member_rec IN
38092
+ SELECT public.resolve_group_member_stock_key(pgi.trade_item_id) AS raw_key
38093
+ FROM public.product_group_items pgi
38094
+ WHERE pgi.product_group_id = part.ref::uuid
38095
+ AND public.resolve_group_member_stock_key(pgi.trade_item_id) IS NOT NULL
38096
+ AND public.resolve_group_member_stock_key(pgi.trade_item_id) NOT LIKE 'group:%'
38097
+ ORDER BY pgi.sort_order ASC NULLS LAST, pgi.created_at ASC, pgi.id ASC
38098
+ LOOP
38099
+ v_key := public.logistics_reserved_key(member_rec.raw_key);
38100
+ IF v_key IS NULL THEN CONTINUE; END IF;
38101
+ v_last := v_key;
38102
+ IF v_rem <= 0 THEN CONTINUE; END IF;
38103
+ SELECT GREATEST(COALESCE(MAX(s.physical_stock), 0)
38104
+ - COALESCE((SELECT SUM(a.qty) FROM public.logistics_reservation_allocations a
38105
+ WHERE a.trade_item_key = v_key
38106
+ AND a.order_item_id <> p_order_item_id), 0), 0)
38107
+ INTO v_av
38108
+ FROM public.trade_item_stock s WHERE s.trade_item_key = v_key;
38109
+ IF COALESCE(v_av, 0) <= 0 THEN CONTINUE; END IF;
38110
+ v_take := LEAST(v_av, v_rem);
38111
+ RETURN QUERY SELECT v_key, v_take, 'group'::text, part.ref::uuid, NULL::text;
38112
+ v_rem := v_rem - v_take;
38113
+ END LOOP;
38114
+ IF v_rem > 0 THEN
38115
+ IF v_last IS NULL THEN
38116
+ RETURN QUERY SELECT ('group:' || part.ref)::text, v_rem, 'error'::text, part.ref::uuid, 'groep zonder lid met voorraadrij'::text;
38117
+ ELSE
38118
+ RETURN QUERY SELECT v_last, v_rem, 'group'::text, part.ref::uuid, NULL::text;
38119
+ END IF;
38120
+ END IF;
38121
+ END IF;
38122
+ END LOOP;
38123
+ END
38124
+ $function$;
38125
+
38126
+ -- Gerichte herberekening: een ongeldige bevroren verdeling telt niet als bevroren.
38127
+ DO $do$
38128
+ DECLARE d text; n text;
38129
+ BEGIN
38130
+ d := pg_get_functiondef('public.logistics_recompute_reserved_targeted(uuid)'::regprocedure);
38131
+ IF position('logistics_allocation_stale(o.order_item_id)' IN d) > 0 THEN RETURN; END IF;
38132
+ n := replace(d, 'AND public.logistics_allocation_frozen(o.order_id)',
38133
+ 'AND public.logistics_allocation_frozen(o.order_id) AND NOT public.logistics_allocation_stale(o.order_item_id)');
38134
+ IF n = d THEN RAISE EXCEPTION '1.0.221: bevroren-stap in logistics_recompute_reserved_targeted niet gevonden'; END IF;
38135
+ EXECUTE n;
38136
+ END $do$;
38137
+
38138
+ -- Herstel: ongeldige bevroren verdelingen van open orders opnieuw maken (droog of uitvoeren).
38139
+ CREATE OR REPLACE FUNCTION public.logistics_reservation_repair_stale_core(p_uitvoeren boolean, p_order_item_ids uuid[])
38140
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
38141
+ AS $function$
38142
+ DECLARE r record; v_old jsonb; v_new jsonb; v_regels jsonb := '[]'::jsonb; v_keys text[] := '{}'; v_n int := 0;
38143
+ BEGIN
38144
+ FOR r IN
38145
+ SELECT x.order_item_id, o.order_number, oi.quantity, oi.product_name AS artikel,
38146
+ public.logistics_allocation_stale_reden(x.order_item_id) AS reden
38147
+ FROM (SELECT DISTINCT a.order_item_id FROM public.logistics_reservation_allocations a
38148
+ WHERE a.source IN ('item', 'group')
38149
+ AND (p_order_item_ids IS NULL OR a.order_item_id = ANY (p_order_item_ids))) x
38150
+ JOIN public.order_items oi ON oi.id = x.order_item_id
38151
+ JOIN public.orders o ON o.id = oi.order_id
38152
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
38153
+ AND NOT public.logistics_order_is_shipped_status(o.status)
38154
+ AND NOT EXISTS (SELECT 1 FROM public.stock_mutation_log sml
38155
+ WHERE sml.order_item_id = x.order_item_id AND sml.mutation_type = 'consumption')
38156
+ ORDER BY o.order_number
38157
+ LOOP
38158
+ CONTINUE WHEN r.reden IS NULL;
38159
+ v_n := v_n + 1;
38160
+ SELECT jsonb_agg(jsonb_build_object('sleutel', a.trade_item_key, 'bron', a.source, 'groep', a.group_id, 'aantal', a.qty)
38161
+ ORDER BY a.source, a.trade_item_key)
38162
+ INTO v_old FROM public.logistics_reservation_allocations a WHERE a.order_item_id = r.order_item_id;
38163
+ SELECT jsonb_agg(jsonb_build_object('sleutel', n.trade_item_key, 'bron', n.source, 'groep', n.group_id, 'aantal', n.qty, 'melding', n.note))
38164
+ INTO v_new FROM public.logistics_order_line_allocation(r.order_item_id) n;
38165
+ v_regels := v_regels || jsonb_build_object('order', r.order_number, 'artikel', r.artikel, 'orderRegel', r.order_item_id,
38166
+ 'besteld', r.quantity, 'reden', r.reden, 'nu', COALESCE(v_old, '[]'::jsonb), 'nieuw', COALESCE(v_new, '[]'::jsonb));
38167
+ v_keys := v_keys
38168
+ || ARRAY(SELECT e->>'sleutel' FROM jsonb_array_elements(COALESCE(v_old, '[]'::jsonb)) e WHERE e->>'sleutel' IS NOT NULL)
38169
+ || ARRAY(SELECT e->>'sleutel' FROM jsonb_array_elements(COALESCE(v_new, '[]'::jsonb)) e WHERE e->>'sleutel' IS NOT NULL);
38170
+ IF p_uitvoeren THEN
38171
+ DELETE FROM public.logistics_reservation_allocations WHERE order_item_id = r.order_item_id;
38172
+ INSERT INTO public.logistics_reservation_allocations (order_item_id, trade_item_key, qty, source, group_id, line_qty)
38173
+ SELECT r.order_item_id, e->>'sleutel', (e->>'aantal')::numeric, e->>'bron', NULLIF(e->>'groep', '')::uuid, r.quantity
38174
+ FROM jsonb_array_elements(COALESCE(v_new, '[]'::jsonb)) e
38175
+ WHERE e->>'sleutel' IS NOT NULL;
38176
+ END IF;
38177
+ END LOOP;
38178
+ RETURN jsonb_build_object('uitgevoerd', p_uitvoeren, 'aantalRegels', v_n,
38179
+ 'sleutels', (SELECT COALESCE(jsonb_agg(DISTINCT k ORDER BY k), '[]'::jsonb) FROM unnest(v_keys) k),
38180
+ 'regels', v_regels);
38181
+ END $function$;
38182
+ REVOKE ALL ON FUNCTION public.logistics_reservation_repair_stale_core(boolean, uuid[]) FROM PUBLIC, anon, authenticated;
38183
+ GRANT EXECUTE ON FUNCTION public.logistics_reservation_repair_stale_core(boolean, uuid[]) TO service_role;
38184
+
38185
+ CREATE OR REPLACE FUNCTION public.logistics_reservation_repair_stale(p_uitvoeren boolean DEFAULT false)
38186
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
38187
+ AS $function$
38188
+ BEGIN
38189
+ IF NOT (public.logistics_acc_is_server() OR public.logistics_is_beheer()) THEN
38190
+ RAISE EXCEPTION 'Geen toegang: alleen beheer of de server mag verdelingen herstellen.' USING ERRCODE = '42501';
38191
+ END IF;
38192
+ RETURN public.logistics_reservation_repair_stale_core(COALESCE(p_uitvoeren, false), NULL);
38193
+ END $function$;
38194
+ REVOKE ALL ON FUNCTION public.logistics_reservation_repair_stale(boolean) FROM PUBLIC, anon;
38195
+ GRANT EXECUTE ON FUNCTION public.logistics_reservation_repair_stale(boolean) TO authenticated, service_role;
38196
+
38197
+ -- Groep op inactief: regels die via die groep verdeeld zijn direct opnieuw maken (nooit blokkeren).
38198
+ CREATE OR REPLACE FUNCTION public.logistics_group_inactive_repair_trg()
38199
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
38200
+ AS $function$
38201
+ DECLARE v_ids uuid[];
38202
+ BEGIN
38203
+ IF COALESCE(OLD.is_active, false) AND NOT COALESCE(NEW.is_active, false) THEN
38204
+ SELECT array_agg(DISTINCT a.order_item_id) INTO v_ids
38205
+ FROM public.logistics_reservation_allocations a WHERE a.group_id = NEW.id;
38206
+ IF v_ids IS NOT NULL THEN
38207
+ BEGIN
38208
+ PERFORM public.logistics_reservation_repair_stale_core(true, v_ids);
38209
+ EXCEPTION WHEN OTHERS THEN
38210
+ RAISE WARNING 'Verdelingen van groep % niet direct hersteld: %', NEW.id, SQLERRM;
38211
+ END;
38212
+ END IF;
38213
+ END IF;
38214
+ RETURN NULL;
38215
+ END $function$;
38216
+ DROP TRIGGER IF EXISTS trg_logistics_group_inactive_repair ON public.product_groups;
38217
+ CREATE TRIGGER trg_logistics_group_inactive_repair AFTER UPDATE OF is_active ON public.product_groups
38218
+ FOR EACH ROW EXECUTE FUNCTION public.logistics_group_inactive_repair_trg();
38219
+
38220
+ -- ---------- 6. cancelled_at gevuld => status geannuleerd ----------
38221
+ INSERT INTO public.pick_document_statuses (code, name, color, sort_order, is_open, is_terminal, active)
38222
+ VALUES ('cancelled', 'Geannuleerd', '#ef4444', 90, false, true, true)
38223
+ ON CONFLICT (code) DO NOTHING;
38224
+
38225
+ CREATE OR REPLACE FUNCTION public.logistics_pick_queue_cancel_status_trg()
38226
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
38227
+ AS $function$
38228
+ DECLARE v_id uuid;
38229
+ BEGIN
38230
+ IF NEW.cancelled_at IS NULL THEN RETURN NEW; END IF;
38231
+ SELECT s.id INTO v_id FROM public.pick_document_statuses s WHERE s.code = 'cancelled';
38232
+ IF v_id IS NULL THEN
38233
+ RAISE EXCEPTION 'Pickdocumentstatus "cancelled" (Geannuleerd) ontbreekt; annuleren kan de status niet zetten.';
38234
+ END IF;
38235
+ IF NEW.status_id IS DISTINCT FROM v_id THEN NEW.status_id := v_id; END IF;
38236
+ RETURN NEW;
38237
+ END $function$;
38238
+ DROP TRIGGER IF EXISTS trg_logistics_pick_queue_cancel_status ON public.pick_queue;
38239
+ CREATE TRIGGER trg_logistics_pick_queue_cancel_status BEFORE INSERT OR UPDATE OF cancelled_at, status_id ON public.pick_queue
38240
+ FOR EACH ROW EXECUTE FUNCTION public.logistics_pick_queue_cancel_status_trg();
38241
+
38242
+ CREATE OR REPLACE FUNCTION public.logistics_pick_queue_fix_cancelled_status(p_uitvoeren boolean DEFAULT false)
38243
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
38244
+ AS $function$
38245
+ DECLARE v_id uuid; v_list jsonb; v_n int := 0;
38246
+ BEGIN
38247
+ IF NOT (public.logistics_acc_is_server() OR public.logistics_is_beheer()) THEN
38248
+ RAISE EXCEPTION 'Geen toegang: alleen beheer of de server mag pickdocumenten rechtzetten.' USING ERRCODE = '42501';
38249
+ END IF;
38250
+ SELECT s.id INTO v_id FROM public.pick_document_statuses s WHERE s.code = 'cancelled';
38251
+ IF v_id IS NULL THEN RAISE EXCEPTION 'Pickdocumentstatus "cancelled" ontbreekt.'; END IF;
38252
+ SELECT jsonb_agg(jsonb_build_object('id', q.id, 'document', q.document_number, 'order', q.order_number,
38253
+ 'statusNu', s.name, 'geannuleerdOp', q.cancelled_at) ORDER BY q.document_number, q.created_at)
38254
+ INTO v_list
38255
+ FROM public.pick_queue q LEFT JOIN public.pick_document_statuses s ON s.id = q.status_id
38256
+ WHERE q.cancelled_at IS NOT NULL AND q.status_id IS DISTINCT FROM v_id;
38257
+ IF p_uitvoeren THEN
38258
+ UPDATE public.pick_queue SET status_id = v_id WHERE cancelled_at IS NOT NULL AND status_id IS DISTINCT FROM v_id;
38259
+ GET DIAGNOSTICS v_n = ROW_COUNT;
38260
+ END IF;
38261
+ RETURN jsonb_build_object('uitgevoerd', COALESCE(p_uitvoeren, false), 'aantal', COALESCE(jsonb_array_length(v_list), 0),
38262
+ 'rechtgezet', v_n, 'documenten', COALESCE(v_list, '[]'::jsonb));
38263
+ END $function$;
38264
+ REVOKE ALL ON FUNCTION public.logistics_pick_queue_fix_cancelled_status(boolean) FROM PUBLIC, anon;
38265
+ GRANT EXECUTE ON FUNCTION public.logistics_pick_queue_fix_cancelled_status(boolean) TO authenticated, service_role;
38266
+
38267
+ -- ---------- 5. Karren uit een eerdere buffer ----------
38268
+ CREATE OR REPLACE FUNCTION public.logistics_stale_buffer_carts(p_buffer uuid DEFAULT NULL)
38269
+ RETURNS TABLE(cart_id uuid, karnummer integer, oude_buffer uuid, orders text, nieuwe_buffer text,
38270
+ actieve_order boolean, geprint boolean, gescand boolean, afgerond boolean, besluit text)
38271
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
38272
+ AS $function$
38273
+ WITH k AS (
38274
+ SELECT c.id, c.cart_number, c.buffer_id, c.order_id FROM public.order_carts c
38275
+ WHERE c.archived_at IS NULL AND c.buffer_id IS NOT NULL
38276
+ AND (p_buffer IS NULL OR c.buffer_id = p_buffer)
38277
+ AND NOT EXISTS (SELECT 1 FROM public.orders o WHERE o.buffer_id = c.buffer_id AND o.deleted_at IS NULL)
38278
+ ), ko AS (
38279
+ SELECT k.id AS cart_id, o.order_number, o.buffer_id, o.deleted_at, o.cancelled_at, o.status FROM k
38280
+ JOIN public.order_cart_items ci ON ci.cart_id = k.id
38281
+ JOIN public.order_items oi ON oi.id = ci.order_item_id
38282
+ JOIN public.orders o ON o.id = oi.order_id
38283
+ UNION
38284
+ SELECT k.id, o.order_number, o.buffer_id, o.deleted_at, o.cancelled_at, o.status FROM k JOIN public.orders o ON o.id = k.order_id
38285
+ ), f AS (
38286
+ SELECT k.*,
38287
+ (SELECT string_agg(DISTINCT ko.order_number, ', ') FROM ko WHERE ko.cart_id = k.id) AS orders,
38288
+ (SELECT string_agg(DISTINCT COALESCE(ko.buffer_id::text, 'geen'), ', ') FROM ko WHERE ko.cart_id = k.id) AS nieuwe_buffer,
38289
+ EXISTS (SELECT 1 FROM ko WHERE ko.cart_id = k.id AND ko.deleted_at IS NULL AND ko.cancelled_at IS NULL
38290
+ AND NOT public.logistics_order_is_shipped_status(ko.status)) AS actief,
38291
+ EXISTS (SELECT 1 FROM public.pick_queue q WHERE (q.reference = k.id::text OR q.payload->>'cartId' = k.id::text)
38292
+ AND (q.printed_at IS NOT NULL OR COALESCE(q.print_count, 0) > 0)) AS geprint,
38293
+ EXISTS (SELECT 1 FROM public.logistics_control_scan s WHERE s.cart_id = k.id
38294
+ AND s.approved_at IS NOT NULL AND s.status <> 'geannuleerd') AS gescand,
38295
+ EXISTS (SELECT 1 FROM public.pick_queue q WHERE (q.reference = k.id::text OR q.payload->>'cartId' = k.id::text)
38296
+ AND q.completed_at IS NOT NULL) AS afgerond
38297
+ FROM k
38298
+ )
38299
+ SELECT f.id, f.cart_number, f.buffer_id, f.orders, f.nieuwe_buffer, f.actief, f.geprint, f.gescand, f.afgerond,
38300
+ CASE WHEN f.actief THEN 'actieve order, niet gearchiveerd'
38301
+ WHEN f.gescand OR f.afgerond THEN 'gescand of afgerond, niet gearchiveerd'
38302
+ WHEN f.geprint THEN 'geprint, niet gearchiveerd'
38303
+ ELSE 'archiveren' END
38304
+ FROM f ORDER BY f.buffer_id, f.cart_number;
38305
+ $function$;
38306
+ REVOKE ALL ON FUNCTION public.logistics_stale_buffer_carts(uuid) FROM PUBLIC, anon, authenticated;
38307
+ GRANT EXECUTE ON FUNCTION public.logistics_stale_buffer_carts(uuid) TO service_role;
38308
+
38309
+ CREATE OR REPLACE FUNCTION public.logistics_archive_stale_buffer_carts_core(p_uitvoeren boolean, p_buffer uuid)
38310
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
38311
+ AS $function$
38312
+ DECLARE v_list jsonb; v_ids uuid[]; v_n int := 0;
38313
+ v_reden constant text := 'Kar hoorde bij een eerdere gecombineerde order; de order is opnieuw ingedeeld in een andere buffer.';
38314
+ BEGIN
38315
+ SELECT jsonb_agg(to_jsonb(s)), array_agg(s.cart_id) FILTER (WHERE s.besluit = 'archiveren')
38316
+ INTO v_list, v_ids FROM public.logistics_stale_buffer_carts(p_buffer) s;
38317
+ IF p_uitvoeren AND v_ids IS NOT NULL THEN
38318
+ UPDATE public.pick_queue q
38319
+ SET cancelled_at = now(),
38320
+ payload = COALESCE(q.payload, '{}'::jsonb) || jsonb_build_object('cancelReason', v_reden, 'cancelledBy', 'karplan')
38321
+ WHERE q.cancelled_at IS NULL AND q.completed_at IS NULL
38322
+ AND (q.reference = ANY (SELECT x::text FROM unnest(v_ids) x) OR q.payload->>'cartId' = ANY (SELECT x::text FROM unnest(v_ids) x));
38323
+ INSERT INTO public.logistics_cart_archive_log (cart_id, action, reason, actor, snapshot)
38324
+ SELECT c.id, 'archive', v_reden, NULL, to_jsonb(c) FROM public.order_carts c WHERE c.id = ANY (v_ids) AND c.archived_at IS NULL;
38325
+ UPDATE public.order_carts SET archived_at = now(), archived_reason = v_reden
38326
+ WHERE id = ANY (v_ids) AND archived_at IS NULL;
38327
+ GET DIAGNOSTICS v_n = ROW_COUNT;
38328
+ END IF;
38329
+ RETURN jsonb_build_object('uitgevoerd', COALESCE(p_uitvoeren, false), 'gearchiveerd', v_n, 'karren', COALESCE(v_list, '[]'::jsonb));
38330
+ END $function$;
38331
+ REVOKE ALL ON FUNCTION public.logistics_archive_stale_buffer_carts_core(boolean, uuid) FROM PUBLIC, anon, authenticated;
38332
+ GRANT EXECUTE ON FUNCTION public.logistics_archive_stale_buffer_carts_core(boolean, uuid) TO service_role;
38333
+
38334
+ CREATE OR REPLACE FUNCTION public.logistics_archive_stale_buffer_carts(p_uitvoeren boolean DEFAULT false)
38335
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
38336
+ AS $function$
38337
+ BEGIN
38338
+ IF NOT (public.logistics_acc_is_server() OR public.logistics_is_beheer()) THEN
38339
+ RAISE EXCEPTION 'Geen toegang: alleen beheer of de server mag karren archiveren.' USING ERRCODE = '42501';
38340
+ END IF;
38341
+ RETURN public.logistics_archive_stale_buffer_carts_core(COALESCE(p_uitvoeren, false), NULL);
38342
+ END $function$;
38343
+ REVOKE ALL ON FUNCTION public.logistics_archive_stale_buffer_carts(boolean) FROM PUBLIC, anon;
38344
+ GRANT EXECUTE ON FUNCTION public.logistics_archive_stale_buffer_carts(boolean) TO authenticated, service_role;
38345
+
38346
+ -- Herindelen: na wijziging van buffer of na leeghalen van karren de oude buffer opruimen.
38347
+ CREATE OR REPLACE FUNCTION public.logistics_orders_old_buffer_trg()
38348
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
38349
+ AS $function$
38350
+ BEGIN
38351
+ IF OLD.buffer_id IS NOT NULL AND NEW.buffer_id IS DISTINCT FROM OLD.buffer_id THEN
38352
+ PERFORM public.logistics_archive_stale_buffer_carts_core(true, OLD.buffer_id);
38353
+ END IF;
38354
+ RETURN NULL;
38355
+ END $function$;
38356
+ DROP TRIGGER IF EXISTS trg_logistics_orders_old_buffer ON public.orders;
38357
+ CREATE TRIGGER trg_logistics_orders_old_buffer AFTER UPDATE OF buffer_id ON public.orders
38358
+ FOR EACH ROW EXECUTE FUNCTION public.logistics_orders_old_buffer_trg();
38359
+
38360
+ CREATE OR REPLACE FUNCTION public.logistics_cart_items_old_buffer_trg()
38361
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
38362
+ AS $function$
38363
+ DECLARE b uuid;
38364
+ BEGIN
38365
+ FOR b IN SELECT DISTINCT c.buffer_id FROM old_rows r JOIN public.order_carts c ON c.id = r.cart_id
38366
+ WHERE c.buffer_id IS NOT NULL AND c.archived_at IS NULL
38367
+ AND NOT EXISTS (SELECT 1 FROM public.orders o WHERE o.buffer_id = c.buffer_id AND o.deleted_at IS NULL) LOOP
38368
+ PERFORM public.logistics_archive_stale_buffer_carts_core(true, b);
38369
+ END LOOP;
38370
+ RETURN NULL;
38371
+ END $function$;
38372
+ DROP TRIGGER IF EXISTS trg_logistics_cart_items_old_buffer ON public.order_cart_items;
38373
+ CREATE TRIGGER trg_logistics_cart_items_old_buffer AFTER DELETE ON public.order_cart_items
38374
+ REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION public.logistics_cart_items_old_buffer_trg();
38375
+
38376
+ -- ---------- Install-check ----------
38377
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_221()
38378
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
38379
+ SELECT 'status geannuleerd bestaat', EXISTS (SELECT 1 FROM public.pick_document_statuses WHERE code = 'cancelled'),
38380
+ 'Pickdocumentstatus cancelled (Geannuleerd) is aanwezig'
38381
+ UNION ALL SELECT 'geannuleerd => status', EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_logistics_pick_queue_cancel_status' AND NOT tgisinternal),
38382
+ 'cancelled_at gevuld zet altijd status Geannuleerd'
38383
+ UNION ALL SELECT 'bevroren verdeling gecontroleerd', position('logistics_allocation_stale' IN pg_get_functiondef('public.logistics_order_line_allocation(uuid)'::regprocedure)) > 0
38384
+ AND position('logistics_allocation_stale' IN pg_get_functiondef('public.logistics_recompute_reserved_targeted(uuid)'::regprocedure)) > 0,
38385
+ 'Ongeldige bevroren verdeling (inactieve groep, niet-canonieke sleutel) wordt opnieuw gemaakt'
38386
+ UNION ALL SELECT 'groep inactief herstelt verdeling', EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_logistics_group_inactive_repair'),
38387
+ 'Groep op inactief maakt verdelingen via die groep opnieuw'
38388
+ UNION ALL SELECT 'karren eerdere buffer', EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_logistics_orders_old_buffer')
38389
+ AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_logistics_cart_items_old_buffer'),
38390
+ 'Ongeprinte karren zonder actieve order uit een eerdere buffer worden gearchiveerd'
38391
+ $$;
38392
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_221() TO authenticated, service_role;
38393
+
38394
+ REVOKE ALL ON FUNCTION public.logistics_group_inactive_repair_trg() FROM PUBLIC, anon, authenticated;
38395
+ REVOKE ALL ON FUNCTION public.logistics_pick_queue_cancel_status_trg() FROM PUBLIC, anon, authenticated;
38396
+ REVOKE ALL ON FUNCTION public.logistics_orders_old_buffer_trg() FROM PUBLIC, anon, authenticated;
38397
+ REVOKE ALL ON FUNCTION public.logistics_cart_items_old_buffer_trg() FROM PUBLIC, anon, authenticated;
38398
+ REVOKE ALL ON FUNCTION public.logistics_install_check_221() FROM PUBLIC, anon;
38399
+
38400
+
38401
+ -- ===== 1.0.222 = Pickvoortgang per order (alleen lezen) =====
38402
+ -- 1.0.222 Pickvoortgang per order (alleen lezen).
38403
+ CREATE OR REPLACE FUNCTION public.logistics_order_pick_progress(p_order_ids uuid[])
38404
+ RETURNS TABLE(order_uuid uuid, carts_total integer, carts_printed integer, carts_scanned integer,
38405
+ first_printed_at timestamptz, last_scanned_at timestamptz, pick_date date,
38406
+ blocked boolean, blocked_reason_plain text, carts_archived boolean)
38407
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
38408
+ AS $function$
38409
+ BEGIN
38410
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_pick_progress');
38411
+ IF cardinality(p_order_ids) > 500 THEN
38412
+ RAISE EXCEPTION 'Maximaal 500 orders per aanroep (ontvangen: %).', cardinality(p_order_ids) USING ERRCODE = '22023';
38413
+ END IF;
38414
+ IF p_order_ids IS NULL OR cardinality(p_order_ids) = 0 THEN RETURN; END IF;
38415
+
38416
+ RETURN QUERY
38417
+ WITH ids AS (
38418
+ SELECT DISTINCT o.id, o.order_number FROM public.orders o
38419
+ WHERE o.id = ANY (p_order_ids) AND o.deleted_at IS NULL
38420
+ ),
38421
+ -- Actieve karren: eigen kar of kar met een regel van de order (gedeelde kar telt bij elke order).
38422
+ act AS (
38423
+ SELECT i.id AS oid, c.id AS cid FROM ids i JOIN logistics_active.order_carts c ON c.order_id = i.id
38424
+ UNION
38425
+ SELECT i.id, ci.cart_id FROM ids i
38426
+ JOIN logistics_active.order_items oi ON oi.order_id = i.id
38427
+ JOIN logistics_active.order_cart_items ci ON ci.order_item_id = oi.id
38428
+ JOIN logistics_active.order_carts c2 ON c2.id = ci.cart_id
38429
+ ),
38430
+ -- Geprinte, niet-geannuleerde pickdocumenten van de order, per kar.
38431
+ doc AS (
38432
+ SELECT i.id AS oid, x.cid, q.printed_at
38433
+ FROM ids i
38434
+ JOIN public.pick_queue q
38435
+ ON q.cancelled_at IS NULL AND q.printed_at IS NOT NULL
38436
+ AND (q.payload->>'orderId' = i.id::text
38437
+ OR (jsonb_typeof(q.payload->'orderIds') = 'array' AND q.payload->'orderIds' ? i.id::text))
38438
+ CROSS JOIN LATERAL (
38439
+ SELECT public.logistics_try_uuid(q.payload->>'cartId') AS cid
38440
+ UNION SELECT public.logistics_try_uuid(u) FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(q.printed_cart_ids) = 'array' THEN q.printed_cart_ids ELSE '[]'::jsonb END) u
38441
+ ) x
38442
+ WHERE x.cid IS NOT NULL
38443
+ ),
38444
+ -- Goedgekeurde, niet-geannuleerde scans (zelfde regel als logistics_cart_scanned).
38445
+ scan AS (
38446
+ SELECT i.id AS oid, s.cart_id AS cid, s.approved_at
38447
+ FROM ids i JOIN public.logistics_control_scan s
38448
+ ON s.approved_at IS NOT NULL AND s.status <> 'geannuleerd' AND s.cart_id IS NOT NULL
38449
+ AND (s.order_id = i.id OR s.cart_id IN (SELECT a.cid FROM act a WHERE a.oid = i.id)
38450
+ OR s.cart_id IN (SELECT d.cid FROM doc d WHERE d.oid = i.id))
38451
+ ),
38452
+ mode AS (
38453
+ SELECT i.id AS oid, EXISTS (SELECT 1 FROM act a WHERE a.oid = i.id) AS heeft_actief FROM ids i
38454
+ ),
38455
+ karren AS (
38456
+ SELECT a.oid, a.cid FROM act a
38457
+ UNION
38458
+ SELECT m.oid, d.cid FROM mode m JOIN doc d ON d.oid = m.oid WHERE NOT m.heeft_actief
38459
+ UNION
38460
+ SELECT m.oid, s.cid FROM mode m JOIN scan s ON s.oid = m.oid WHERE NOT m.heeft_actief
38461
+ ),
38462
+ telling AS (
38463
+ SELECT k.oid,
38464
+ count(DISTINCT k.cid)::int AS tot,
38465
+ count(DISTINCT k.cid) FILTER (WHERE EXISTS (SELECT 1 FROM doc d WHERE d.oid = k.oid AND d.cid = k.cid))::int AS pr,
38466
+ count(DISTINCT k.cid) FILTER (WHERE EXISTS (SELECT 1 FROM scan s WHERE s.oid = k.oid AND s.cid = k.cid))::int AS sc,
38467
+ min(d1.printed_at) AS first_pr,
38468
+ max(s1.approved_at) AS last_sc
38469
+ FROM karren k
38470
+ LEFT JOIN doc d1 ON d1.oid = k.oid AND d1.cid = k.cid
38471
+ LEFT JOIN scan s1 ON s1.oid = k.oid AND s1.cid = k.cid
38472
+ GROUP BY k.oid
38473
+ ),
38474
+ blok AS (
38475
+ SELECT DISTINCT ON (q.order_id) q.order_id AS oid, q.reason
38476
+ FROM public.logistics_plan_queue q JOIN ids i ON i.id = q.order_id
38477
+ WHERE q.status = 'geblokkeerd'
38478
+ ORDER BY q.order_id, q.updated_at DESC
38479
+ ),
38480
+ kand AS MATERIALIZED (
38481
+ SELECT a.oid, a.cid FROM act a
38482
+ WHERE NOT EXISTS (SELECT 1 FROM scan s WHERE s.cid = a.cid) -- gescande kar is afgerond
38483
+ AND NOT EXISTS (SELECT 1 FROM blok b WHERE b.oid = a.oid) -- blokkade gaat al voor
38484
+ ),
38485
+ oud AS (
38486
+ SELECT DISTINCT ON (k.oid) k.oid, v.j->>'tekst' AS tekst
38487
+ FROM kand k CROSS JOIN LATERAL (SELECT public.logistics_cart_verouderd_core(k.cid) AS j) v
38488
+ WHERE coalesce((v.j->>'verouderd')::boolean, false)
38489
+ ORDER BY k.oid, k.cid
38490
+ ),
38491
+ taak AS (
38492
+ SELECT DISTINCT ON (i.id) i.id AS oid,
38493
+ ((coalesce(t.planned_start, t.created_at) AT TIME ZONE public.logistics_tz())::date) AS dag
38494
+ FROM ids i JOIN public.logistics_tasks t
38495
+ ON t.task_type = 'pick.execute' AND t.shadow IS NOT TRUE AND t.status NOT IN ('cancelled','superseded')
38496
+ AND (t.payload->>'orderId' = i.id::text OR (nullif(t.payload->>'orderId','') IS NULL AND t.order_ref = i.order_number))
38497
+ ORDER BY i.id, t.created_at DESC
38498
+ )
38499
+ SELECT i.id,
38500
+ coalesce(t.tot, 0), coalesce(t.pr, 0), coalesce(t.sc, 0),
38501
+ t.first_pr, t.last_sc,
38502
+ coalesce(pd.pick_date, tk.dag),
38503
+ (b.oid IS NOT NULL OR o.oid IS NOT NULL),
38504
+ CASE
38505
+ WHEN b.oid IS NOT NULL AND coalesce(t.tot, 0) = 0 THEN 'Geen kar: ' || coalesce(nullif(b.reason, ''), 'order is geblokkeerd')
38506
+ WHEN b.oid IS NOT NULL THEN 'Geblokkeerd: ' || coalesce(nullif(b.reason, ''), 'order is geblokkeerd')
38507
+ WHEN o.oid IS NOT NULL THEN 'Kar verouderd: opnieuw indelen' || coalesce(' — ' || nullif(o.tekst, ''), '')
38508
+ END,
38509
+ (NOT m.heeft_actief AND coalesce(t.tot, 0) > 0)
38510
+ FROM ids i
38511
+ JOIN mode m ON m.oid = i.id
38512
+ LEFT JOIN telling t ON t.oid = i.id
38513
+ LEFT JOIN blok b ON b.oid = i.id
38514
+ LEFT JOIN oud o ON o.oid = i.id
38515
+ LEFT JOIN taak tk ON tk.oid = i.id
38516
+ LEFT JOIN public.logistics_order_pick_day pd ON pd.order_id = i.id;
38517
+ END $function$;
38518
+
38519
+ REVOKE ALL ON FUNCTION public.logistics_order_pick_progress(uuid[]) FROM PUBLIC, anon;
38520
+ GRANT EXECUTE ON FUNCTION public.logistics_order_pick_progress(uuid[]) TO authenticated, service_role;
38521
+
38522
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_222()
38523
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
38524
+ SELECT 'pickvoortgang per order',
38525
+ to_regprocedure('public.logistics_order_pick_progress(uuid[])') IS NOT NULL,
38526
+ 'Leesfunctie logistics_order_pick_progress (max 500 orders) is aanwezig'
38527
+ $$;
38528
+ REVOKE ALL ON FUNCTION public.logistics_install_check_222() FROM PUBLIC, anon;
38529
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_222() TO service_role;
38530
+
38531
+ -- ===== 1.0.223 = Snelle pickvoortgang: set-gebaseerde verouderd-controle (alleen lezen) =====
38532
+ -- bron: migratie 1.0.223
38533
+ CREATE OR REPLACE FUNCTION public.logistics_carts_verouderd_set(p_cart_ids uuid[])
38534
+ RETURNS TABLE(cart_id uuid, redenen jsonb)
38535
+ LANGUAGE sql STABLE SET search_path TO 'public'
38536
+ AS $function$
38537
+ WITH c AS (
38538
+ SELECT oc.id, coalesce(oc.cart_number::text, '?') nr, oc.order_id, oc.buffer_id, oc.carrier_type, oc.total_layers_used
38539
+ FROM public.order_carts oc WHERE oc.id = ANY (p_cart_ids)
38540
+ ),
38541
+ ln AS MATERIALIZED (
38542
+ SELECT ci.id ci_id, ci.cart_id cid, ci.order_item_id, ci.crates, ci.crates_per_layer, ci.product_id,
38543
+ oi.id oi_id, oi.quarantined_at, oi.load_carrier_type, oi.product_id oi_pid
38544
+ FROM public.order_cart_items ci JOIN c ON c.id = ci.cart_id
38545
+ LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
38546
+ ),
38547
+ agg AS (
38548
+ SELECT ln.cid,
38549
+ count(*) FILTER (WHERE ln.oi_id IS NOT NULL AND ln.quarantined_at IS NOT NULL) qn,
38550
+ count(*) FILTER (WHERE coalesce(ln.crates, 0) > 0 AND ln.quarantined_at IS NULL) live,
38551
+ sum(ln.crates::numeric / nullif(ln.crates_per_layer, 0)) lagen_som
38552
+ FROM ln GROUP BY ln.cid
38553
+ ),
38554
+ basis AS (
38555
+ SELECT c.*, coalesce(g.qn, 0) qn, coalesce(g.live, 0) live, g.lagen_som
38556
+ FROM c LEFT JOIN agg g ON g.cid = c.id
38557
+ ),
38558
+ items AS (SELECT DISTINCT ln.order_item_id FROM ln WHERE ln.order_item_id IS NOT NULL),
38559
+ s AS (
38560
+ SELECT oi.id, coalesce(oi.product_name, 'regel') naam, oi.quantity q, sum(ci.crates) fust,
38561
+ max(ci.plants_per_crate) ppc, count(DISTINCT ci.product_id) nprod,
38562
+ bool_or(ci.components IS NOT NULL AND ci.components::text NOT IN ('null','[]','{}')) comp
38563
+ FROM items r JOIN public.order_items oi ON oi.id = r.order_item_id
38564
+ JOIN public.order_cart_items ci ON ci.order_item_id = oi.id
38565
+ JOIN public.order_carts oc ON oc.id = ci.cart_id AND oc.archived_at IS NULL
38566
+ WHERE oi.quarantined_at IS NULL
38567
+ GROUP BY oi.id, oi.product_name, oi.quantity
38568
+ ),
38569
+ afw AS (
38570
+ SELECT x.cid, array_agg(s.naam ORDER BY s.naam) namen
38571
+ FROM (SELECT DISTINCT ln.cid, ln.order_item_id FROM ln WHERE ln.order_item_id IS NOT NULL) x
38572
+ JOIN s ON s.id = x.order_item_id
38573
+ WHERE NOT s.comp AND s.nprod <= 1 AND s.q IS NOT NULL AND s.q > 0
38574
+ AND s.fust <> s.q AND s.fust <> ceil(s.q::numeric / greatest(coalesce(s.ppc, 1), 1))
38575
+ GROUP BY x.cid
38576
+ ),
38577
+ vo AS (
38578
+ SELECT DISTINCT c.id cid, x.id, x.order_number, public.logistics_order_transport_group(to_jsonb(x)) g
38579
+ FROM c JOIN public.orders x
38580
+ ON x.deleted_at IS NULL AND (x.id = c.order_id OR (c.buffer_id IS NOT NULL AND x.buffer_id = c.buffer_id))
38581
+ ),
38582
+ verv AS (
38583
+ SELECT vo.cid, 'Op deze kar staan verschillende soorten vertrek (' ||
38584
+ string_agg(vo.order_number || ': ' || CASE vo.g WHEN 'eigen_vervoer' THEN 'eigen vervoer' WHEN 'klant_haalt_op' THEN 'klant haalt op' ELSE 'vervoerder' END, ', ' ORDER BY vo.order_number)
38585
+ || '); die worden niet samen gebufferd.' tekst
38586
+ FROM vo GROUP BY vo.cid HAVING count(DISTINCT vo.g) > 1
38587
+ ),
38588
+ eo AS (
38589
+ SELECT ln.cid, x.order_number, x.buffer_id FROM ln
38590
+ JOIN public.order_items oi ON oi.id = ln.order_item_id
38591
+ JOIN public.orders x ON x.id = oi.order_id
38592
+ WHERE x.deleted_at IS NULL
38593
+ UNION
38594
+ SELECT c.id, x.order_number, x.buffer_id FROM c JOIN public.orders x ON x.id = c.order_id WHERE x.deleted_at IS NULL
38595
+ ),
38596
+ ea AS (
38597
+ SELECT DISTINCT eo.cid, eo.order_number FROM eo JOIN c ON c.id = eo.cid
38598
+ WHERE eo.buffer_id IS DISTINCT FROM c.buffer_id AND (eo.buffer_id IS NOT NULL OR c.order_id IS NULL)
38599
+ ),
38600
+ eerd AS (
38601
+ SELECT ea.cid, 'Deze kar is ingedeeld voor een eerdere gecombineerde order; '
38602
+ || string_agg(ea.order_number, ', ' ORDER BY ea.order_number)
38603
+ || CASE WHEN count(*) > 1 THEN ' zitten' ELSE ' zit' END || ' nu in een andere gecombineerde order.' tekst
38604
+ FROM ea GROUP BY ea.cid
38605
+ ),
38606
+ -- Ladingdrager: de keuze hangt alleen af van (regelcode, product); elke combinatie één keer laten kiezen.
38607
+ reg AS (
38608
+ SELECT ln.cid, ln.load_carrier_type rc, coalesce(ln.product_id, ln.oi_pid) pid
38609
+ FROM ln JOIN c ON c.id = ln.cid
38610
+ WHERE coalesce(ln.crates, 0) > 0
38611
+ AND NOT (nullif(c.carrier_type, '') IS NULL OR c.carrier_type IN ('loose', 'none', 'NONE'))
38612
+ ),
38613
+ regu AS (
38614
+ SELECT u.rc, u.pid, row_number() OVER (ORDER BY u.rc NULLS FIRST, u.pid NULLS FIRST)::text sleutel
38615
+ FROM (SELECT DISTINCT reg.rc, reg.pid FROM reg) u
38616
+ ),
38617
+ kies AS MATERIALIZED (
38618
+ SELECT kv.key, kv.value
38619
+ FROM (SELECT public.logistics_ladingdrager_kies(jsonb_agg(jsonb_build_object('key', regu.sleutel, 'regel_code', regu.rc, 'product_id', regu.pid))) k
38620
+ FROM regu GROUP BY (regu.sleutel::bigint - 1) / 2000) b
38621
+ CROSS JOIN LATERAL jsonb_each(b.k) kv
38622
+ ),
38623
+ codes AS (
38624
+ SELECT reg.cid, min(kies.value->>'code') eerste,
38625
+ array_agg(DISTINCT kies.value->>'code') lijst
38626
+ FROM reg
38627
+ JOIN regu ON regu.rc IS NOT DISTINCT FROM reg.rc AND regu.pid IS NOT DISTINCT FROM reg.pid
38628
+ JOIN kies ON kies.key = regu.sleutel
38629
+ WHERE kies.value->>'code' IS NOT NULL AND kies.value->>'bron' <> 'werkwijze'
38630
+ GROUP BY reg.cid
38631
+ ),
38632
+ -- Hoogte: zelfde afleiding als logistics_product_maten (fusthoogte + planthoogte), artikelgegevens één keer per product.
38633
+ pids AS (SELECT DISTINCT ln.product_id FROM ln WHERE ln.product_id IS NOT NULL),
38634
+ fm AS MATERIALIZED (SELECT * FROM public.logistics_fust_maten() WHERE EXISTS (SELECT 1 FROM pids)),
38635
+ pr AS MATERIALIZED (
38636
+ SELECT p.id, p.fust_type_id AS hand_fid, p.plant_height_cm AS hand_h, public.logistics_artikel_vbn(p.id) AS a
38637
+ FROM public.products p JOIN pids ON pids.product_id = p.id
38638
+ ),
38639
+ k2 AS (
38640
+ SELECT pr.id, pr.hand_h, pr.a,
38641
+ coalesce(pr.hand_fid,
38642
+ (SELECT m.fust_type_id FROM fm m
38643
+ WHERE (pr.a->>'vbn_package_code') ~ '^[0-9]+$'
38644
+ AND (m.vbn_code = (pr.a->>'vbn_package_code')::int OR m.fust_key = 'vbn:' || (pr.a->>'vbn_package_code'))
38645
+ 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
38646
+ LIMIT 1),
38647
+ (SELECT f.id FROM public.fust_types f
38648
+ WHERE nullif(pr.a->>'custom_package_id', '') IS NOT NULL AND f.custom_package_id = pr.a->>'custom_package_id'
38649
+ ORDER BY f.created_at, f.id LIMIT 1)) AS fid
38650
+ FROM pr
38651
+ ),
38652
+ maten AS MATERIALIZED (
38653
+ SELECT x.product_id, greatest(coalesce(x.fh / 10.0, 0), coalesce(x.ph, 0)) lh FROM (
38654
+ SELECT k.id product_id,
38655
+ coalesce(m.height_mm, (SELECT m2.height_mm 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)) fh,
38656
+ coalesce(public.logistics_pos_num(k.hand_h::text), public.logistics_pos_num(k.a->>'s15_cm'), public.logistics_pos_num(k.a->>'s02_cm')) ph
38657
+ FROM k2 k LEFT JOIN fm m ON m.fust_type_id = k.fid) x
38658
+ WHERE x.fh IS NOT NULL AND x.ph IS NOT NULL
38659
+ ),
38660
+ lhc AS (
38661
+ SELECT x.cid, max(m.lh) lh FROM (SELECT DISTINCT ln.cid, ln.product_id FROM ln WHERE ln.product_id IS NOT NULL) x
38662
+ JOIN maten m ON m.product_id = x.product_id GROUP BY x.cid
38663
+ ),
38664
+ hoog AS (
38665
+ SELECT b.id cid,
38666
+ (SELECT t.max_height_cm FROM public.load_carrier_types t WHERE t.code = b.carrier_type ORDER BY t.is_active DESC LIMIT 1) vmax,
38667
+ l.lh, coalesce(b.total_layers_used, b.lagen_som) lagen
38668
+ FROM basis b LEFT JOIN lhc l ON l.cid = b.id
38669
+ ),
38670
+ regels AS (
38671
+ SELECT c.id cid,
38672
+ CASE
38673
+ WHEN nullif(c.carrier_type, '') IS NULL OR c.carrier_type IN ('loose', 'none', 'NONE') THEN
38674
+ jsonb_build_object('code', 'geen_ladingdrager', 'tekst', 'Kar staat zonder ladingdrager; een product zonder ladingdrager kan niet op een kar.')
38675
+ WHEN cd.cid IS NOT NULL AND NOT (c.carrier_type = ANY (cd.lijst)) THEN
38676
+ jsonb_build_object('code', 'andere_ladingdrager', 'tekst',
38677
+ format('Volgens het artikel hoort deze kar op %s, niet op %s.',
38678
+ coalesce((SELECT coalesce(t.name, t.code) FROM public.load_carrier_types t WHERE t.code = cd.eerste LIMIT 1), cd.eerste),
38679
+ coalesce((SELECT coalesce(t.name, t.code) FROM public.load_carrier_types t WHERE t.code = c.carrier_type LIMIT 1), c.carrier_type)))
38680
+ WHEN h.vmax IS NOT NULL AND h.vmax > 0 AND h.lh IS NOT NULL AND h.lh > 0 AND h.lagen IS NOT NULL
38681
+ AND ceil(h.lagen - 0.001) > floor(h.vmax * (1 + public.logistics_laag_tolerantie()) / h.lh) THEN
38682
+ jsonb_build_object('code', 'te_veel_lagen', 'tekst',
38683
+ format('%s lagen van %s cm passen niet in %s cm; maximaal %s lagen.', trim_scale(round(h.lagen, 1)), trim_scale(round(h.lh, 1)), h.vmax,
38684
+ floor(h.vmax * (1 + public.logistics_laag_tolerantie()) / h.lh)))
38685
+ END r
38686
+ FROM c LEFT JOIN codes cd ON cd.cid = c.id JOIN hoog h ON h.cid = c.id
38687
+ ),
38688
+ alles AS (
38689
+ SELECT b.id,
38690
+ CASE WHEN b.qn > 0 THEN jsonb_build_array(jsonb_build_object('code', 'quarantaine',
38691
+ 'tekst', format('%s regel(s) op kar %s staan in quarantaine bij Verkoop.', b.qn, b.nr))) ELSE '[]'::jsonb END
38692
+ || CASE WHEN b.live = 0 THEN jsonb_build_array(jsonb_build_object('code', 'leeg', 'tekst', format('Kar %s heeft geen regels meer.', b.nr)))
38693
+ ELSE
38694
+ CASE WHEN coalesce(cardinality(a.namen), 0) > 0 THEN jsonb_build_array(jsonb_build_object('code', 'aantal',
38695
+ 'tekst', format('De aantallen op de kar wijken af van de order (%s).', array_to_string(a.namen, ', ')))) ELSE '[]'::jsonb END
38696
+ || CASE WHEN v.cid IS NOT NULL THEN jsonb_build_array(jsonb_build_object('code', 'vervoer_gemengd', 'tekst', v.tekst)) ELSE '[]'::jsonb END
38697
+ || CASE WHEN e.cid IS NOT NULL THEN jsonb_build_array(jsonb_build_object('code', 'eerdere_buffer', 'tekst', e.tekst)) ELSE '[]'::jsonb END
38698
+ || CASE WHEN r.r IS NOT NULL THEN jsonb_build_array(r.r) ELSE '[]'::jsonb END
38699
+ END AS lijst
38700
+ FROM basis b
38701
+ LEFT JOIN afw a ON a.cid = b.id
38702
+ LEFT JOIN verv v ON v.cid = b.id
38703
+ LEFT JOIN eerd e ON e.cid = b.id
38704
+ LEFT JOIN regels r ON r.cid = b.id
38705
+ )
38706
+ SELECT alles.id, alles.lijst FROM alles WHERE jsonb_array_length(alles.lijst) > 0
38707
+ $function$;
38708
+ REVOKE ALL ON FUNCTION public.logistics_carts_verouderd_set(uuid[]) FROM PUBLIC, anon, authenticated;
38709
+ GRANT EXECUTE ON FUNCTION public.logistics_carts_verouderd_set(uuid[]) TO service_role;
38710
+
38711
+ -- 1.0.223 Pickvoortgang: set-gebaseerde verouderd-beoordeling.
38712
+ CREATE OR REPLACE FUNCTION public.logistics_order_pick_progress(p_order_ids uuid[])
38713
+ RETURNS TABLE(order_uuid uuid, carts_total integer, carts_printed integer, carts_scanned integer,
38714
+ first_printed_at timestamptz, last_scanned_at timestamptz, pick_date date,
38715
+ blocked boolean, blocked_reason_plain text, carts_archived boolean)
38716
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
38717
+ AS $function$
38718
+ BEGIN
38719
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_pick_progress');
38720
+ IF cardinality(p_order_ids) > 500 THEN
38721
+ RAISE EXCEPTION 'Maximaal 500 orders per aanroep (ontvangen: %).', cardinality(p_order_ids) USING ERRCODE = '22023';
38722
+ END IF;
38723
+ IF p_order_ids IS NULL OR cardinality(p_order_ids) = 0 THEN RETURN; END IF;
38724
+
38725
+ RETURN QUERY
38726
+ WITH ids AS (
38727
+ SELECT DISTINCT o.id, o.order_number FROM public.orders o
38728
+ WHERE o.id = ANY (p_order_ids) AND o.deleted_at IS NULL
38729
+ ),
38730
+ act AS (
38731
+ SELECT i.id AS oid, c.id AS cid FROM ids i JOIN logistics_active.order_carts c ON c.order_id = i.id
38732
+ UNION
38733
+ SELECT i.id, ci.cart_id FROM ids i
38734
+ JOIN logistics_active.order_items oi ON oi.order_id = i.id
38735
+ JOIN logistics_active.order_cart_items ci ON ci.order_item_id = oi.id
38736
+ JOIN logistics_active.order_carts c2 ON c2.id = ci.cart_id
38737
+ ),
38738
+ doc AS (
38739
+ SELECT i.id AS oid, x.cid, q.printed_at
38740
+ FROM ids i
38741
+ JOIN public.pick_queue q
38742
+ ON q.cancelled_at IS NULL AND q.printed_at IS NOT NULL
38743
+ AND (q.payload->>'orderId' = i.id::text
38744
+ OR (jsonb_typeof(q.payload->'orderIds') = 'array' AND q.payload->'orderIds' ? i.id::text))
38745
+ CROSS JOIN LATERAL (
38746
+ SELECT public.logistics_try_uuid(q.payload->>'cartId') AS cid
38747
+ UNION SELECT public.logistics_try_uuid(u) FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(q.printed_cart_ids) = 'array' THEN q.printed_cart_ids ELSE '[]'::jsonb END) u
38748
+ ) x
38749
+ WHERE x.cid IS NOT NULL
38750
+ ),
38751
+ scan AS (
38752
+ SELECT i.id AS oid, s.cart_id AS cid, s.approved_at
38753
+ FROM ids i JOIN public.logistics_control_scan s
38754
+ ON s.approved_at IS NOT NULL AND s.status <> 'geannuleerd' AND s.cart_id IS NOT NULL
38755
+ AND (s.order_id = i.id OR s.cart_id IN (SELECT a.cid FROM act a WHERE a.oid = i.id)
38756
+ OR s.cart_id IN (SELECT d.cid FROM doc d WHERE d.oid = i.id))
38757
+ ),
38758
+ mode AS (
38759
+ SELECT i.id AS oid, EXISTS (SELECT 1 FROM act a WHERE a.oid = i.id) AS heeft_actief FROM ids i
38760
+ ),
38761
+ karren AS (
38762
+ SELECT a.oid, a.cid FROM act a
38763
+ UNION
38764
+ SELECT m.oid, d.cid FROM mode m JOIN doc d ON d.oid = m.oid WHERE NOT m.heeft_actief
38765
+ UNION
38766
+ SELECT m.oid, s.cid FROM mode m JOIN scan s ON s.oid = m.oid WHERE NOT m.heeft_actief
38767
+ ),
38768
+ telling AS (
38769
+ SELECT k.oid,
38770
+ count(DISTINCT k.cid)::int AS tot,
38771
+ count(DISTINCT k.cid) FILTER (WHERE EXISTS (SELECT 1 FROM doc d WHERE d.oid = k.oid AND d.cid = k.cid))::int AS pr,
38772
+ count(DISTINCT k.cid) FILTER (WHERE EXISTS (SELECT 1 FROM scan s WHERE s.oid = k.oid AND s.cid = k.cid))::int AS sc,
38773
+ min(d1.printed_at) AS first_pr,
38774
+ max(s1.approved_at) AS last_sc
38775
+ FROM karren k
38776
+ LEFT JOIN doc d1 ON d1.oid = k.oid AND d1.cid = k.cid
38777
+ LEFT JOIN scan s1 ON s1.oid = k.oid AND s1.cid = k.cid
38778
+ GROUP BY k.oid
38779
+ ),
38780
+ blok AS (
38781
+ SELECT DISTINCT ON (q.order_id) q.order_id AS oid, q.reason
38782
+ FROM public.logistics_plan_queue q JOIN ids i ON i.id = q.order_id
38783
+ WHERE q.status = 'geblokkeerd'
38784
+ ORDER BY q.order_id, q.updated_at DESC
38785
+ ),
38786
+ kand AS MATERIALIZED (
38787
+ SELECT a.oid, a.cid FROM act a
38788
+ WHERE NOT EXISTS (SELECT 1 FROM scan s WHERE s.cid = a.cid)
38789
+ AND NOT EXISTS (SELECT 1 FROM blok b WHERE b.oid = a.oid)
38790
+ ),
38791
+ oud AS (
38792
+ SELECT DISTINCT ON (k.oid) k.oid, v.redenen->0->>'tekst' AS tekst
38793
+ FROM kand k
38794
+ JOIN public.logistics_carts_verouderd_set((SELECT array_agg(DISTINCT k2.cid) FROM kand k2)) v ON v.cart_id = k.cid
38795
+ ORDER BY k.oid, k.cid
38796
+ ),
38797
+ taak AS (
38798
+ SELECT DISTINCT ON (i.id) i.id AS oid,
38799
+ ((coalesce(t.planned_start, t.created_at) AT TIME ZONE public.logistics_tz())::date) AS dag
38800
+ FROM ids i JOIN public.logistics_tasks t
38801
+ ON t.task_type = 'pick.execute' AND t.shadow IS NOT TRUE AND t.status NOT IN ('cancelled','superseded')
38802
+ AND (t.payload->>'orderId' = i.id::text OR (nullif(t.payload->>'orderId','') IS NULL AND t.order_ref = i.order_number))
38803
+ ORDER BY i.id, t.created_at DESC
38804
+ )
38805
+ SELECT i.id,
38806
+ coalesce(t.tot, 0), coalesce(t.pr, 0), coalesce(t.sc, 0),
38807
+ t.first_pr, t.last_sc,
38808
+ coalesce(pd.pick_date, tk.dag),
38809
+ (b.oid IS NOT NULL OR o.oid IS NOT NULL),
38810
+ CASE
38811
+ WHEN b.oid IS NOT NULL AND coalesce(t.tot, 0) = 0 THEN 'Geen kar: ' || coalesce(nullif(b.reason, ''), 'order is geblokkeerd')
38812
+ WHEN b.oid IS NOT NULL THEN 'Geblokkeerd: ' || coalesce(nullif(b.reason, ''), 'order is geblokkeerd')
38813
+ WHEN o.oid IS NOT NULL THEN 'Kar verouderd: opnieuw indelen' || coalesce(' — ' || nullif(o.tekst, ''), '')
38814
+ END,
38815
+ (NOT m.heeft_actief AND coalesce(t.tot, 0) > 0)
38816
+ FROM ids i
38817
+ JOIN mode m ON m.oid = i.id
38818
+ LEFT JOIN telling t ON t.oid = i.id
38819
+ LEFT JOIN blok b ON b.oid = i.id
38820
+ LEFT JOIN oud o ON o.oid = i.id
38821
+ LEFT JOIN taak tk ON tk.oid = i.id
38822
+ LEFT JOIN public.logistics_order_pick_day pd ON pd.order_id = i.id;
38823
+ END $function$;
38824
+ -- bron: migratie 1.0.223
38825
+ CREATE INDEX IF NOT EXISTS logistics_control_scan_order_idx ON public.logistics_control_scan (order_id) WHERE order_id IS NOT NULL;
38826
+ CREATE INDEX IF NOT EXISTS pick_queue_printed_order_idx ON public.pick_queue ((payload->>'orderId')) WHERE cancelled_at IS NULL AND printed_at IS NOT NULL;
38827
+ CREATE INDEX IF NOT EXISTS pick_queue_printed_orderids_gin ON public.pick_queue USING gin ((payload->'orderIds')) WHERE cancelled_at IS NULL AND printed_at IS NOT NULL;
38828
+ CREATE INDEX IF NOT EXISTS logistics_tasks_pick_order_idx ON public.logistics_tasks ((payload->>'orderId')) WHERE task_type = 'pick.execute';
38829
+ CREATE INDEX IF NOT EXISTS logistics_tasks_pick_ref_idx ON public.logistics_tasks (order_ref) WHERE task_type = 'pick.execute';
38830
+
38831
+ CREATE OR REPLACE FUNCTION public.logistics_order_pick_progress(p_order_ids uuid[])
38832
+ RETURNS TABLE(order_uuid uuid, carts_total integer, carts_printed integer, carts_scanned integer,
38833
+ first_printed_at timestamptz, last_scanned_at timestamptz, pick_date date,
38834
+ blocked boolean, blocked_reason_plain text, carts_archived boolean)
38835
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
38836
+ AS $function$
38837
+ BEGIN
38838
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_pick_progress');
38839
+ IF cardinality(p_order_ids) > 500 THEN
38840
+ RAISE EXCEPTION 'Maximaal 500 orders per aanroep (ontvangen: %).', cardinality(p_order_ids) USING ERRCODE = '22023';
38841
+ END IF;
38842
+ IF p_order_ids IS NULL OR cardinality(p_order_ids) = 0 THEN RETURN; END IF;
38843
+
38844
+ -- 1.0.223: zelfde regels als 1.0.222, set-gebaseerd en via indexen.
38845
+ RETURN QUERY
38846
+ WITH ids AS MATERIALIZED (
38847
+ SELECT DISTINCT o.id, o.order_number FROM public.orders o
38848
+ WHERE o.id = ANY (p_order_ids) AND o.deleted_at IS NULL
38849
+ ),
38850
+ -- Actieve karren; regels volgens logistics_active.order_items (geen quarantaine, niet extern afgehandeld), hier set-gebaseerd.
38851
+ act AS MATERIALIZED (
38852
+ SELECT i.id AS oid, c.id AS cid FROM ids i JOIN public.order_carts c ON c.order_id = i.id AND c.archived_at IS NULL
38853
+ UNION
38854
+ SELECT i.id, ci.cart_id FROM ids i
38855
+ JOIN public.order_items oi ON oi.order_id = i.id AND oi.quarantined_at IS NULL
38856
+ JOIN public.order_cart_items ci ON ci.order_item_id = oi.id
38857
+ JOIN public.order_carts c2 ON c2.id = ci.cart_id AND c2.archived_at IS NULL
38858
+ WHERE NOT EXISTS (SELECT 1 FROM public.logistics_external_fulfillment x
38859
+ WHERE x.order_id = oi.order_id
38860
+ AND (x.full_fulfilled OR lower(btrim(coalesce(oi.source_line_key, ''))) = ANY (x.sales_order_ids)))
38861
+ ),
38862
+ dq AS (
38863
+ SELECT i.id AS oid, q.id AS qid FROM ids i JOIN public.pick_queue q
38864
+ ON q.cancelled_at IS NULL AND q.printed_at IS NOT NULL AND q.payload->>'orderId' = i.id::text
38865
+ UNION
38866
+ SELECT i.id, q.id FROM ids i JOIN public.pick_queue q
38867
+ ON q.cancelled_at IS NULL AND q.printed_at IS NOT NULL
38868
+ AND jsonb_typeof(q.payload->'orderIds') = 'array' AND q.payload->'orderIds' ? i.id::text
38869
+ ),
38870
+ doc AS MATERIALIZED (
38871
+ SELECT dq.oid, x.cid, q.printed_at
38872
+ FROM dq JOIN public.pick_queue q ON q.id = dq.qid
38873
+ CROSS JOIN LATERAL (
38874
+ SELECT public.logistics_try_uuid(q.payload->>'cartId') AS cid
38875
+ UNION SELECT public.logistics_try_uuid(u) FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(q.printed_cart_ids) = 'array' THEN q.printed_cart_ids ELSE '[]'::jsonb END) u
38876
+ ) x
38877
+ WHERE x.cid IS NOT NULL
38878
+ ),
38879
+ docs AS (SELECT doc.oid, doc.cid, min(doc.printed_at) AS pa FROM doc GROUP BY doc.oid, doc.cid),
38880
+ scan AS MATERIALIZED (
38881
+ SELECT i.id AS oid, s.id AS sid, s.cart_id AS cid, s.approved_at FROM ids i JOIN public.logistics_control_scan s
38882
+ ON s.order_id = i.id AND s.approved_at IS NOT NULL AND s.status <> 'geannuleerd' AND s.cart_id IS NOT NULL
38883
+ UNION
38884
+ SELECT a.oid, s.id, s.cart_id, s.approved_at FROM act a JOIN public.logistics_control_scan s
38885
+ ON s.cart_id = a.cid AND s.approved_at IS NOT NULL AND s.status <> 'geannuleerd'
38886
+ UNION
38887
+ SELECT d.oid, s.id, s.cart_id, s.approved_at FROM (SELECT DISTINCT doc.oid, doc.cid FROM doc) d JOIN public.logistics_control_scan s
38888
+ ON s.cart_id = d.cid AND s.approved_at IS NOT NULL AND s.status <> 'geannuleerd'
38889
+ ),
38890
+ scans AS (SELECT scan.oid, scan.cid, max(scan.approved_at) AS sa FROM scan GROUP BY scan.oid, scan.cid),
38891
+ mode AS (
38892
+ SELECT i.id AS oid, (a.oid IS NOT NULL) AS heeft_actief
38893
+ FROM ids i LEFT JOIN (SELECT DISTINCT act.oid FROM act) a ON a.oid = i.id
38894
+ ),
38895
+ karren AS (
38896
+ SELECT a.oid, a.cid FROM act a
38897
+ UNION
38898
+ SELECT m.oid, d.cid FROM mode m JOIN docs d ON d.oid = m.oid WHERE NOT m.heeft_actief
38899
+ UNION
38900
+ SELECT m.oid, s.cid FROM mode m JOIN scans s ON s.oid = m.oid WHERE NOT m.heeft_actief
38901
+ ),
38902
+ telling AS (
38903
+ SELECT k.oid, count(*)::int AS tot, count(d.cid)::int AS pr, count(s.cid)::int AS sc,
38904
+ min(d.pa) AS first_pr, max(s.sa) AS last_sc
38905
+ FROM karren k
38906
+ LEFT JOIN docs d ON d.oid = k.oid AND d.cid = k.cid
38907
+ LEFT JOIN scans s ON s.oid = k.oid AND s.cid = k.cid
38908
+ GROUP BY k.oid
38909
+ ),
38910
+ blok AS (
38911
+ SELECT DISTINCT ON (q.order_id) q.order_id AS oid, q.reason
38912
+ FROM public.logistics_plan_queue q JOIN ids i ON i.id = q.order_id
38913
+ WHERE q.status = 'geblokkeerd'
38914
+ ORDER BY q.order_id, q.updated_at DESC
38915
+ ),
38916
+ kand AS MATERIALIZED (
38917
+ SELECT a.oid, a.cid FROM act a
38918
+ WHERE NOT EXISTS (SELECT 1 FROM scan s WHERE s.cid = a.cid)
38919
+ AND NOT EXISTS (SELECT 1 FROM blok b WHERE b.oid = a.oid)
38920
+ ),
38921
+ oud AS (
38922
+ SELECT DISTINCT ON (k.oid) k.oid, v.redenen->0->>'tekst' AS tekst
38923
+ FROM kand k
38924
+ JOIN public.logistics_carts_verouderd_set((SELECT array_agg(DISTINCT k2.cid) FROM kand k2)) v ON v.cart_id = k.cid
38925
+ ORDER BY k.oid, k.cid
38926
+ ),
38927
+ tk0 AS (
38928
+ SELECT i.id AS oid, t.created_at, t.planned_start FROM ids i JOIN public.logistics_tasks t
38929
+ ON t.task_type = 'pick.execute' AND t.payload->>'orderId' = i.id::text
38930
+ AND t.shadow IS NOT TRUE AND t.status NOT IN ('cancelled','superseded')
38931
+ UNION ALL
38932
+ SELECT i.id, t.created_at, t.planned_start FROM ids i JOIN public.logistics_tasks t
38933
+ ON t.task_type = 'pick.execute' AND t.order_ref = i.order_number AND nullif(t.payload->>'orderId','') IS NULL
38934
+ AND t.shadow IS NOT TRUE AND t.status NOT IN ('cancelled','superseded')
38935
+ ),
38936
+ taak AS (
38937
+ SELECT DISTINCT ON (tk0.oid) tk0.oid,
38938
+ ((coalesce(tk0.planned_start, tk0.created_at) AT TIME ZONE public.logistics_tz())::date) AS dag
38939
+ FROM tk0 ORDER BY tk0.oid, tk0.created_at DESC
38940
+ )
38941
+ SELECT i.id,
38942
+ coalesce(t.tot, 0), coalesce(t.pr, 0), coalesce(t.sc, 0),
38943
+ t.first_pr, t.last_sc,
38944
+ coalesce(pd.pick_date, tk.dag),
38945
+ (b.oid IS NOT NULL OR o.oid IS NOT NULL),
38946
+ CASE
38947
+ WHEN b.oid IS NOT NULL AND coalesce(t.tot, 0) = 0 THEN 'Geen kar: ' || coalesce(nullif(b.reason, ''), 'order is geblokkeerd')
38948
+ WHEN b.oid IS NOT NULL THEN 'Geblokkeerd: ' || coalesce(nullif(b.reason, ''), 'order is geblokkeerd')
38949
+ WHEN o.oid IS NOT NULL THEN 'Kar verouderd: opnieuw indelen' || coalesce(' — ' || nullif(o.tekst, ''), '')
38950
+ END,
38951
+ (NOT m.heeft_actief AND coalesce(t.tot, 0) > 0)
38952
+ FROM ids i
38953
+ JOIN mode m ON m.oid = i.id
38954
+ LEFT JOIN telling t ON t.oid = i.id
38955
+ LEFT JOIN blok b ON b.oid = i.id
38956
+ LEFT JOIN oud o ON o.oid = i.id
38957
+ LEFT JOIN taak tk ON tk.oid = i.id
38958
+ LEFT JOIN public.logistics_order_pick_day pd ON pd.order_id = i.id;
38959
+ END $function$;
38960
+ -- bron: migratie 1.0.223
38961
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_223()
38962
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SET search_path TO 'public' AS $$
38963
+ SELECT 'set-gebaseerde verouderd-controle',
38964
+ to_regprocedure('public.logistics_carts_verouderd_set(uuid[])') IS NOT NULL,
38965
+ 'logistics_carts_verouderd_set is aanwezig (gebruikt door logistics_order_pick_progress)'
38966
+ UNION ALL
38967
+ SELECT 'indexen pickvoortgang',
38968
+ (SELECT count(*) FROM pg_indexes WHERE schemaname = 'public' AND indexname IN
38969
+ ('logistics_control_scan_order_idx','pick_queue_printed_order_idx','pick_queue_printed_orderids_gin',
38970
+ 'logistics_tasks_pick_order_idx','logistics_tasks_pick_ref_idx')) = 5,
38971
+ 'Vijf indexen voor documenten, scans en picktaken per order zijn aanwezig'
38972
+ $$;
38973
+ REVOKE ALL ON FUNCTION public.logistics_install_check_223() FROM PUBLIC, anon, authenticated;
38974
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_223() TO service_role;