@flowselections/logistiek-engine 1.0.200 → 1.0.203

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.
@@ -34601,3 +34601,658 @@ BEGIN
34601
34601
  END IF;
34602
34602
  EXCEPTION WHEN undefined_function THEN NULL;
34603
34603
  END $do$;
34604
+
34605
+ -- ============================================================================
34606
+ -- 1.0.201 — extra platen, geenIndeling, vastzet-bevestiging, stickerstand, startlijst
34607
+ -- ============================================================================
34608
+ -- logistiek-engine 1.0.201 (additief, idempotent)
34609
+
34610
+ CREATE OR REPLACE FUNCTION public.logistics_extra_platen(p_lagen numeric)
34611
+ RETURNS integer LANGUAGE sql IMMUTABLE SET search_path TO 'public' AS $$
34612
+ SELECT CASE WHEN p_lagen IS NULL OR p_lagen < 0 THEN NULL
34613
+ ELSE greatest(0, ceil(p_lagen - 0.001)::int - 1) END
34614
+ $$;
34615
+
34616
+ CREATE OR REPLACE FUNCTION public.logistics_ladingdrager_tekst(p_carrier_code text, p_lagen numeric, p_pct numeric)
34617
+ RETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34618
+ WITH n AS (
34619
+ SELECT coalesce(
34620
+ (SELECT nullif(btrim(t.name), '') FROM public.load_carrier_types t WHERE t.code = p_carrier_code ORDER BY t.is_active DESC LIMIT 1),
34621
+ 'Ladingdrager') AS naam,
34622
+ public.logistics_extra_platen(p_lagen) AS platen)
34623
+ SELECT n.naam
34624
+ || CASE WHEN n.platen IS NULL OR n.platen = 0 THEN '' WHEN n.platen = 1 THEN ' + 1 plaat' ELSE ' + ' || n.platen || ' platen' END
34625
+ || CASE WHEN p_lagen IS NULL THEN ''
34626
+ ELSE ' (' || replace(trim(trailing '.' FROM trim(trailing '0' FROM round(p_lagen, 2)::text)), '.', ',') || ' lagen'
34627
+ || CASE WHEN p_pct IS NULL THEN '' ELSE ', ' || round(p_pct)::int || ' %' END || ')' END
34628
+ FROM n
34629
+ $$;
34630
+
34631
+ CREATE OR REPLACE FUNCTION public.logistics_cart_platen_verrijk(p_cart jsonb)
34632
+ RETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34633
+ SELECT p_cart || jsonb_build_object(
34634
+ 'additionalLayers', public.logistics_extra_platen(nullif(p_cart->>'layersUsed', '')::numeric),
34635
+ 'ladingdragerTekst', public.logistics_ladingdrager_tekst(p_cart->>'carrierType', nullif(p_cart->>'layersUsed', '')::numeric,
34636
+ coalesce(nullif(p_cart->>'layerUtilizationPct', '')::numeric, nullif(p_cart->>'occupancyPct', '')::numeric)))
34637
+ $$;
34638
+
34639
+ -- Kopie van de huidige slip-plan als _v200 (eenmalig), daarna wrapper met additionalLayers.
34640
+ DO $do$
34641
+ BEGIN
34642
+ IF to_regprocedure('public.logistics_cart_slip_plan_v200(uuid)') IS NULL
34643
+ AND to_regprocedure('public.logistics_cart_slip_plan(uuid)') IS NOT NULL THEN
34644
+ EXECUTE replace(pg_get_functiondef('public.logistics_cart_slip_plan(uuid)'::regprocedure),
34645
+ 'FUNCTION public.logistics_cart_slip_plan(', 'FUNCTION public.logistics_cart_slip_plan_v200(');
34646
+ END IF;
34647
+ END $do$;
34648
+
34649
+ DO $do$
34650
+ BEGIN
34651
+ IF to_regprocedure('public.logistics_cart_slip_plan_v200(uuid)') IS NOT NULL THEN
34652
+ EXECUTE $f$
34653
+ CREATE OR REPLACE FUNCTION public.logistics_cart_slip_plan(p_order_id uuid)
34654
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $b$
34655
+ DECLARE b jsonb := public.logistics_cart_slip_plan_v200(p_order_id);
34656
+ BEGIN
34657
+ IF b IS NULL OR jsonb_typeof(b->'carts') IS DISTINCT FROM 'array' THEN RETURN b; END IF;
34658
+ RETURN b || jsonb_build_object('carts', (
34659
+ SELECT coalesce(jsonb_agg(public.logistics_cart_platen_verrijk(t.e) ORDER BY t.o), '[]'::jsonb)
34660
+ FROM jsonb_array_elements(b->'carts') WITH ORDINALITY t(e, o)));
34661
+ END $b$;
34662
+ $f$;
34663
+ END IF;
34664
+ END $do$;
34665
+
34666
+ -- Reden bij geen/deels indeling.
34667
+ CREATE OR REPLACE FUNCTION public.logistics_order_geen_indeling_core(p_order_id uuid)
34668
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34669
+ DECLARE o record; q record; d record; v_code text; v_tekst text; v_soort text; v_actie text;
34670
+ BEGIN
34671
+ SELECT id, order_number, buffer_id INTO o FROM public.orders WHERE id = p_order_id;
34672
+ IF NOT FOUND THEN RETURN NULL; END IF;
34673
+ SELECT status, reason, reason_code INTO q FROM public.logistics_plan_queue
34674
+ WHERE order_id = p_order_id ORDER BY updated_at DESC NULLS LAST LIMIT 1;
34675
+ IF FOUND AND q.status = 'geblokkeerd' THEN
34676
+ v_code := coalesce(q.reason_code, 'geblokkeerd'); v_tekst := q.reason;
34677
+ ELSE
34678
+ SELECT dd.result, dd.explanation, dd.input_snapshot INTO d
34679
+ FROM public.logistics_decisions dd
34680
+ WHERE dd.slot = 'beladingindeling'
34681
+ AND ((dd.subject_type = 'order' AND dd.subject_id = o.id::text)
34682
+ OR (dd.subject_type = 'buffer' AND o.buffer_id IS NOT NULL AND dd.subject_id = o.buffer_id::text)
34683
+ OR (o.order_number IS NOT NULL AND coalesce(dd.input_snapshot->'members', '[]'::jsonb) ? o.order_number))
34684
+ ORDER BY dd.created_at DESC LIMIT 1;
34685
+ IF FOUND AND coalesce((d.result->>'blocked')::boolean, false) THEN
34686
+ v_code := coalesce(d.result->>'reason_code', 'geblokkeerd'); v_tekst := d.explanation;
34687
+ v_soort := d.input_snapshot->>'soort';
34688
+ ELSIF q.status IN ('pending', 'running') THEN
34689
+ v_code := 'wacht_op_indeling'; v_tekst := 'De karindeling wordt gemaakt; dit duurt meestal enkele seconden.';
34690
+ ELSE
34691
+ v_code := 'onbekend';
34692
+ v_tekst := 'Er is nog geen karindeling en er is geen blokkade vastgelegd. De minuutronde probeert het opnieuw; blijft dit staan, meld het aan beheer.';
34693
+ END IF;
34694
+ END IF;
34695
+ IF v_code IN ('belading_onwaarschijnlijk', 'belading_onvolledig', 'ladingdrager_niet_in_beheer') OR v_code LIKE 'belading%' THEN
34696
+ v_actie := 'belading_aanpassen';
34697
+ END IF;
34698
+ RETURN jsonb_build_object('code', v_code, 'soort', v_soort,
34699
+ 'tekst', coalesce(nullif(btrim(v_tekst), ''), 'Geen indeling; de reden is niet bekend.'),
34700
+ 'actie', v_actie,
34701
+ 'actieLabel', CASE WHEN v_actie IS NOT NULL THEN 'Belading aanpassen' END,
34702
+ 'actieUrl', CASE WHEN v_actie IS NOT NULL THEN '/logistiek-engine/beheer#gegevens-voor-karindeling' END);
34703
+ END $$;
34704
+
34705
+ CREATE OR REPLACE FUNCTION public.logistics_order_geen_indeling_many(p_order_ids uuid[])
34706
+ RETURNS TABLE(order_id uuid, geen_indeling jsonb) LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34707
+ BEGIN
34708
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_geen_indeling_many');
34709
+ IF coalesce(cardinality(p_order_ids), 0) > 1000 THEN
34710
+ RAISE EXCEPTION 'Maximaal 1000 orders per aanroep (ontvangen: %).', cardinality(p_order_ids) USING ERRCODE = '22023';
34711
+ END IF;
34712
+ RETURN QUERY
34713
+ SELECT b.order_id,
34714
+ CASE WHEN b.state IN ('geen_indeling', 'deels') AND NOT coalesce(x.full_fulfilled, false)
34715
+ THEN public.logistics_order_geen_indeling_core(b.order_id) END
34716
+ FROM public.logistics_order_plan_coverage_list_v152(p_order_ids) b
34717
+ LEFT JOIN public.logistics_external_fulfillment x ON x.order_id = b.order_id;
34718
+ END $$;
34719
+
34720
+ CREATE OR REPLACE FUNCTION public.logistics_order_plan_coverage(p_order_id uuid)
34721
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34722
+ DECLARE b jsonb := public.logistics_order_plan_coverage_v165(p_order_id); x jsonb := public.logistics_order_external_state(p_order_id);
34723
+ BEGIN
34724
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_plan_coverage');
34725
+ IF jsonb_typeof(b->'carts') = 'array' THEN
34726
+ b := b || jsonb_build_object('carts', (
34727
+ SELECT coalesce(jsonb_agg(
34728
+ CASE WHEN c.id IS NULL THEN t.e || jsonb_build_object('additionalLayers', NULL, 'layersUsed', NULL)
34729
+ ELSE public.logistics_cart_platen_verrijk(t.e || jsonb_build_object(
34730
+ 'layersUsed', c.total_layers_used, 'occupancyPct', c.occupancy_pct, 'maxLayers', c.max_layers,
34731
+ 'layerUtilizationPct', CASE WHEN coalesce(c.max_layers, 0) > 0 AND c.total_layers_used IS NOT NULL
34732
+ THEN round(100.0 * c.total_layers_used / c.max_layers, 1) END)) END
34733
+ ORDER BY t.o), '[]'::jsonb)
34734
+ FROM jsonb_array_elements(b->'carts') WITH ORDINALITY t(e, o)
34735
+ LEFT JOIN logistics_active.order_carts c ON c.id::text = t.e->>'cartId'));
34736
+ END IF;
34737
+ IF x IS NOT NULL AND (x->>'full')::boolean THEN
34738
+ b := b || jsonb_build_object('state', 'afgehandeld_extern', 'label', 'Afgehandeld in Floriday',
34739
+ 'reason', 'De order is in Floriday afgehandeld; de Engine plant en print er niets meer voor.');
34740
+ END IF;
34741
+ b := b || jsonb_build_object('geenIndeling',
34742
+ CASE WHEN b->>'state' IN ('geen_indeling', 'deels') THEN public.logistics_order_geen_indeling_core(p_order_id) END);
34743
+ RETURN b || jsonb_build_object('externalFulfilled', x);
34744
+ END $$;
34745
+
34746
+ -- Lijstvariant met extra kolom (bestaande lijst-RPC heeft een vast tabeltype).
34747
+ CREATE OR REPLACE FUNCTION public.logistics_order_plan_coverage_list_v201(p_order_ids uuid[])
34748
+ RETURNS TABLE(order_id uuid, order_number text, buffer_id uuid, state text, label text, progress_pct integer, reason text,
34749
+ cart_count integer, pick_row_count integer, pick_status text, picked_at timestamptz,
34750
+ buffer_order_count integer, buffer_cart_count integer, geen_indeling jsonb)
34751
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34752
+ BEGIN
34753
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_plan_coverage_list_v201');
34754
+ IF coalesce(cardinality(p_order_ids), 0) > 1000 THEN
34755
+ RAISE EXCEPTION 'Maximaal 1000 orders per aanroep (ontvangen: %).', cardinality(p_order_ids) USING ERRCODE = '22023';
34756
+ END IF;
34757
+ RETURN QUERY
34758
+ SELECT l.*, CASE WHEN l.state IN ('geen_indeling', 'deels') THEN public.logistics_order_geen_indeling_core(l.order_id) END
34759
+ FROM public.logistics_order_plan_coverage_list(p_order_ids) l;
34760
+ END $$;
34761
+
34762
+ -- Vastzetten met vooraf het gevolg.
34763
+ CREATE OR REPLACE FUNCTION public.logistics_belading_vastzet_preview(p_order_item_id uuid, p_wijziging jsonb)
34764
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34765
+ DECLARE oi record; w jsonb := coalesce(p_wijziging, '{}'::jsonb);
34766
+ o_ppc numeric; o_cpl numeric; o_lpc numeric; n_ppc numeric; n_cpl numeric; n_lpc numeric;
34767
+ o_fust numeric; n_fust numeric; o_kar numeric; o_kar_echt int; n_kar numeric; v_nodig boolean := false; v_tekst text;
34768
+ BEGIN
34769
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_belading_vastzet_preview');
34770
+ SELECT * INTO oi FROM public.order_items WHERE id = p_order_item_id;
34771
+ IF NOT FOUND THEN RAISE EXCEPTION 'Orderregel niet gevonden.'; END IF;
34772
+ o_ppc := nullif(oi.plants_per_crate, 0); o_cpl := nullif(oi.crates_per_layer, 0); o_lpc := nullif(oi.layers_per_cart, 0);
34773
+ n_ppc := coalesce(nullif(w->>'plants_per_crate', '')::numeric, o_ppc);
34774
+ n_cpl := coalesce(nullif(w->>'crates_per_layer', '')::numeric, o_cpl);
34775
+ n_lpc := coalesce(nullif(w->>'layers_per_cart', '')::numeric, o_lpc);
34776
+ IF n_ppc IS NOT NULL AND n_ppc <= 0 OR n_cpl IS NOT NULL AND n_cpl <= 0 OR n_lpc IS NOT NULL AND n_lpc <= 0 THEN
34777
+ RAISE EXCEPTION 'Waarden moeten groter dan 0 zijn.';
34778
+ END IF;
34779
+ o_fust := CASE WHEN o_ppc IS NOT NULL THEN ceil(oi.quantity / o_ppc) END;
34780
+ n_fust := CASE WHEN n_ppc IS NOT NULL THEN ceil(oi.quantity / n_ppc) END;
34781
+ SELECT count(DISTINCT ci.cart_id) INTO o_kar_echt FROM logistics_active.order_cart_items ci WHERE ci.order_item_id = p_order_item_id;
34782
+ o_kar := CASE WHEN coalesce(o_kar_echt, 0) > 0 THEN o_kar_echt
34783
+ WHEN o_fust IS NOT NULL AND o_cpl IS NOT NULL AND o_lpc IS NOT NULL THEN ceil(o_fust / (o_cpl * o_lpc)) END;
34784
+ n_kar := CASE WHEN n_fust IS NOT NULL AND n_cpl IS NOT NULL AND n_lpc IS NOT NULL THEN ceil(n_fust / (n_cpl * n_lpc)) END;
34785
+ IF o_fust > 0 AND n_fust > 0 AND greatest(n_fust / o_fust, o_fust / n_fust) > 2 THEN v_nodig := true; END IF;
34786
+ IF o_kar > 0 AND n_kar > 0 AND greatest(n_kar / o_kar, o_kar / n_kar) > 2 THEN v_nodig := true; END IF;
34787
+ v_tekst := 'Wordt ' || coalesce(n_fust::int::text, '?') || ' fust (was ' || coalesce(o_fust::int::text, '?') || ') op '
34788
+ || coalesce(n_kar::int::text, '?') || ' kar(ren) (was ' || coalesce(o_kar::int::text, '?') || ').';
34789
+ IF v_nodig THEN v_tekst := 'Let op: ' || coalesce(n_fust::int::text, '?') || ' fust (was ' || coalesce(o_fust::int::text, '?') || ') op '
34790
+ || coalesce(n_kar::int::text, '?') || ' kar(ren) (was ' || coalesce(o_kar::int::text, '?') || '). Klopt het aantal planten per fust?';
34791
+ END IF;
34792
+ RETURN jsonb_build_object('orderItemId', p_order_item_id, 'planten', oi.quantity,
34793
+ 'oud', jsonb_build_object('plantsPerCrate', o_ppc, 'cratesPerLayer', o_cpl, 'layersPerCart', o_lpc, 'fust', o_fust, 'karren', o_kar, 'karrenBron', CASE WHEN coalesce(o_kar_echt,0) > 0 THEN 'indeling' ELSE 'berekend' END),
34794
+ 'nieuw', jsonb_build_object('plantsPerCrate', n_ppc, 'cratesPerLayer', n_cpl, 'layersPerCart', n_lpc, 'fust', n_fust, 'karren', n_kar),
34795
+ 'bevestigingNodig', v_nodig, 'tekst', v_tekst);
34796
+ END $$;
34797
+
34798
+ CREATE OR REPLACE FUNCTION public.logistics_packing_set_manual_checked(p_order_item_id uuid, p_wijziging jsonb, p_bevestigd boolean)
34799
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
34800
+ DECLARE pv jsonb; w jsonb := coalesce(p_wijziging, '{}'::jsonb);
34801
+ BEGIN
34802
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_packing_set_manual_checked');
34803
+ IF auth.uid() IS NULL THEN RAISE EXCEPTION 'niet ingelogd'; END IF;
34804
+ pv := public.logistics_belading_vastzet_preview(p_order_item_id, w);
34805
+ IF (pv->>'bevestigingNodig')::boolean AND NOT coalesce(p_bevestigd, false) THEN
34806
+ RETURN jsonb_build_object('ok', false, 'bevestigingNodig', true, 'tekst', pv->>'tekst', 'preview', pv);
34807
+ END IF;
34808
+ UPDATE public.order_items SET
34809
+ plants_per_crate = coalesce(nullif(w->>'plants_per_crate', '')::numeric, plants_per_crate),
34810
+ crates_per_layer = coalesce(nullif(w->>'crates_per_layer', '')::numeric, crates_per_layer),
34811
+ layers_per_cart = coalesce(nullif(w->>'layers_per_cart', '')::numeric, layers_per_cart),
34812
+ packing_locked_at = now(), packing_locked_by = auth.uid()
34813
+ WHERE id = p_order_item_id;
34814
+ RETURN jsonb_build_object('ok', true, 'bevestigingNodig', (pv->>'bevestigingNodig')::boolean, 'tekst', pv->>'tekst', 'preview', pv);
34815
+ END $$;
34816
+
34817
+ -- Stickerstand per kar inclusief printopdrachten en stickerGevraagd.
34818
+ CREATE OR REPLACE FUNCTION public.logistics_cart_sticker_state(p_cart_id uuid)
34819
+ RETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $function$
34820
+ WITH s AS (SELECT * FROM public.logistics_sticker_prints WHERE cart_id = p_cart_id),
34821
+ vraag AS (
34822
+ SELECT count(*) FILTER (WHERE coalesce(oi.needs_sticker, false)) AS met_sticker, count(*) AS regels,
34823
+ array_agg(DISTINCT oi.order_id) FILTER (WHERE oi.order_id IS NOT NULL) AS order_ids
34824
+ FROM public.order_cart_items ci
34825
+ LEFT JOIN public.order_items oi ON oi.id::text = split_part(ci.order_item_id::text, '::', 1)
34826
+ WHERE ci.cart_id = p_cart_id
34827
+ ),
34828
+ klant AS (
34829
+ SELECT EXISTS (
34830
+ SELECT 1 FROM public.orders o JOIN public.customers c ON c.id = o.customer_id, vraag v
34831
+ WHERE o.id = ANY(coalesce(v.order_ids, '{}'::uuid[]))
34832
+ AND (EXISTS (SELECT 1 FROM public.customer_product_stickers ps
34833
+ WHERE ps.customer_key IN (c.id::text, 'tf:' || coalesce(c.twinfield_code, '-'), 'cn:' || coalesce(c.customer_number, '-')))
34834
+ OR EXISTS (SELECT 1 FROM public.customer_sticker_settings ss
34835
+ WHERE ss.customer_key IN (c.id::text, 'tf:' || coalesce(c.twinfield_code, '-'), 'cn:' || coalesce(c.customer_number, '-'))))
34836
+ ) AS ingesteld
34837
+ ),
34838
+ pj AS (
34839
+ SELECT coalesce(jsonb_agg(jsonb_build_object('printJobId', j.id, 'documenttype', j.documenttype::text, 'documentnaam', j.documentnaam,
34840
+ 'status', j.status::text, 'kopieen', j.kopieen, 'isHerprint', coalesce(j.is_herprint, false),
34841
+ 'aangemaaktOp', j.created_at, 'voltooidOp', j.voltooid_op, 'foutmelding', j.foutmelding) ORDER BY j.created_at), '[]'::jsonb) AS lijst,
34842
+ count(*) AS n,
34843
+ count(*) FILTER (WHERE j.status::text = 'voltooid') AS n_klaar,
34844
+ count(*) FILTER (WHERE j.status::text IN ('mislukt')) AS n_fout
34845
+ FROM public.print_jobs j
34846
+ WHERE j.payload->>'cart_id' = p_cart_id::text
34847
+ AND j.documenttype::text ILIKE '%sticker%'
34848
+ ),
34849
+ t AS (
34850
+ SELECT
34851
+ count(*) FILTER (WHERE status <> 'niet_printen') AS totaal,
34852
+ count(*) FILTER (WHERE status IN ('in_printwachtrij','afgemeld')) AS in_wachtrij,
34853
+ count(*) FILTER (WHERE status IN ('wacht_op_moment','klaar')) AS wachtend,
34854
+ count(*) FILTER (WHERE status IN ('geblokkeerd','mislukt')) AS geblokkeerd,
34855
+ count(*) FILTER (WHERE status = 'niet_printen') AS niet_printen,
34856
+ (array_agg(reden ORDER BY soort, bron_id) FILTER (WHERE status = 'niet_printen' AND coalesce(btrim(reden), '') <> ''))[1] AS niet_reden,
34857
+ coalesce(jsonb_agg(jsonb_build_object('soort', soort, 'stickerId', bron_id, 'kopieen', kopieen,
34858
+ 'status', status, 'reden', reden, 'printJobId', print_job_id, 'breedteMm', breedte_mm, 'hoogteMm', hoogte_mm)
34859
+ ORDER BY soort, bron_id), '[]'::jsonb) AS regels
34860
+ FROM s
34861
+ )
34862
+ SELECT jsonb_build_object(
34863
+ 'cartId', p_cart_id,
34864
+ 'totaal', t.totaal,
34865
+ 'inPrintwachtrij', t.in_wachtrij,
34866
+ 'wachtend', t.wachtend,
34867
+ 'geblokkeerd', t.geblokkeerd,
34868
+ 'regels', t.regels,
34869
+ 'stickersNodig', t.totaal > 0,
34870
+ 'geenStickersReden', CASE
34871
+ WHEN t.totaal > 0 THEN NULL
34872
+ WHEN t.niet_printen > 0 THEN coalesce(t.niet_reden, 'De stickers van deze kar hoeven niet geprint te worden.')
34873
+ WHEN v.regels = 0 THEN 'Deze kar heeft geen regels.'
34874
+ WHEN v.met_sticker = 0 THEN 'Geen regel op deze kar vraagt om een sticker.'
34875
+ ELSE 'Er vraagt een regel om een sticker, maar die is nog niet klaargezet. De stickerronde pakt dit automatisch op.'
34876
+ END,
34877
+ 'stickerGevraagd', (v.met_sticker > 0 OR k.ingesteld),
34878
+ 'stickerGevraagdBron', CASE WHEN v.met_sticker > 0 THEN 'orderregel' WHEN k.ingesteld THEN 'klant' END,
34879
+ 'printopdrachten', pj.lijst,
34880
+ 'printopdrachtenTotaal', pj.n,
34881
+ 'printopdrachtenVoltooid', pj.n_klaar,
34882
+ 'printopdrachtenMislukt', pj.n_fout)
34883
+ FROM t, vraag v, klant k, pj;
34884
+ $function$;
34885
+
34886
+ -- Startlijst: alleen gewicht ontbreekt = compleet met waarschuwing.
34887
+ CREATE OR REPLACE FUNCTION public.logistics_startlijst()
34888
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34889
+ DECLARE v_drag int; v_drag_mist int; v_werk boolean; v_prod int; v_prod_mist int; v_prod_warn int;
34890
+ BEGIN
34891
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_startlijst');
34892
+ 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)
34893
+ INTO v_drag, v_drag_mist
34894
+ FROM public.load_carrier_types t
34895
+ 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');
34896
+ SELECT EXISTS (SELECT 1 FROM public.logistics_rule_configs s WHERE s.is_active AND s.settings ? 'karrenindeling') INTO v_werk;
34897
+ SELECT count(*),
34898
+ count(*) FILTER (WHERE cardinality(array_remove(g.ontbrekend, 'weight')) > 0),
34899
+ count(*) FILTER (WHERE g.ontbrekend = ARRAY['weight']::text[])
34900
+ INTO v_prod, v_prod_mist, v_prod_warn FROM public.logistics_data_gereedheid(NULL) g;
34901
+ RETURN jsonb_build_object(
34902
+ 'ladingdragers', jsonb_build_object('klaar', v_drag > 0 AND v_drag_mist = 0, 'aantal', v_drag, 'onvolledig', v_drag_mist),
34903
+ 'werkwijze', jsonb_build_object('klaar', v_werk),
34904
+ 'gereedheid', jsonb_build_object('klaar', v_prod_mist = 0, 'producten', v_prod, 'onvolledig', v_prod_mist, 'waarschuwing', v_prod_warn));
34905
+ END $$;
34906
+
34907
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_201()
34908
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34909
+ SELECT '1 extra platen', public.logistics_extra_platen(7.45) = 7 AND public.logistics_extra_platen(2.0) = 1 AND public.logistics_extra_platen(0.125) = 0,
34910
+ 'logistics_extra_platen rekent extra platen boven de bodem'
34911
+ UNION ALL SELECT '1 slip-plan wrapper', to_regprocedure('public.logistics_cart_slip_plan_v200(uuid)') IS NOT NULL, 'logistics_cart_slip_plan geeft additionalLayers per kar'
34912
+ UNION ALL SELECT '2 reden geen indeling', to_regprocedure('public.logistics_order_geen_indeling_core(uuid)') IS NOT NULL
34913
+ AND to_regprocedure('public.logistics_order_plan_coverage_list_v201(uuid[])') IS NOT NULL, 'geenIndeling in coverage en lijst'
34914
+ UNION ALL SELECT '3 vastzet-bevestiging', to_regprocedure('public.logistics_packing_set_manual_checked(uuid,jsonb,boolean)') IS NOT NULL, 'bevestiging bij >2x fust of karren'
34915
+ UNION ALL SELECT '5 stickerstand', position('printopdrachten' IN pg_get_functiondef('public.logistics_cart_sticker_state(uuid)'::regprocedure)) > 0, 'printopdrachten en stickerGevraagd per kar'
34916
+ $$;
34917
+
34918
+ DO $g$ BEGIN
34919
+ GRANT EXECUTE ON FUNCTION public.logistics_extra_platen(numeric) TO authenticated, service_role;
34920
+ GRANT EXECUTE ON FUNCTION public.logistics_order_geen_indeling_many(uuid[]) TO authenticated, service_role;
34921
+ GRANT EXECUTE ON FUNCTION public.logistics_order_plan_coverage_list_v201(uuid[]) TO authenticated, service_role;
34922
+ GRANT EXECUTE ON FUNCTION public.logistics_belading_vastzet_preview(uuid, jsonb) TO authenticated, service_role;
34923
+ GRANT EXECUTE ON FUNCTION public.logistics_packing_set_manual_checked(uuid, jsonb, boolean) TO authenticated, service_role;
34924
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_201() TO authenticated, service_role;
34925
+ END $g$;
34926
+
34927
+ CREATE OR REPLACE FUNCTION public.logistics_cart_platen_verrijk(p_cart jsonb)
34928
+ RETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34929
+ SELECT p_cart || jsonb_build_object(
34930
+ 'additionalLayers', public.logistics_extra_platen(nullif(p_cart->>'layersUsed', '')::numeric),
34931
+ 'ladingdragerTekst', public.logistics_ladingdrager_tekst(p_cart->>'carrierType', nullif(p_cart->>'layersUsed', '')::numeric,
34932
+ coalesce(nullif(p_cart->>'occupancyPct', '')::numeric, nullif(p_cart->>'layerUtilizationPct', '')::numeric)))
34933
+ $$;
34934
+
34935
+ DO $d$ BEGIN
34936
+ IF position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_order_geen_indeling_core(uuid)'::regprocedure)) = 0 THEN
34937
+ EXECUTE replace(pg_get_functiondef('public.logistics_order_geen_indeling_core(uuid)'::regprocedure),
34938
+ 'SELECT id, order_number, buffer_id INTO o FROM public.orders WHERE id = p_order_id;',
34939
+ 'PERFORM public.logistics_acc_guard(''ingelogd'', ''logistics_order_geen_indeling_core'');
34940
+ SELECT id, order_number, buffer_id INTO o FROM public.orders WHERE id = p_order_id;');
34941
+ END IF;
34942
+ END $d$;
34943
+
34944
+ -- 1.0.201 vervolg — kar_te_hoog, guards pakbonfuncties, productoordeel, Beheerlink (additief, idempotent)
34945
+ -- 1.0.201 vervolg: te hoge karren, guards op pakbonfuncties, productoordeel, Beheerlink altijd gevuld.
34946
+
34947
+ -- Rekenhulp zonder gegevens: blokkade voor één kar.
34948
+ CREATE OR REPLACE FUNCTION public.logistics_kar_blokkade(p_kar text, p_lagen numeric, p_max numeric)
34949
+ RETURNS jsonb LANGUAGE sql IMMUTABLE SET search_path TO 'public' AS $$
34950
+ SELECT CASE WHEN p_lagen IS NOT NULL AND coalesce(p_max, 0) > 0 AND p_lagen > p_max + 0.001
34951
+ THEN jsonb_build_object('teHoog', true, 'blokkade', jsonb_build_object('code', 'kar_te_hoog',
34952
+ 'tekst', format('Kar %s heeft %s lagen, maximaal %s. Deel de order opnieuw in of pas de belading aan.',
34953
+ coalesce(nullif(btrim(p_kar), ''), '?'),
34954
+ replace(trim_scale(round(p_lagen, 2))::text, '.', ','),
34955
+ replace(trim_scale(round(p_max, 2))::text, '.', ','))))
34956
+ ELSE jsonb_build_object('teHoog', false, 'blokkade', NULL) END
34957
+ $$;
34958
+
34959
+ -- Karren aanvullen (platen + blokkade). Geeft alleen de meegegeven kar terug, maar leest ladingdragernamen: guard.
34960
+ CREATE OR REPLACE FUNCTION public.logistics_cart_platen_verrijk(p_cart jsonb)
34961
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34962
+ DECLARE v_l numeric := nullif(p_cart->>'layersUsed', '')::numeric;
34963
+ BEGIN
34964
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_cart_platen_verrijk');
34965
+ RETURN p_cart || jsonb_build_object(
34966
+ 'additionalLayers', public.logistics_extra_platen(v_l),
34967
+ 'ladingdragerTekst', public.logistics_ladingdrager_tekst(p_cart->>'carrierType', v_l,
34968
+ coalesce(nullif(p_cart->>'layerUtilizationPct', '')::numeric, nullif(p_cart->>'occupancyPct', '')::numeric)))
34969
+ || public.logistics_kar_blokkade(coalesce(p_cart->>'cartNumber', p_cart->>'cart_number', p_cart->>'seq'), v_l,
34970
+ nullif(p_cart->>'maxLayers', '')::numeric);
34971
+ END $$;
34972
+
34973
+ -- Guard op de kopie van de pakbonfunctie.
34974
+ DO $d$ BEGIN
34975
+ IF to_regprocedure('public.logistics_cart_slip_plan_v200(uuid)') IS NOT NULL
34976
+ AND position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_cart_slip_plan_v200(uuid)'::regprocedure)) = 0 THEN
34977
+ EXECUTE regexp_replace(pg_get_functiondef('public.logistics_cart_slip_plan_v200(uuid)'::regprocedure),
34978
+ '\nBEGIN\n', E'\nBEGIN\n PERFORM public.logistics_acc_guard(''ingelogd'', ''logistics_cart_slip_plan_v200'');\n');
34979
+ END IF;
34980
+ END $d$;
34981
+
34982
+ -- Pakbonfunctie: guard + per kar teHoog/blokkade.
34983
+ DO $do$ BEGIN
34984
+ IF to_regprocedure('public.logistics_cart_slip_plan_v200(uuid)') IS NOT NULL THEN
34985
+ EXECUTE $f$
34986
+ CREATE OR REPLACE FUNCTION public.logistics_cart_slip_plan(p_order_id uuid)
34987
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $b$
34988
+ DECLARE b jsonb;
34989
+ BEGIN
34990
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_cart_slip_plan');
34991
+ b := public.logistics_cart_slip_plan_v200(p_order_id);
34992
+ IF b IS NULL OR jsonb_typeof(b->'carts') IS DISTINCT FROM 'array' THEN RETURN b; END IF;
34993
+ RETURN b || jsonb_build_object('carts', (
34994
+ SELECT coalesce(jsonb_agg(public.logistics_cart_platen_verrijk(t.e) ORDER BY t.o), '[]'::jsonb)
34995
+ FROM jsonb_array_elements(b->'carts') WITH ORDINALITY t(e, o)));
34996
+ END $b$;
34997
+ $f$;
34998
+ END IF;
34999
+ END $do$;
35000
+
35001
+ -- Te hoge karren per order (via de buffer, dus ook gedeelde karren).
35002
+ CREATE OR REPLACE FUNCTION public.logistics_order_te_hoge_karren(p_order_ids uuid[])
35003
+ RETURNS TABLE(order_id uuid, cart_id uuid, cart_number integer, lagen numeric, max_lagen numeric, blokkade jsonb)
35004
+ LANGUAGE plpgsql STABLE SET search_path TO 'public' AS $$
35005
+ BEGIN
35006
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_te_hoge_karren');
35007
+ RETURN QUERY
35008
+ SELECT u.order_id, u.cart_id, coalesce(u.display_seq, u.cart_number), c.total_layers_used::numeric, c.max_layers::numeric,
35009
+ public.logistics_kar_blokkade(coalesce(u.display_seq, u.cart_number)::text, c.total_layers_used, c.max_layers)->'blokkade'
35010
+ FROM public.logistics_order_unit_carts_core(p_order_ids) u
35011
+ JOIN logistics_active.order_carts c ON c.id = u.cart_id
35012
+ WHERE (public.logistics_kar_blokkade(NULL, c.total_layers_used, c.max_layers)->>'teHoog')::boolean;
35013
+ END $$;
35014
+
35015
+ -- Reden bij geen indeling: kar_te_hoog eerst, actieUrl altijd gevuld.
35016
+ CREATE OR REPLACE FUNCTION public.logistics_order_geen_indeling_core(p_order_id uuid)
35017
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35018
+ DECLARE o record; q record; d record; t record; v_code text; v_tekst text; v_soort text; v_actie text;
35019
+ c_beheer constant text := '/logistiek-engine/beheer';
35020
+ BEGIN
35021
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_geen_indeling_core');
35022
+ SELECT id, order_number, buffer_id INTO o FROM public.orders WHERE id = p_order_id;
35023
+ IF NOT FOUND THEN RETURN NULL; END IF;
35024
+ SELECT * INTO t FROM public.logistics_order_te_hoge_karren(ARRAY[p_order_id]) ORDER BY cart_number LIMIT 1;
35025
+ IF FOUND THEN
35026
+ v_code := 'kar_te_hoog'; v_tekst := t.blokkade->>'tekst'; v_actie := 'belading_aanpassen';
35027
+ ELSE
35028
+ SELECT status, reason, reason_code INTO q FROM public.logistics_plan_queue
35029
+ WHERE order_id = p_order_id ORDER BY updated_at DESC NULLS LAST LIMIT 1;
35030
+ IF FOUND AND q.status = 'geblokkeerd' THEN
35031
+ v_code := coalesce(q.reason_code, 'geblokkeerd'); v_tekst := q.reason;
35032
+ ELSE
35033
+ SELECT dd.result, dd.explanation, dd.input_snapshot INTO d
35034
+ FROM public.logistics_decisions dd
35035
+ WHERE dd.slot = 'beladingindeling'
35036
+ AND ((dd.subject_type = 'order' AND dd.subject_id = o.id::text)
35037
+ OR (dd.subject_type = 'buffer' AND o.buffer_id IS NOT NULL AND dd.subject_id = o.buffer_id::text)
35038
+ OR (o.order_number IS NOT NULL AND coalesce(dd.input_snapshot->'members', '[]'::jsonb) ? o.order_number))
35039
+ ORDER BY dd.created_at DESC LIMIT 1;
35040
+ IF FOUND AND coalesce((d.result->>'blocked')::boolean, false) THEN
35041
+ v_code := coalesce(d.result->>'reason_code', 'geblokkeerd'); v_tekst := d.explanation;
35042
+ v_soort := d.input_snapshot->>'soort';
35043
+ ELSIF q.status IN ('pending', 'running') THEN
35044
+ v_code := 'wacht_op_indeling'; v_tekst := 'De karindeling wordt gemaakt; dit duurt meestal enkele seconden.';
35045
+ ELSE
35046
+ v_code := 'onbekend';
35047
+ v_tekst := 'Er is nog geen karindeling en er is geen blokkade vastgelegd. De minuutronde probeert het opnieuw; blijft dit staan, meld het aan beheer.';
35048
+ END IF;
35049
+ END IF;
35050
+ IF v_code LIKE 'belading%' OR v_code = 'ladingdrager_niet_in_beheer' THEN v_actie := 'belading_aanpassen'; END IF;
35051
+ END IF;
35052
+ RETURN jsonb_build_object('code', v_code, 'soort', v_soort,
35053
+ 'tekst', coalesce(nullif(btrim(v_tekst), ''), 'Geen indeling; de reden is niet bekend.'),
35054
+ 'actie', coalesce(v_actie, 'beheer'),
35055
+ 'actieLabel', CASE WHEN v_actie IS NOT NULL THEN 'Belading aanpassen' ELSE 'Naar Engine-beheer' END,
35056
+ 'actieUrl', CASE WHEN v_actie IS NOT NULL THEN c_beheer || '#gegevens-voor-karindeling' ELSE c_beheer END);
35057
+ END $$;
35058
+
35059
+ CREATE OR REPLACE FUNCTION public.logistics_order_geen_indeling_many(p_order_ids uuid[])
35060
+ RETURNS TABLE(order_id uuid, geen_indeling jsonb) LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35061
+ BEGIN
35062
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_geen_indeling_many');
35063
+ IF coalesce(cardinality(p_order_ids), 0) > 1000 THEN
35064
+ RAISE EXCEPTION 'Maximaal 1000 orders per aanroep (ontvangen: %).', cardinality(p_order_ids) USING ERRCODE = '22023';
35065
+ END IF;
35066
+ RETURN QUERY
35067
+ WITH th AS (SELECT DISTINCT h.order_id FROM public.logistics_order_te_hoge_karren(p_order_ids) h)
35068
+ SELECT b.order_id,
35069
+ CASE WHEN (b.state IN ('geen_indeling', 'deels') AND NOT coalesce(x.full_fulfilled, false)) OR th.order_id IS NOT NULL
35070
+ THEN public.logistics_order_geen_indeling_core(b.order_id) END
35071
+ FROM public.logistics_order_plan_coverage_list_v152(p_order_ids) b
35072
+ LEFT JOIN public.logistics_external_fulfillment x ON x.order_id = b.order_id
35073
+ LEFT JOIN th ON th.order_id = b.order_id;
35074
+ END $$;
35075
+
35076
+ CREATE OR REPLACE FUNCTION public.logistics_order_plan_coverage_list_v201(p_order_ids uuid[])
35077
+ RETURNS TABLE(order_id uuid, order_number text, buffer_id uuid, state text, label text, progress_pct integer, reason text,
35078
+ cart_count integer, pick_row_count integer, pick_status text, picked_at timestamptz,
35079
+ buffer_order_count integer, buffer_cart_count integer, geen_indeling jsonb)
35080
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35081
+ BEGIN
35082
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_plan_coverage_list_v201');
35083
+ IF coalesce(cardinality(p_order_ids), 0) > 1000 THEN
35084
+ RAISE EXCEPTION 'Maximaal 1000 orders per aanroep (ontvangen: %).', cardinality(p_order_ids) USING ERRCODE = '22023';
35085
+ END IF;
35086
+ RETURN QUERY
35087
+ WITH th AS (SELECT DISTINCT h.order_id FROM public.logistics_order_te_hoge_karren(p_order_ids) h)
35088
+ SELECT l.order_id, l.order_number, l.buffer_id, l.state,
35089
+ CASE WHEN th.order_id IS NOT NULL THEN 'Ingedeeld — kar te hoog' ELSE l.label END,
35090
+ l.progress_pct, l.reason, l.cart_count, l.pick_row_count, l.pick_status, l.picked_at,
35091
+ l.buffer_order_count, l.buffer_cart_count,
35092
+ CASE WHEN l.state IN ('geen_indeling', 'deels') OR th.order_id IS NOT NULL THEN public.logistics_order_geen_indeling_core(l.order_id) END
35093
+ FROM public.logistics_order_plan_coverage_list(p_order_ids) l
35094
+ LEFT JOIN th ON th.order_id = l.order_id;
35095
+ END $$;
35096
+
35097
+ CREATE OR REPLACE FUNCTION public.logistics_order_plan_coverage(p_order_id uuid)
35098
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35099
+ DECLARE b jsonb; x jsonb; v_hoog boolean;
35100
+ BEGIN
35101
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_plan_coverage');
35102
+ b := public.logistics_order_plan_coverage_v165(p_order_id);
35103
+ x := public.logistics_order_external_state(p_order_id);
35104
+ IF jsonb_typeof(b->'carts') = 'array' THEN
35105
+ b := b || jsonb_build_object('carts', (
35106
+ SELECT coalesce(jsonb_agg(
35107
+ CASE WHEN c.id IS NULL THEN t.e || jsonb_build_object('additionalLayers', NULL, 'layersUsed', NULL, 'teHoog', false, 'blokkade', NULL)
35108
+ ELSE public.logistics_cart_platen_verrijk(t.e || jsonb_build_object(
35109
+ 'layersUsed', c.total_layers_used, 'occupancyPct', c.occupancy_pct, 'maxLayers', c.max_layers,
35110
+ 'layerUtilizationPct', CASE WHEN coalesce(c.max_layers, 0) > 0 AND c.total_layers_used IS NOT NULL
35111
+ THEN round(100.0 * c.total_layers_used / c.max_layers, 1) END)) END
35112
+ ORDER BY t.o), '[]'::jsonb)
35113
+ FROM jsonb_array_elements(b->'carts') WITH ORDINALITY t(e, o)
35114
+ LEFT JOIN logistics_active.order_carts c ON c.id::text = t.e->>'cartId'));
35115
+ END IF;
35116
+ IF x IS NOT NULL AND (x->>'full')::boolean THEN
35117
+ b := b || jsonb_build_object('state', 'afgehandeld_extern', 'label', 'Afgehandeld in Floriday',
35118
+ 'reason', 'De order is in Floriday afgehandeld; de Engine plant en print er niets meer voor.');
35119
+ END IF;
35120
+ v_hoog := EXISTS (SELECT 1 FROM jsonb_array_elements(coalesce(b->'carts', '[]'::jsonb)) e WHERE (e->>'teHoog')::boolean)
35121
+ OR EXISTS (SELECT 1 FROM public.logistics_order_te_hoge_karren(ARRAY[p_order_id]));
35122
+ IF v_hoog AND b->>'state' IS DISTINCT FROM 'afgehandeld_extern' THEN
35123
+ b := b || jsonb_build_object('label', 'Ingedeeld — kar te hoog');
35124
+ END IF;
35125
+ b := b || jsonb_build_object('geenIndeling',
35126
+ CASE WHEN b->>'state' IN ('geen_indeling', 'deels') OR (v_hoog AND b->>'state' IS DISTINCT FROM 'afgehandeld_extern')
35127
+ THEN public.logistics_order_geen_indeling_core(p_order_id) END);
35128
+ RETURN b || jsonb_build_object('externalFulfilled', x);
35129
+ END $$;
35130
+
35131
+ -- Kar-check voor de server (sticker- en verzendbriefronde).
35132
+ CREATE OR REPLACE FUNCTION public.logistics_cart_te_hoog(p_cart_id uuid)
35133
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35134
+ DECLARE c record;
35135
+ BEGIN
35136
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_cart_te_hoog');
35137
+ SELECT oc.cart_number, oc.total_layers_used, oc.max_layers INTO c FROM logistics_active.order_carts oc WHERE oc.id = p_cart_id;
35138
+ IF NOT FOUND THEN RETURN jsonb_build_object('teHoog', false, 'blokkade', NULL); END IF;
35139
+ RETURN public.logistics_kar_blokkade(c.cart_number::text, c.total_layers_used, c.max_layers);
35140
+ END $$;
35141
+
35142
+ -- LayerSpace (zelfde algoritme als dimensionPlanner.ts): maximaal aantal fusten per laag, beide startrichtingen.
35143
+ CREATE OR REPLACE FUNCTION public.logistics_max_fusten_per_laag(p_ll numeric, p_lw numeric, p_fl numeric, p_fw numeric)
35144
+ RETURNS integer LANGUAGE plpgsql IMMUTABLE SET search_path TO 'public' AS $$
35145
+ DECLARE best int := 0; rnd int; a0 numeric; b0 numeric; n int; placed boolean;
35146
+ dep numeric[]; used numeric[]; total numeric; i int; k int; oa numeric; ob numeric; e numeric := 1e-9;
35147
+ BEGIN
35148
+ IF coalesce(p_ll,0) <= 0 OR coalesce(p_lw,0) <= 0 OR coalesce(p_fl,0) <= 0 OR coalesce(p_fw,0) <= 0 THEN RETURN NULL; END IF;
35149
+ FOR rnd IN 1..2 LOOP
35150
+ IF rnd = 1 THEN a0 := p_fl; b0 := p_fw; ELSE a0 := p_fw; b0 := p_fl; END IF;
35151
+ dep := '{}'; used := '{}'; total := 0; n := 0;
35152
+ LOOP
35153
+ EXIT WHEN n >= 100000;
35154
+ placed := false;
35155
+ FOR k IN 1..2 LOOP
35156
+ IF k = 1 THEN oa := a0; ob := b0; ELSE oa := b0; ob := a0; END IF;
35157
+ CONTINUE WHEN oa > p_ll OR ob > p_lw;
35158
+ FOR i IN 1..coalesce(array_length(dep,1),0) LOOP
35159
+ CONTINUE WHEN NOT (used[i] + oa <= p_ll + e);
35160
+ IF ob <= dep[i] + e THEN used[i] := used[i] + oa; placed := true; EXIT; END IF;
35161
+ IF i = array_length(dep,1) AND total - dep[i] + ob <= p_lw + e THEN
35162
+ total := total - dep[i] + ob; dep[i] := ob; used[i] := used[i] + oa; placed := true; EXIT;
35163
+ END IF;
35164
+ END LOOP;
35165
+ EXIT WHEN placed;
35166
+ IF total + ob <= p_lw + e THEN dep := dep || ob; used := used || oa; total := total + ob; placed := true; EXIT; END IF;
35167
+ END LOOP;
35168
+ EXIT WHEN NOT placed;
35169
+ n := n + 1;
35170
+ END LOOP;
35171
+ best := greatest(best, n);
35172
+ END LOOP;
35173
+ RETURN best;
35174
+ END $$;
35175
+
35176
+ -- Productoordeel (zelfde regel als "Gegevens voor karindeling").
35177
+ CREATE OR REPLACE FUNCTION public.logistics_product_oordeel_many(p_product_ids uuid[])
35178
+ RETURNS TABLE(product_id uuid, oordeel text, tekst text, "actieUrl" text)
35179
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35180
+ #variable_conflict use_column
35181
+ DECLARE r record; c record; v_carrier text; ls int; fh numeric; ph numeric; cpl numeric; lpc numeric; laag numeric;
35182
+ blok text[]; waar text[]; mist text[]; hmist text[]; naam text; bron text;
35183
+ c_url constant text := '/logistiek-engine/beheer#gegevens-voor-karindeling';
35184
+ BEGIN
35185
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_product_oordeel_many');
35186
+ IF coalesce(cardinality(p_product_ids), 0) = 0 THEN RETURN; END IF;
35187
+ IF cardinality(p_product_ids) > 1000 THEN
35188
+ RAISE EXCEPTION 'Maximaal 1000 producten per aanroep (ontvangen: %).', cardinality(p_product_ids) USING ERRCODE = '22023';
35189
+ END IF;
35190
+ SELECT nullif(btrim(coalesce(s.settings->'karrenindeling'->'fields'->>'default_carrier_type', s.settings->'karrenindeling'->>'default_carrier_type')), '')
35191
+ INTO v_carrier FROM public.logistics_rule_configs s WHERE s.is_active ORDER BY s.updated_at DESC LIMIT 1;
35192
+ SELECT t.code, t.name, t.layer_length_cm, t.layer_width_cm, t.max_height_cm, t.max_crates_per_layer, t.layers_per_carrier INTO c
35193
+ FROM public.load_carrier_types t
35194
+ WHERE t.is_active AND (t.code = v_carrier OR (v_carrier IS NULL AND t.is_default))
35195
+ ORDER BY (t.code = v_carrier) DESC NULLS LAST LIMIT 1;
35196
+ naam := coalesce(c.name, c.code, 'de ladingdrager');
35197
+
35198
+ FOR r IN SELECT * FROM public.logistics_data_gereedheid(p_product_ids) LOOP
35199
+ blok := '{}'; waar := '{}'; hmist := '{}';
35200
+ mist := array_remove(coalesce(r.ontbrekend, '{}'), 'weight');
35201
+ bron := coalesce(r.velden->'crates_per_layer'->>'bron', 'product');
35202
+ cpl := coalesce(nullif(r.velden->'crates_per_layer'->>'waarde', '')::numeric, 0);
35203
+ lpc := coalesce(nullif(r.velden->'layers_per_cart'->>'waarde', '')::numeric, 0);
35204
+ fh := nullif(r.velden->'fust_height_mm'->>'waarde', '')::numeric / 10.0;
35205
+ ph := nullif(r.velden->'plant_height_cm'->>'waarde', '')::numeric;
35206
+ ls := public.logistics_max_fusten_per_laag(c.layer_length_cm, c.layer_width_cm,
35207
+ nullif(r.velden->'fust_length_mm'->>'waarde', '')::numeric / 10.0, nullif(r.velden->'fust_width_mm'->>'waarde', '')::numeric / 10.0);
35208
+ IF coalesce(fh,0) > 0 AND coalesce(ph,0) > 0 AND coalesce(c.max_height_cm,0) > 0 THEN
35209
+ laag := greatest(fh, ph);
35210
+ IF lpc * laag > c.max_height_cm + 1e-9 THEN
35211
+ blok := blok || format('%s lagen × %s cm = %s cm, terwijl %s %s cm hoog is; er passen maximaal %s lagen',
35212
+ lpc, trim_scale(round(laag,1)), trim_scale(round(lpc*laag,1)), naam, trim_scale(round(c.max_height_cm::numeric,1)), floor(c.max_height_cm / laag));
35213
+ END IF;
35214
+ ELSE
35215
+ IF coalesce(fh,0) <= 0 THEN hmist := hmist || 'fusthoogte'::text; END IF;
35216
+ IF coalesce(ph,0) <= 0 THEN hmist := hmist || 'planthoogte'::text; END IF;
35217
+ IF coalesce(c.max_height_cm,0) <= 0 THEN hmist := hmist || ('hoogte van ' || naam); END IF;
35218
+ END IF;
35219
+ IF ls IS NOT NULL AND cpl > ls THEN
35220
+ blok := blok || format('%s fust per laag (bron: %s), maar op een laag van %s passen er maximaal %s', cpl, bron, naam, ls);
35221
+ END IF;
35222
+ IF coalesce(c.max_crates_per_layer,0) > 0 AND cpl > c.max_crates_per_layer THEN
35223
+ blok := blok || format('%s fust per laag (bron: %s), maar %s staat maximaal %s fust per laag toe', cpl, bron, naam, c.max_crates_per_layer);
35224
+ END IF;
35225
+ IF cardinality(blok) = 0 AND cardinality(hmist) > 0 AND coalesce(c.layers_per_carrier,0) > 0 AND lpc > c.layers_per_carrier THEN
35226
+ waar := waar || format('%s lagen opgegeven, standaard %s; niet te controleren omdat de %s ontbreekt', lpc, c.layers_per_carrier, array_to_string(hmist, ', '));
35227
+ END IF;
35228
+ IF 'weight' = ANY(coalesce(r.ontbrekend, '{}')) THEN waar := waar || 'gewicht per fust ontbreekt'::text; END IF;
35229
+
35230
+ product_id := r.product_id; "actieUrl" := c_url;
35231
+ IF cardinality(blok) > 0 THEN
35232
+ oordeel := 'onmogelijk'; tekst := format('%s past niet op %s: %s.', r.product, naam, array_to_string(blok, '; '));
35233
+ ELSIF cardinality(mist) > 0 THEN
35234
+ oordeel := 'mist_gegevens'; tekst := r.uitleg;
35235
+ ELSIF cardinality(waar) > 0 THEN
35236
+ oordeel := 'waarschuwing'; tekst := format('%s: %s.', r.product, array_to_string(waar, '; '));
35237
+ ELSE
35238
+ oordeel := 'compleet'; tekst := format('%s: gegevens voor karindeling compleet.', r.product);
35239
+ END IF;
35240
+ RETURN NEXT;
35241
+ END LOOP;
35242
+ END $$;
35243
+
35244
+ GRANT EXECUTE ON FUNCTION public.logistics_kar_blokkade(text, numeric, numeric) TO authenticated, service_role;
35245
+ GRANT EXECUTE ON FUNCTION public.logistics_max_fusten_per_laag(numeric, numeric, numeric, numeric) TO authenticated, service_role;
35246
+ GRANT EXECUTE ON FUNCTION public.logistics_order_te_hoge_karren(uuid[]) TO authenticated, service_role;
35247
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_te_hoog(uuid) TO authenticated, service_role;
35248
+ GRANT EXECUTE ON FUNCTION public.logistics_product_oordeel_many(uuid[]) TO authenticated, service_role;
35249
+
35250
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_201b()
35251
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35252
+ SELECT 'kar_te_hoog-regel', to_regprocedure('public.logistics_kar_blokkade(text,numeric,numeric)') IS NOT NULL, 'Blokkade per kar (teHoog/blokkade)'
35253
+ UNION ALL SELECT 'guard pakbonfunctie', position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_cart_slip_plan(uuid)'::regprocedure)) > 0, 'logistics_cart_slip_plan controleert de aanroeper'
35254
+ UNION ALL SELECT 'guard pakbonkopie', to_regprocedure('public.logistics_cart_slip_plan_v200(uuid)') IS NULL OR position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_cart_slip_plan_v200(uuid)'::regprocedure)) > 0, 'logistics_cart_slip_plan_v200 controleert de aanroeper'
35255
+ UNION ALL SELECT 'guard kar-aanvulling', position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_cart_platen_verrijk(jsonb)'::regprocedure)) > 0, 'logistics_cart_platen_verrijk controleert de aanroeper'
35256
+ UNION ALL SELECT 'productoordeel', to_regprocedure('public.logistics_product_oordeel_many(uuid[])') IS NOT NULL, 'Oordeel per product voor het orderformulier'
35257
+ $$;
35258
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_201b() TO authenticated, service_role;