@flowselections/logistiek-engine 1.0.203 → 1.0.209

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.
@@ -35256,3 +35256,1336 @@ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY D
35256
35256
  UNION ALL SELECT 'productoordeel', to_regprocedure('public.logistics_product_oordeel_many(uuid[])') IS NOT NULL, 'Oordeel per product voor het orderformulier'
35257
35257
  $$;
35258
35258
  GRANT EXECUTE ON FUNCTION public.logistics_install_check_201b() TO authenticated, service_role;
35259
+
35260
+ -- ============================================================================
35261
+ -- 1.0.204: capaciteit gemengde kar, vastzetten na vastliggen, geenIndeling.regels, productoordeel met gekozen belading
35262
+ -- ============================================================================
35263
+ -- 1.0.204: capaciteit gemengde kar, vastzetten na vastliggen, geenIndeling.regels, productoordeel met gekozen belading.
35264
+
35265
+ -- Kern (geen guard, SECURITY INVOKER): capaciteit en lagen van één kar.
35266
+ CREATE OR REPLACE FUNCTION public.logistics_cart_capaciteit_core(p_cart_id uuid)
35267
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path TO 'public' AS $$
35268
+ WITH c AS (SELECT oc.cart_number, oc.total_layers_used, oc.max_layers FROM logistics_active.order_carts oc WHERE oc.id = p_cart_id),
35269
+ i AS (SELECT count(*) n, count(*) FILTER (WHERE coalesce(ci.crates_per_layer, 0) > 0) ncpl,
35270
+ sum(ci.crates::numeric / nullif(ci.crates_per_layer, 0)) lagen, max(nullif(ci.layers_per_cart, 0)) lpc
35271
+ FROM logistics_active.order_cart_items ci WHERE ci.cart_id = p_cart_id),
35272
+ v AS (SELECT c.cart_number,
35273
+ CASE WHEN i.n > 0 AND i.n = i.ncpl THEN round(i.lagen, 2) ELSE c.total_layers_used::numeric END lagen,
35274
+ coalesce(i.lpc::numeric, nullif(c.max_layers, 0)::numeric) cap,
35275
+ CASE WHEN i.lpc IS NOT NULL THEN 'product' WHEN coalesce(c.max_layers, 0) > 0 THEN 'ladingdrager' END bron
35276
+ FROM c, i)
35277
+ SELECT jsonb_build_object('layersUsed', v.lagen, 'maxLayers', v.cap, 'capaciteitBron', v.bron,
35278
+ 'vulgraadPct', CASE WHEN coalesce(v.cap, 0) > 0 AND v.lagen IS NOT NULL THEN round(100 * v.lagen / v.cap) END,
35279
+ 'layerUtilizationPct', CASE WHEN coalesce(v.cap, 0) > 0 AND v.lagen IS NOT NULL THEN round(100 * v.lagen / v.cap) END)
35280
+ || public.logistics_kar_blokkade(v.cart_number::text, v.lagen, v.cap)
35281
+ FROM v
35282
+ $$;
35283
+
35284
+ CREATE OR REPLACE FUNCTION public.logistics_cart_platen_verrijk(p_cart jsonb)
35285
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35286
+ DECLARE v_id uuid; k jsonb; c jsonb := p_cart; v_l numeric;
35287
+ BEGIN
35288
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_cart_platen_verrijk');
35289
+ BEGIN v_id := nullif(coalesce(p_cart->>'cartId', p_cart->>'cart_id'), '')::uuid; EXCEPTION WHEN invalid_text_representation THEN v_id := NULL; END;
35290
+ IF v_id IS NOT NULL THEN k := public.logistics_cart_capaciteit_core(v_id); END IF;
35291
+ IF k IS NOT NULL THEN
35292
+ c := c || jsonb_build_object('layersUsed', k->'layersUsed', 'maxLayers', k->'maxLayers', 'capaciteitBron', k->'capaciteitBron',
35293
+ 'vulgraadPct', k->'vulgraadPct', 'layerUtilizationPct', k->'layerUtilizationPct');
35294
+ END IF;
35295
+ v_l := nullif(c->>'layersUsed', '')::numeric;
35296
+ RETURN c || jsonb_build_object(
35297
+ 'additionalLayers', public.logistics_extra_platen(v_l),
35298
+ 'ladingdragerTekst', public.logistics_ladingdrager_tekst(c->>'carrierType', v_l,
35299
+ coalesce(nullif(c->>'vulgraadPct', '')::numeric, nullif(c->>'layerUtilizationPct', '')::numeric, nullif(c->>'occupancyPct', '')::numeric)))
35300
+ || public.logistics_kar_blokkade(coalesce(c->>'cartNumber', c->>'cart_number', c->>'seq'), v_l, nullif(c->>'maxLayers', '')::numeric);
35301
+ END $$;
35302
+
35303
+ -- Te hoge karren: alleen karren met eigen regels van de order.
35304
+ CREATE OR REPLACE FUNCTION public.logistics_order_te_hoge_karren(p_order_ids uuid[])
35305
+ RETURNS TABLE(order_id uuid, cart_id uuid, cart_number integer, lagen numeric, max_lagen numeric, blokkade jsonb)
35306
+ LANGUAGE plpgsql STABLE SET search_path TO 'public' AS $$
35307
+ BEGIN
35308
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_te_hoge_karren');
35309
+ RETURN QUERY
35310
+ SELECT x.order_id, x.cart_id, x.nr, (x.k->>'layersUsed')::numeric, (x.k->>'maxLayers')::numeric,
35311
+ public.logistics_kar_blokkade(x.nr::text, (x.k->>'layersUsed')::numeric, (x.k->>'maxLayers')::numeric)->'blokkade'
35312
+ FROM (SELECT u.order_id, u.cart_id, coalesce(u.display_seq, u.cart_number) nr, public.logistics_cart_capaciteit_core(u.cart_id) k
35313
+ FROM public.logistics_order_unit_carts_core(p_order_ids) u
35314
+ WHERE EXISTS (SELECT 1 FROM logistics_active.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
35315
+ WHERE ci.cart_id = u.cart_id AND oi.order_id = u.order_id)) x
35316
+ WHERE coalesce((x.k->>'teHoog')::boolean, false);
35317
+ END $$;
35318
+
35319
+ CREATE OR REPLACE FUNCTION public.logistics_cart_te_hoog(p_cart_id uuid)
35320
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35321
+ DECLARE k jsonb;
35322
+ BEGIN
35323
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_cart_te_hoog');
35324
+ k := public.logistics_cart_capaciteit_core(p_cart_id);
35325
+ IF k IS NULL THEN RETURN jsonb_build_object('teHoog', false, 'blokkade', NULL); END IF;
35326
+ RETURN jsonb_build_object('teHoog', k->'teHoog', 'blokkade', k->'blokkade');
35327
+ END $$;
35328
+
35329
+ -- Korte zinnen uit een redentekst.
35330
+ CREATE OR REPLACE FUNCTION public.logistics_tekst_regels(p_tekst text)
35331
+ RETURNS jsonb LANGUAGE sql IMMUTABLE SET search_path TO 'public' AS $$
35332
+ SELECT coalesce(jsonb_agg(z ORDER BY o), '[]'::jsonb)
35333
+ FROM (SELECT btrim(regexp_replace(s, '[;\s]+$', '')) z, o
35334
+ FROM regexp_split_to_table(coalesce(p_tekst, ''), '(?<=[.;])\s+') WITH ORDINALITY t(s, o)) q
35335
+ WHERE z <> ''
35336
+ $$;
35337
+
35338
+ -- Vastliggend (geprint/gescand/in pick), zonder guard: alleen voor gebruik in bewaakte functies.
35339
+ CREATE OR REPLACE FUNCTION public.logistics_cart_vast_core(p_cart_id uuid)
35340
+ RETURNS boolean LANGUAGE sql STABLE SET search_path TO 'public' AS $$
35341
+ SELECT p_cart_id IS NOT NULL AND (
35342
+ EXISTS (SELECT 1 FROM public.print_jobs pj
35343
+ WHERE pj.payload->>'cart_id' = p_cart_id::text AND pj.documenttype = 'pakbon'
35344
+ AND coalesce(pj.is_herprint, false) = false AND pj.status IN ('wachtend', 'bezig', 'voltooid')
35345
+ AND coalesce(pj.foutcode, '') NOT IN ('verlopen', 'expired')
35346
+ AND coalesce(pj.verwerkingsfase, '') NOT IN ('verlopen', 'expired'))
35347
+ OR EXISTS (SELECT 1 FROM public.cart_control_items ci WHERE ci.cart_number_id = p_cart_id)
35348
+ OR EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.cancelled_at IS NULL
35349
+ AND jsonb_typeof(q.printed_cart_ids) = 'array' AND q.printed_cart_ids ? p_cart_id::text))
35350
+ $$;
35351
+
35352
+ -- Karindeling verouderd: vastgezette belading wijkt af van wat op de karren staat.
35353
+ CREATE OR REPLACE FUNCTION public.logistics_order_indeling_verouderd_core(p_order_id uuid)
35354
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path TO 'public' AS $$
35355
+ WITH a AS (
35356
+ SELECT oi.id, coalesce(oi.product_name, 'regel') naam, ci.cart_id,
35357
+ (oi.plants_per_crate IS DISTINCT FROM ci.plants_per_crate
35358
+ OR oi.crates_per_layer IS DISTINCT FROM ci.crates_per_layer
35359
+ OR oi.layers_per_cart IS DISTINCT FROM ci.layers_per_cart) afwijkend
35360
+ FROM public.order_items oi
35361
+ JOIN logistics_active.order_cart_items ci ON ci.order_item_id = oi.id
35362
+ WHERE oi.order_id = p_order_id AND oi.packing_locked_at IS NOT NULL)
35363
+ SELECT CASE WHEN bool_or(afwijkend) THEN jsonb_build_object(
35364
+ 'verouderd', true,
35365
+ 'producten', to_jsonb(array_agg(DISTINCT naam) FILTER (WHERE afwijkend)),
35366
+ 'vastliggendeKarren', (SELECT count(DISTINCT a2.cart_id) FROM a a2 WHERE public.logistics_cart_vast_core(a2.cart_id)),
35367
+ 'tekst', 'De belading is gewijzigd, maar de karindeling is nog oud. Deel de niet-geprinte karren opnieuw in.')
35368
+ ELSE NULL END
35369
+ FROM a
35370
+ $$;
35371
+
35372
+ -- geenIndeling met korte zinnen.
35373
+ CREATE OR REPLACE FUNCTION public.logistics_order_geen_indeling_core(p_order_id uuid)
35374
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35375
+ DECLARE o record; q record; d record; t record; v_code text; v_tekst text; v_soort text; v_actie text;
35376
+ c_beheer constant text := '/logistiek-engine/beheer';
35377
+ BEGIN
35378
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_geen_indeling_core');
35379
+ SELECT id, order_number, buffer_id INTO o FROM public.orders WHERE id = p_order_id;
35380
+ IF NOT FOUND THEN RETURN NULL; END IF;
35381
+ SELECT * INTO t FROM public.logistics_order_te_hoge_karren(ARRAY[p_order_id]) ORDER BY cart_number LIMIT 1;
35382
+ IF FOUND THEN
35383
+ v_code := 'kar_te_hoog'; v_tekst := t.blokkade->>'tekst'; v_actie := 'belading_aanpassen';
35384
+ ELSE
35385
+ SELECT status, reason, reason_code INTO q FROM public.logistics_plan_queue
35386
+ WHERE order_id = p_order_id ORDER BY updated_at DESC NULLS LAST LIMIT 1;
35387
+ IF FOUND AND q.status = 'geblokkeerd' THEN
35388
+ v_code := coalesce(q.reason_code, 'geblokkeerd'); v_tekst := q.reason;
35389
+ ELSE
35390
+ SELECT dd.result, dd.explanation, dd.input_snapshot INTO d
35391
+ FROM public.logistics_decisions dd
35392
+ WHERE dd.slot = 'beladingindeling'
35393
+ AND ((dd.subject_type = 'order' AND dd.subject_id = o.id::text)
35394
+ OR (dd.subject_type = 'buffer' AND o.buffer_id IS NOT NULL AND dd.subject_id = o.buffer_id::text)
35395
+ OR (o.order_number IS NOT NULL AND coalesce(dd.input_snapshot->'members', '[]'::jsonb) ? o.order_number))
35396
+ ORDER BY dd.created_at DESC LIMIT 1;
35397
+ IF FOUND AND coalesce((d.result->>'blocked')::boolean, false) THEN
35398
+ v_code := coalesce(d.result->>'reason_code', 'geblokkeerd'); v_tekst := d.explanation;
35399
+ v_soort := d.input_snapshot->>'soort';
35400
+ ELSIF q.status IN ('pending', 'running') THEN
35401
+ v_code := 'wacht_op_indeling'; v_tekst := 'De karindeling wordt gemaakt; dit duurt meestal enkele seconden.';
35402
+ ELSE
35403
+ v_code := 'onbekend';
35404
+ 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.';
35405
+ END IF;
35406
+ END IF;
35407
+ IF v_code LIKE 'belading%' OR v_code = 'ladingdrager_niet_in_beheer' THEN v_actie := 'belading_aanpassen'; END IF;
35408
+ END IF;
35409
+ v_tekst := coalesce(nullif(btrim(v_tekst), ''), 'Geen indeling; de reden is niet bekend.');
35410
+ RETURN jsonb_build_object('code', v_code, 'soort', v_soort, 'tekst', v_tekst,
35411
+ 'regels', public.logistics_tekst_regels(v_tekst),
35412
+ 'actie', coalesce(v_actie, 'beheer'),
35413
+ 'actieLabel', CASE WHEN v_actie IS NOT NULL THEN 'Belading aanpassen' ELSE 'Naar Engine-beheer' END,
35414
+ 'actieUrl', CASE WHEN v_actie IS NOT NULL THEN c_beheer || '#gegevens-voor-karindeling' ELSE c_beheer END);
35415
+ END $$;
35416
+
35417
+ -- Coverage: per kar capaciteit uit de producten, één vulgraad; indelingVerouderd op orderniveau.
35418
+ CREATE OR REPLACE FUNCTION public.logistics_order_plan_coverage(p_order_id uuid)
35419
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35420
+ DECLARE b jsonb; x jsonb; v_hoog boolean;
35421
+ BEGIN
35422
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_plan_coverage');
35423
+ b := public.logistics_order_plan_coverage_v165(p_order_id);
35424
+ x := public.logistics_order_external_state(p_order_id);
35425
+ IF jsonb_typeof(b->'carts') = 'array' THEN
35426
+ b := b || jsonb_build_object('carts', (
35427
+ SELECT coalesce(jsonb_agg(
35428
+ CASE WHEN c.id IS NULL THEN t.e || jsonb_build_object('additionalLayers', NULL, 'layersUsed', NULL, 'teHoog', false, 'blokkade', NULL,
35429
+ 'capaciteitBron', NULL, 'vulgraadPct', NULL)
35430
+ ELSE public.logistics_cart_platen_verrijk(t.e || jsonb_build_object('occupancyPct', c.occupancy_pct)) END
35431
+ ORDER BY t.o), '[]'::jsonb)
35432
+ FROM jsonb_array_elements(b->'carts') WITH ORDINALITY t(e, o)
35433
+ LEFT JOIN logistics_active.order_carts c ON c.id::text = t.e->>'cartId'));
35434
+ END IF;
35435
+ IF x IS NOT NULL AND (x->>'full')::boolean THEN
35436
+ b := b || jsonb_build_object('state', 'afgehandeld_extern', 'label', 'Afgehandeld in Floriday',
35437
+ 'reason', 'De order is in Floriday afgehandeld; de Engine plant en print er niets meer voor.');
35438
+ END IF;
35439
+ v_hoog := EXISTS (SELECT 1 FROM public.logistics_order_te_hoge_karren(ARRAY[p_order_id]));
35440
+ IF v_hoog AND b->>'state' IS DISTINCT FROM 'afgehandeld_extern' THEN
35441
+ b := b || jsonb_build_object('label', 'Ingedeeld — kar te hoog');
35442
+ END IF;
35443
+ b := b || jsonb_build_object('geenIndeling',
35444
+ CASE WHEN b->>'state' IN ('geen_indeling', 'deels') OR (v_hoog AND b->>'state' IS DISTINCT FROM 'afgehandeld_extern')
35445
+ THEN public.logistics_order_geen_indeling_core(p_order_id) END,
35446
+ 'indelingVerouderd', public.logistics_order_indeling_verouderd_core(p_order_id));
35447
+ RETURN b || jsonb_build_object('externalFulfilled', x);
35448
+ END $$;
35449
+
35450
+ -- Vastzet-voorvertoning: meldt vastliggende karren en de mogelijke actie.
35451
+ DO $do$ BEGIN
35452
+ IF to_regprocedure('public.logistics_belading_vastzet_preview_v201(uuid,jsonb)') IS NULL THEN
35453
+ EXECUTE replace(pg_get_functiondef('public.logistics_belading_vastzet_preview(uuid,jsonb)'::regprocedure),
35454
+ 'public.logistics_belading_vastzet_preview(', 'public.logistics_belading_vastzet_preview_v201(');
35455
+ END IF;
35456
+ END $do$;
35457
+
35458
+ CREATE OR REPLACE FUNCTION public.logistics_belading_vastzet_preview(p_order_item_id uuid, p_wijziging jsonb)
35459
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35460
+ DECLARE pv jsonb; v_order uuid; v_vast int; v_totaal int; v_tekst text;
35461
+ BEGIN
35462
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_belading_vastzet_preview');
35463
+ pv := public.logistics_belading_vastzet_preview_v201(p_order_item_id, p_wijziging);
35464
+ SELECT order_id INTO v_order FROM public.order_items WHERE id = p_order_item_id;
35465
+ SELECT count(DISTINCT u.cart_id), count(DISTINCT u.cart_id) FILTER (WHERE public.logistics_cart_vast_core(u.cart_id))
35466
+ INTO v_totaal, v_vast FROM public.logistics_order_unit_carts_core(ARRAY[v_order]) u;
35467
+ IF coalesce(v_vast, 0) > 0 THEN
35468
+ v_tekst := format('%s van %s kar(ren) zijn al geprint of in pick; de nieuwe belading geldt pas na herindelen. Herindelen verandert alleen de niet-geprinte karren.',
35469
+ v_vast, v_totaal);
35470
+ pv := pv || jsonb_build_object('tekst', (pv->>'tekst') || ' ' || v_tekst, 'bevestigingNodig', true);
35471
+ END IF;
35472
+ RETURN pv || jsonb_build_object('vastliggendeKarren', coalesce(v_vast, 0), 'karrenTotaal', coalesce(v_totaal, 0),
35473
+ 'vastliggendTekst', v_tekst,
35474
+ 'acties', CASE WHEN coalesce(v_vast, 0) > 0
35475
+ THEN jsonb_build_array(jsonb_build_object('actie', 'herindelen_niet_geprint', 'label', 'Niet-geprinte karren opnieuw indelen',
35476
+ 'rpc', 'logistics_order_herindelen'))
35477
+ ELSE '[]'::jsonb END);
35478
+ END $$;
35479
+
35480
+ -- Herindelen van de niet-geprinte karren (vastliggende karren blijven staan).
35481
+ CREATE OR REPLACE FUNCTION public.logistics_order_herindelen(p_order_id uuid)
35482
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
35483
+ DECLARE v_vast int;
35484
+ BEGIN
35485
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_herindelen');
35486
+ IF auth.uid() IS NULL AND NOT public._scan_is_server() THEN RAISE EXCEPTION 'niet ingelogd'; END IF;
35487
+ IF NOT EXISTS (SELECT 1 FROM public.orders WHERE id = p_order_id) THEN RAISE EXCEPTION 'Order niet gevonden.'; END IF;
35488
+ SELECT count(DISTINCT u.cart_id) FILTER (WHERE public.logistics_cart_vast_core(u.cart_id)) INTO v_vast
35489
+ FROM public.logistics_order_unit_carts_core(ARRAY[p_order_id]) u;
35490
+ PERFORM public.logistics_plan_enqueue(p_order_id, 'belading gewijzigd: niet-geprinte karren opnieuw indelen');
35491
+ RETURN jsonb_build_object('ok', true, 'vastliggendeKarren', coalesce(v_vast, 0),
35492
+ 'tekst', CASE WHEN coalesce(v_vast, 0) > 0 THEN format('Herindeling gestart; %s vastliggende kar(ren) blijven staan.', v_vast)
35493
+ ELSE 'Herindeling gestart.' END);
35494
+ END $$;
35495
+
35496
+ -- Productoordeel met de gekozen belading per regel (zelfde fysieke regel als Beheer).
35497
+ CREATE OR REPLACE FUNCTION public.logistics_product_oordeel_belading(p_regels jsonb)
35498
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35499
+ DECLARE e jsonb; g record; c record; v_def text; v_code text; ids uuid[]; uit jsonb := '[]'::jsonb;
35500
+ ls int; fh numeric; ph numeric; cpl numeric; lpc numeric; ppc numeric; laag numeric; naam text;
35501
+ blok text[]; waar text[]; mist text[]; hmist text[]; v_pid uuid; v_oordeel text; v_tekst text;
35502
+ BEGIN
35503
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_product_oordeel_belading');
35504
+ IF p_regels IS NULL OR jsonb_typeof(p_regels) <> 'array' THEN RAISE EXCEPTION 'p_regels moet een lijst zijn.' USING ERRCODE = '22023'; END IF;
35505
+ IF jsonb_array_length(p_regels) > 1000 THEN
35506
+ RAISE EXCEPTION 'Maximaal 1000 regels per aanroep (ontvangen: %).', jsonb_array_length(p_regels) USING ERRCODE = '22023';
35507
+ END IF;
35508
+ SELECT array_agg(DISTINCT (x->>'product_id')::uuid) INTO ids FROM jsonb_array_elements(p_regels) x WHERE nullif(x->>'product_id', '') IS NOT NULL;
35509
+ CREATE TEMP TABLE IF NOT EXISTS _oordeel_g ON COMMIT DROP AS SELECT * FROM public.logistics_data_gereedheid(ARRAY[]::uuid[]) LIMIT 0;
35510
+ DELETE FROM _oordeel_g WHERE true;
35511
+ IF ids IS NOT NULL THEN INSERT INTO _oordeel_g SELECT * FROM public.logistics_data_gereedheid(ids); END IF;
35512
+ SELECT nullif(btrim(coalesce(s.settings->'karrenindeling'->'fields'->>'default_carrier_type', s.settings->'karrenindeling'->>'default_carrier_type')), '')
35513
+ INTO v_def FROM public.logistics_rule_configs s WHERE s.is_active ORDER BY s.updated_at DESC LIMIT 1;
35514
+
35515
+ FOR e IN SELECT x FROM jsonb_array_elements(p_regels) x LOOP
35516
+ blok := '{}'; waar := '{}'; mist := '{}'; hmist := '{}';
35517
+ v_pid := nullif(e->>'product_id', '')::uuid;
35518
+ SELECT * INTO g FROM _oordeel_g WHERE product_id = v_pid;
35519
+ v_code := coalesce(nullif(btrim(e->>'carrier_code'), ''), v_def);
35520
+ 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
35521
+ FROM public.load_carrier_types t
35522
+ WHERE t.is_active AND (t.code = v_code OR (v_code IS NULL AND t.is_default))
35523
+ ORDER BY (t.code = v_code) DESC NULLS LAST LIMIT 1;
35524
+ naam := coalesce(c.name, c.code, 'de ladingdrager');
35525
+ ppc := nullif(e->>'plants_per_crate', '')::numeric;
35526
+ cpl := nullif(e->>'crates_per_layer', '')::numeric;
35527
+ lpc := nullif(e->>'layers_per_cart', '')::numeric;
35528
+ IF coalesce(ppc, 0) <= 0 THEN mist := mist || 'planten per fust'::text; END IF;
35529
+ IF coalesce(cpl, 0) <= 0 THEN mist := mist || 'fusten per laag'::text; END IF;
35530
+ IF coalesce(lpc, 0) <= 0 THEN mist := mist || 'lagen per kar'::text; END IF;
35531
+ IF v_code IS NOT NULL AND c.code IS NULL THEN blok := blok || format('ladingdrager %s staat niet in Beheer', v_code); END IF;
35532
+ IF g.product_id IS NULL THEN
35533
+ mist := mist || 'product onbekend'::text;
35534
+ ELSE
35535
+ fh := nullif(g.velden->'fust_height_mm'->>'waarde', '')::numeric / 10.0;
35536
+ ph := nullif(g.velden->'plant_height_cm'->>'waarde', '')::numeric;
35537
+ ls := public.logistics_max_fusten_per_laag(c.layer_length_cm, c.layer_width_cm,
35538
+ nullif(g.velden->'fust_length_mm'->>'waarde', '')::numeric / 10.0, nullif(g.velden->'fust_width_mm'->>'waarde', '')::numeric / 10.0);
35539
+ IF coalesce(lpc,0) > 0 AND coalesce(fh,0) > 0 AND coalesce(ph,0) > 0 AND coalesce(c.max_height_cm,0) > 0 THEN
35540
+ laag := greatest(fh, ph);
35541
+ IF lpc * laag > c.max_height_cm + 1e-9 THEN
35542
+ blok := blok || format('%s lagen × %s cm = %s cm, terwijl %s %s cm hoog is; er passen maximaal %s lagen',
35543
+ 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));
35544
+ END IF;
35545
+ ELSE
35546
+ IF coalesce(fh,0) <= 0 THEN hmist := hmist || 'fusthoogte'::text; END IF;
35547
+ IF coalesce(ph,0) <= 0 THEN hmist := hmist || 'planthoogte'::text; END IF;
35548
+ IF coalesce(c.max_height_cm,0) <= 0 THEN hmist := hmist || ('hoogte van ' || naam); END IF;
35549
+ END IF;
35550
+ IF ls IS NOT NULL AND coalesce(cpl,0) > ls THEN
35551
+ blok := blok || format('%s fust per laag, maar op een laag van %s passen er maximaal %s', cpl, naam, ls);
35552
+ END IF;
35553
+ IF coalesce(c.max_crates_per_layer,0) > 0 AND coalesce(cpl,0) > c.max_crates_per_layer THEN
35554
+ blok := blok || format('%s fust per laag, maar %s staat maximaal %s fust per laag toe', cpl, naam, c.max_crates_per_layer);
35555
+ END IF;
35556
+ IF cardinality(blok) = 0 AND cardinality(hmist) > 0 AND coalesce(c.layers_per_carrier,0) > 0 AND coalesce(lpc,0) > c.layers_per_carrier THEN
35557
+ waar := waar || format('%s lagen opgegeven, standaard %s; niet te controleren omdat de %s ontbreekt', lpc, c.layers_per_carrier, array_to_string(hmist, ', '));
35558
+ END IF;
35559
+ IF 'weight' = ANY(coalesce(g.ontbrekend, '{}')) THEN waar := waar || 'gewicht per fust ontbreekt'::text; END IF;
35560
+ END IF;
35561
+ IF cardinality(blok) > 0 THEN
35562
+ v_oordeel := 'onmogelijk'; v_tekst := format('%s past niet op %s.', coalesce(g.product, 'Product'), naam);
35563
+ ELSIF cardinality(mist) > 0 THEN
35564
+ v_oordeel := 'mist_gegevens'; v_tekst := format('%s: %s ontbreekt.', coalesce(g.product, 'Product'), array_to_string(mist, ', '));
35565
+ ELSIF cardinality(waar) > 0 THEN
35566
+ v_oordeel := 'waarschuwing'; v_tekst := format('%s: belading mogelijk, met waarschuwing.', g.product);
35567
+ ELSE
35568
+ v_oordeel := 'compleet'; v_tekst := format('%s: belading past.', g.product);
35569
+ END IF;
35570
+ uit := uit || jsonb_build_array(jsonb_build_object('product_id', v_pid, 'oordeel', v_oordeel, 'tekst', v_tekst,
35571
+ 'regels', to_jsonb(ARRAY(SELECT upper(left(z,1)) || substr(z,2) || '.' FROM unnest(blok || mist || waar) z))));
35572
+ END LOOP;
35573
+ RETURN uit;
35574
+ END $$;
35575
+
35576
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_capaciteit_core(uuid) TO authenticated, service_role;
35577
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_vast_core(uuid) TO service_role;
35578
+ GRANT EXECUTE ON FUNCTION public.logistics_order_indeling_verouderd_core(uuid) TO service_role;
35579
+ GRANT EXECUTE ON FUNCTION public.logistics_tekst_regels(text) TO authenticated, service_role;
35580
+ GRANT EXECUTE ON FUNCTION public.logistics_order_herindelen(uuid) TO authenticated, service_role;
35581
+ GRANT EXECUTE ON FUNCTION public.logistics_product_oordeel_belading(jsonb) TO authenticated, service_role;
35582
+
35583
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_204()
35584
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35585
+ SELECT 'capaciteit gemengde kar', to_regprocedure('public.logistics_cart_capaciteit_core(uuid)') IS NOT NULL, 'Capaciteit = hoogste lagen per kar van de producten'
35586
+ UNION ALL SELECT 'te hoog per kar', position('oi.order_id = u.order_id' IN pg_get_functiondef('public.logistics_order_te_hoge_karren(uuid[])'::regprocedure)) > 0, 'Alleen karren met eigen regels'
35587
+ UNION ALL SELECT 'indeling verouderd', position('indelingVerouderd' IN pg_get_functiondef('public.logistics_order_plan_coverage(uuid)'::regprocedure)) > 0, 'Coverage meldt verouderde indeling'
35588
+ UNION ALL SELECT 'vastzet vastliggend', position('vastliggendeKarren' IN pg_get_functiondef('public.logistics_belading_vastzet_preview(uuid,jsonb)'::regprocedure)) > 0, 'Voorvertoning meldt vastliggende karren'
35589
+ UNION ALL SELECT 'geenIndeling.regels', position('logistics_tekst_regels' IN pg_get_functiondef('public.logistics_order_geen_indeling_core(uuid)'::regprocedure)) > 0, 'Reden in korte zinnen'
35590
+ UNION ALL SELECT 'productoordeel belading', to_regprocedure('public.logistics_product_oordeel_belading(jsonb)') IS NOT NULL, 'Oordeel met gekozen belading (Verkoop)'
35591
+ UNION ALL SELECT 'guards 1.0.204', position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_order_herindelen(uuid)'::regprocedure)) > 0
35592
+ AND position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_product_oordeel_belading(jsonb)'::regprocedure)) > 0, 'Nieuwe functies controleren de aanroeper'
35593
+ $$;
35594
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_204() TO authenticated, service_role;
35595
+ -- 1.0.204: tekst_regels zonder lookbehind
35596
+ CREATE OR REPLACE FUNCTION public.logistics_tekst_regels(p_tekst text)
35597
+ RETURNS jsonb LANGUAGE sql IMMUTABLE SET search_path TO 'public' AS $$
35598
+ SELECT coalesce(jsonb_agg(z || CASE WHEN z ~ '[.!?]$' THEN '' ELSE '.' END ORDER BY o), '[]'::jsonb)
35599
+ FROM (SELECT btrim(s) z, o FROM regexp_split_to_table(coalesce(p_tekst, ''), '[.;]\s+') WITH ORDINALITY t(s, o)) q
35600
+ WHERE z <> ''
35601
+ $$;
35602
+
35603
+ -- ===== 1.0.204 vervolg: menging per laag, printer verzendbrief =====
35604
+ -- 1.0.204 vervolg: menging per laag in de capaciteitsberekening; printer van de volgende verzendbrief.
35605
+
35606
+ CREATE OR REPLACE FUNCTION public.logistics_laag_menging_core()
35607
+ RETURNS jsonb LANGUAGE plpgsql STABLE SET search_path TO 'public' AS $$
35608
+ DECLARE k jsonb; v_opt boolean; v_kpl int; v_bel int; v_een boolean := false;
35609
+ BEGIN
35610
+ SELECT coalesce(s.settings->'karrenindeling'->'fields', s.settings->'karrenindeling') INTO k
35611
+ FROM public.logistics_rule_configs s WHERE s.is_active ORDER BY s.updated_at DESC LIMIT 1;
35612
+ v_opt := coalesce((k->>'optimal_fill')::boolean, true);
35613
+ v_kpl := nullif(k->>'max_customers_per_layer', '')::numeric::int;
35614
+ IF to_regclass('public.cart_planning_settings') IS NOT NULL THEN
35615
+ EXECUTE 'SELECT max_beladingen_per_layer, coalesce(max_one_customer_per_layer, false) FROM public.cart_planning_settings LIMIT 1'
35616
+ INTO v_bel, v_een;
35617
+ END IF;
35618
+ v_bel := CASE WHEN NOT v_opt THEN 1 ELSE coalesce(v_bel, 2) END;
35619
+ IF NOT v_opt THEN RETURN jsonb_build_object('menging', 'per_regel', 'reden', 'optimale vulling staat uit'); END IF;
35620
+ IF v_bel > 0 AND v_bel < 999 THEN RETURN jsonb_build_object('menging', 'per_regel', 'reden', format('maximaal %s beladingen per laag', v_bel)); END IF;
35621
+ IF coalesce(v_een, false) THEN RETURN jsonb_build_object('menging', 'per_regel', 'reden', 'één klant per laag'); END IF;
35622
+ IF coalesce(v_kpl, 0) > 0 THEN RETURN jsonb_build_object('menging', 'per_regel', 'reden', format('maximaal %s klant(en) per laag', v_kpl)); END IF;
35623
+ RETURN jsonb_build_object('menging', 'vrij', 'reden', 'mengen per laag toegestaan');
35624
+ END $$;
35625
+
35626
+ CREATE OR REPLACE FUNCTION public.logistics_cart_capaciteit_core(p_cart_id uuid)
35627
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path TO 'public' AS $$
35628
+ WITH m AS (SELECT public.logistics_laag_menging_core()->>'menging' = 'per_regel' hele),
35629
+ c AS (SELECT oc.cart_number, oc.total_layers_used, oc.max_layers FROM logistics_active.order_carts oc WHERE oc.id = p_cart_id),
35630
+ i AS (SELECT count(*) n, count(*) FILTER (WHERE coalesce(ci.crates_per_layer, 0) > 0) ncpl,
35631
+ sum(CASE WHEN m.hele THEN ceil(ci.crates::numeric / nullif(ci.crates_per_layer, 0) - 1e-9)
35632
+ ELSE ci.crates::numeric / nullif(ci.crates_per_layer, 0) END) lagen,
35633
+ max(nullif(ci.layers_per_cart, 0)) lpc
35634
+ FROM logistics_active.order_cart_items ci, m WHERE ci.cart_id = p_cart_id),
35635
+ v AS (SELECT c.cart_number,
35636
+ CASE WHEN i.n > 0 AND i.n = i.ncpl THEN round(i.lagen, 2) ELSE c.total_layers_used::numeric END lagen,
35637
+ coalesce(i.lpc::numeric, nullif(c.max_layers, 0)::numeric) cap,
35638
+ CASE WHEN i.lpc IS NOT NULL THEN 'product' WHEN coalesce(c.max_layers, 0) > 0 THEN 'ladingdrager' END bron
35639
+ FROM c, i)
35640
+ SELECT jsonb_build_object('layersUsed', v.lagen, 'maxLayers', v.cap, 'capaciteitBron', v.bron,
35641
+ 'laagMenging', (SELECT CASE WHEN hele THEN 'per_regel' ELSE 'vrij' END FROM m),
35642
+ 'vulgraadPct', CASE WHEN coalesce(v.cap, 0) > 0 AND v.lagen IS NOT NULL THEN round(100 * v.lagen / v.cap) END,
35643
+ 'layerUtilizationPct', CASE WHEN coalesce(v.cap, 0) > 0 AND v.lagen IS NOT NULL THEN round(100 * v.lagen / v.cap) END)
35644
+ || public.logistics_kar_blokkade(v.cart_number::text, v.lagen, v.cap)
35645
+ FROM v
35646
+ $$;
35647
+
35648
+ -- Welke printer krijgt de volgende verzendbrief (route "verzendbrief bij pakbon")?
35649
+ CREATE OR REPLACE FUNCTION public.logistics_shipping_note_printer(p_order_id uuid DEFAULT NULL)
35650
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35651
+ DECLARE slip record; v_plek uuid; v_wp text; r jsonb; v_naam text; v_pid uuid;
35652
+ BEGIN
35653
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_shipping_note_printer');
35654
+ IF to_regprocedure('public.printer_routering(text,uuid,text,uuid,text,uuid)') IS NULL THEN
35655
+ RETURN jsonb_build_object('printerId', NULL, 'printerNaam', NULL, 'regel', NULL,
35656
+ 'reden', 'De printerregels van Layouts zijn niet geïnstalleerd; de printer is niet te bepalen.');
35657
+ END IF;
35658
+ SELECT pj.id, pj.payload INTO slip FROM public.print_jobs pj
35659
+ WHERE pj.documenttype::text = 'pakbon'
35660
+ AND (p_order_id IS NULL OR pj.payload->>'order_id' = p_order_id::text)
35661
+ ORDER BY pj.created_at DESC LIMIT 1;
35662
+ IF slip.id IS NOT NULL THEN
35663
+ BEGIN v_plek := nullif(coalesce(slip.payload->>'print_plek_id', slip.payload->>'printPlekId'), '')::uuid;
35664
+ EXCEPTION WHEN invalid_text_representation THEN v_plek := NULL; END;
35665
+ v_wp := nullif(btrim(coalesce(slip.payload->>'werkplek', slip.payload->>'computer_naam', '')), '');
35666
+ END IF;
35667
+ r := public.printer_routering('afleverbrief', v_plek, v_wp, NULL, NULL, NULL);
35668
+ v_pid := nullif(r->>'printer_id', '')::uuid;
35669
+ IF v_pid IS NOT NULL THEN
35670
+ SELECT coalesce(nullif(p.weergavenaam, ''), p.naam) INTO v_naam FROM public.printers p WHERE p.id = v_pid;
35671
+ END IF;
35672
+ RETURN jsonb_build_object(
35673
+ 'printerId', v_pid, 'printerNaam', v_naam,
35674
+ 'regel', CASE WHEN v_pid IS NULL THEN NULL ELSE jsonb_build_object('bron', r->>'bron', 'regelId', r->>'regel_id',
35675
+ 'printPlekId', v_plek, 'werkplek', v_wp, 'pakbonJobId', slip.id) END,
35676
+ 'reden', CASE WHEN v_pid IS NOT NULL THEN NULL
35677
+ WHEN slip.id IS NULL THEN 'Er is nog geen pakbon geprint; zonder printplek is de printer van de verzendbrief niet te bepalen.'
35678
+ ELSE 'Er is geen printerregel voor de verzendbrief op deze printplek. Stel die in bij Layouts → printers.' END,
35679
+ 'overgeslagen', r->'overgeslagen');
35680
+ END $$;
35681
+ GRANT EXECUTE ON FUNCTION public.logistics_shipping_note_printer(uuid) TO authenticated, service_role;
35682
+ GRANT EXECUTE ON FUNCTION public.logistics_laag_menging_core() TO service_role;
35683
+
35684
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_204b()
35685
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35686
+ SELECT 'menging per laag', position('logistics_laag_menging_core' IN pg_get_functiondef('public.logistics_cart_capaciteit_core(uuid)'::regprocedure)) > 0, 'Capaciteit telt begonnen lagen per regel als menging beperkt is'
35687
+ UNION ALL SELECT 'printer verzendbrief', to_regprocedure('public.logistics_shipping_note_printer(uuid)') IS NOT NULL
35688
+ AND position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_shipping_note_printer(uuid)'::regprocedure)) > 0, 'Leesfunctie met eigen toegangscontrole'
35689
+ $$;
35690
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_204b() TO authenticated, service_role;
35691
+
35692
+ -- ===== 1.0.205: mengregel per laag via groepen, quarantaine overslaan =====
35693
+ -- 1.0.205: mengregel per laag via groepen; regels in quarantaine overal overslaan.
35694
+
35695
+ CREATE OR REPLACE FUNCTION public.logistics_laag_menging_core()
35696
+ RETURNS jsonb LANGUAGE plpgsql STABLE SET search_path TO 'public' AS $$
35697
+ DECLARE k jsonb; v_opt boolean; v_kpl int; v_bel int; v_een boolean := false; v_kl int; v_b int; v_reden text[] := '{}';
35698
+ BEGIN
35699
+ SELECT coalesce(s.settings->'karrenindeling'->'fields', s.settings->'karrenindeling') INTO k
35700
+ FROM public.logistics_rule_configs s WHERE s.is_active ORDER BY s.updated_at DESC LIMIT 1;
35701
+ v_opt := coalesce((k->>'optimal_fill')::boolean, true);
35702
+ v_kpl := nullif(k->>'max_customers_per_layer', '')::numeric::int;
35703
+ IF to_regclass('public.cart_planning_settings') IS NOT NULL THEN
35704
+ EXECUTE 'SELECT max_beladingen_per_layer, coalesce(max_one_customer_per_layer, false) FROM public.cart_planning_settings LIMIT 1'
35705
+ INTO v_bel, v_een;
35706
+ END IF;
35707
+ IF NOT v_opt THEN
35708
+ RETURN jsonb_build_object('menging', 'per_regel', 'klantenPerLaag', NULL, 'beladingenPerLaag', NULL, 'reden', 'optimale vulling staat uit');
35709
+ END IF;
35710
+ v_b := CASE WHEN coalesce(v_bel, 2) > 0 AND coalesce(v_bel, 2) < 999 THEN coalesce(v_bel, 2) END;
35711
+ v_kl := CASE WHEN coalesce(v_een, false) THEN 1 WHEN coalesce(v_kpl, 0) > 0 THEN v_kpl END;
35712
+ IF v_kl IS NOT NULL THEN v_reden := v_reden || format('maximaal %s klant(en) per laag', v_kl); END IF;
35713
+ IF v_b IS NOT NULL THEN v_reden := v_reden || format('maximaal %s beladingen per laag binnen een klant', v_b); END IF;
35714
+ RETURN jsonb_build_object('menging', 'vrij', 'klantenPerLaag', v_kl, 'beladingenPerLaag', v_b,
35715
+ 'reden', CASE WHEN cardinality(v_reden) = 0 THEN 'mengen per laag toegestaan' ELSE array_to_string(v_reden, '; ') END);
35716
+ END $$;
35717
+
35718
+ -- Lagen van een kar volgens de mengregels (zelfde als karLagen() in de planner).
35719
+ CREATE OR REPLACE FUNCTION public.logistics_cart_lagen_core(p_cart_id uuid)
35720
+ RETURNS numeric LANGUAGE plpgsql STABLE SET search_path TO 'public' AS $$
35721
+ DECLARE m jsonb := public.logistics_laag_menging_core(); v_n int; v_m int; units numeric[] := '{}'; r record; v_tot numeric := 0;
35722
+ v_ontbreekt boolean;
35723
+ BEGIN
35724
+ SELECT bool_or(coalesce(ci.crates_per_layer, 0) <= 0) INTO v_ontbreekt
35725
+ FROM logistics_active.order_cart_items ci LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
35726
+ WHERE ci.cart_id = p_cart_id AND oi.quarantined_at IS NULL;
35727
+ IF v_ontbreekt IS NULL OR v_ontbreekt THEN RETURN NULL; END IF;
35728
+ IF m->>'menging' = 'per_regel' THEN
35729
+ SELECT sum(ceil(ci.crates::numeric / ci.crates_per_layer - 1e-9)) INTO v_tot
35730
+ FROM logistics_active.order_cart_items ci LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
35731
+ WHERE ci.cart_id = p_cart_id AND oi.quarantined_at IS NULL;
35732
+ RETURN round(v_tot, 2);
35733
+ END IF;
35734
+ v_n := nullif(m->>'klantenPerLaag', '')::int;
35735
+ v_m := nullif(m->>'beladingenPerLaag', '')::int;
35736
+ IF v_n IS NULL AND v_m IS NULL THEN
35737
+ SELECT sum(ci.crates::numeric / ci.crates_per_layer) INTO v_tot
35738
+ FROM logistics_active.order_cart_items ci LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
35739
+ WHERE ci.cart_id = p_cart_id AND oi.quarantined_at IS NULL;
35740
+ RETURN round(v_tot, 2);
35741
+ END IF;
35742
+ IF v_m IS NOT NULL THEN
35743
+ FOR r IN
35744
+ SELECT sum(f) u FROM (
35745
+ SELECT x.f, x.klant, (row_number() OVER (PARTITION BY x.klant ORDER BY x.regel) - 1) / v_m grp FROM (
35746
+ SELECT ci.crates::numeric / ci.crates_per_layer f, coalesce(o.customer_id::text, oi.order_id::text, 'regel:' || ci.order_item_id::text) klant,
35747
+ ci.order_item_id::text regel
35748
+ FROM logistics_active.order_cart_items ci
35749
+ LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
35750
+ LEFT JOIN public.orders o ON o.id = oi.order_id
35751
+ WHERE ci.cart_id = p_cart_id AND oi.quarantined_at IS NULL) x) y
35752
+ GROUP BY klant, grp
35753
+ LOOP units := units || r.u; END LOOP;
35754
+ ELSE
35755
+ FOR r IN
35756
+ SELECT sum(f) u FROM (
35757
+ SELECT z.f, (dense_rank() OVER (ORDER BY z.klant) - 1) / v_n grp FROM (
35758
+ SELECT ci.crates::numeric / ci.crates_per_layer f, coalesce(o.customer_id::text, oi.order_id::text, 'regel:' || ci.order_item_id::text) klant
35759
+ FROM logistics_active.order_cart_items ci
35760
+ LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
35761
+ LEFT JOIN public.orders o ON o.id = oi.order_id
35762
+ WHERE ci.cart_id = p_cart_id AND oi.quarantined_at IS NULL) z) y
35763
+ GROUP BY grp
35764
+ LOOP units := units || r.u; END LOOP;
35765
+ END IF;
35766
+ IF cardinality(units) <= 1 THEN
35767
+ SELECT coalesce(sum(u), 0) INTO v_tot FROM unnest(units) u;
35768
+ ELSE
35769
+ SELECT sum(ceil(u - 1e-9)) INTO v_tot FROM unnest(units) u;
35770
+ END IF;
35771
+ RETURN round(v_tot, 2);
35772
+ END $$;
35773
+
35774
+ CREATE OR REPLACE FUNCTION public.logistics_cart_capaciteit_core(p_cart_id uuid)
35775
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path TO 'public' AS $$
35776
+ WITH c AS (SELECT oc.cart_number, oc.total_layers_used, oc.max_layers FROM logistics_active.order_carts oc WHERE oc.id = p_cart_id),
35777
+ i AS (SELECT max(nullif(ci.layers_per_cart, 0)) lpc
35778
+ FROM logistics_active.order_cart_items ci LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
35779
+ WHERE ci.cart_id = p_cart_id AND oi.quarantined_at IS NULL),
35780
+ v AS (SELECT c.cart_number,
35781
+ coalesce(public.logistics_cart_lagen_core(p_cart_id), c.total_layers_used::numeric) lagen,
35782
+ coalesce(i.lpc::numeric, nullif(c.max_layers, 0)::numeric) cap,
35783
+ CASE WHEN i.lpc IS NOT NULL THEN 'product' WHEN coalesce(c.max_layers, 0) > 0 THEN 'ladingdrager' END bron
35784
+ FROM c, i)
35785
+ SELECT jsonb_build_object('layersUsed', v.lagen, 'maxLayers', v.cap, 'capaciteitBron', v.bron,
35786
+ 'laagMenging', public.logistics_laag_menging_core(),
35787
+ 'vulgraadPct', CASE WHEN coalesce(v.cap, 0) > 0 AND v.lagen IS NOT NULL THEN round(100 * v.lagen / v.cap) END,
35788
+ 'layerUtilizationPct', CASE WHEN coalesce(v.cap, 0) > 0 AND v.lagen IS NOT NULL THEN round(100 * v.lagen / v.cap) END)
35789
+ || public.logistics_kar_blokkade(v.cart_number::text, v.lagen, v.cap)
35790
+ FROM v
35791
+ $$;
35792
+
35793
+ -- Quarantaine: bestaande functies additief bijwerken (alleen als de kolom bestaat; idempotent).
35794
+ DO $do$
35795
+ DECLARE f record; d text; n text;
35796
+ BEGIN
35797
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'order_items' AND column_name = 'quarantined_at') THEN
35798
+ RAISE NOTICE 'order_items.quarantined_at bestaat niet; quarantainefilter overgeslagen.';
35799
+ RETURN;
35800
+ END IF;
35801
+ FOR f IN SELECT * FROM (VALUES
35802
+ ('public.logistics_order_plan_coverage_v165(uuid)', 'JOIN public.order_items oi ON oi.id = oci.order_item_id', 'JOIN public.order_items oi ON oi.id = oci.order_item_id AND oi.quarantined_at IS NULL'),
35803
+ ('public.logistics_order_unit_carts_core(uuid[])', 'JOIN public.order_items oi ON oi.id = ci.order_item_id', 'JOIN public.order_items oi ON oi.id = ci.order_item_id AND oi.quarantined_at IS NULL'),
35804
+ ('public.logistics_order_te_hoge_karren(uuid[])', 'JOIN public.order_items oi ON oi.id = ci.order_item_id', 'JOIN public.order_items oi ON oi.id = ci.order_item_id AND oi.quarantined_at IS NULL'),
35805
+ ('public.logistics_order_indeling_verouderd_core(uuid)', 'WHERE oi.order_id = p_order_id AND oi.packing_locked_at IS NOT NULL', 'WHERE oi.order_id = p_order_id AND oi.packing_locked_at IS NOT NULL AND oi.quarantined_at IS NULL'),
35806
+ ('public.logistics_cart_slip_plan_v200(uuid)', 'WHERE ci.cart_id = d.id', 'WHERE ci.cart_id = d.id AND NOT EXISTS (SELECT 1 FROM public.order_items qi WHERE qi.id = ci.order_item_id AND qi.quarantined_at IS NOT NULL)')
35807
+ ) t(fn, oud, nieuw) LOOP
35808
+ IF to_regprocedure(f.fn) IS NULL THEN RAISE NOTICE '% ontbreekt', f.fn; CONTINUE; END IF;
35809
+ d := pg_get_functiondef(f.fn::regprocedure);
35810
+ IF position(f.nieuw IN d) > 0 THEN CONTINUE; END IF;
35811
+ IF position(f.oud IN d) = 0 THEN RAISE NOTICE 'patroon niet gevonden in %', f.fn; CONTINUE; END IF;
35812
+ n := replace(d, f.oud, f.nieuw);
35813
+ EXECUTE n;
35814
+ END LOOP;
35815
+ END $do$;
35816
+
35817
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_lagen_core(uuid) TO authenticated, service_role;
35818
+
35819
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_205()
35820
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35821
+ SELECT 'mengregel groepen', position('logistics_cart_lagen_core' IN pg_get_functiondef('public.logistics_cart_capaciteit_core(uuid)'::regprocedure)) > 0, 'Ceil alleen tussen groepen die geen laag mogen delen'
35822
+ UNION ALL SELECT 'quarantaine dekking', position('quarantined_at IS NULL' IN pg_get_functiondef('public.logistics_order_plan_coverage_v165(uuid)'::regprocedure)) > 0, 'Dekking slaat regels in quarantaine over'
35823
+ UNION ALL SELECT 'quarantaine pakbonplan', position('quarantined_at IS NOT NULL' IN pg_get_functiondef('public.logistics_cart_slip_plan_v200(uuid)'::regprocedure)) > 0, 'Pakbonplan toont geen regels in quarantaine'
35824
+ UNION ALL SELECT 'quarantaine kar-nummering', position('quarantined_at IS NULL' IN pg_get_functiondef('public.logistics_order_unit_carts_core(uuid[])'::regprocedure)) > 0, 'Karren van een order via actieve regels'
35825
+ UNION ALL SELECT 'quarantaine te hoog', position('quarantined_at IS NULL' IN pg_get_functiondef('public.logistics_order_te_hoge_karren(uuid[])'::regprocedure)) > 0, 'Te hoog alleen op actieve regels'
35826
+ $$;
35827
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_205() TO authenticated, service_role;
35828
+ -- ============================================================================
35829
+ -- 1.0.207 productoordeel belading zonder tijdelijke tabel (0A000-herstel)
35830
+ -- ============================================================================
35831
+ CREATE OR REPLACE FUNCTION public.logistics_product_oordeel_belading(p_regels jsonb)
35832
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35833
+ DECLARE e jsonb; gj jsonb; gmap jsonb := '{}'::jsonb; c record; v_code text; v_eigen text; ids uuid[]; uit jsonb := '[]'::jsonb;
35834
+ ls int; fh numeric; ph numeric; cpl numeric; lpc numeric; ppc numeric; laag numeric; naam text; v_prod text;
35835
+ blok text[]; waar text[]; mist text[]; hmist text[]; v_pid uuid; v_oordeel text; v_tekst text; v_heeft_kol boolean;
35836
+ BEGIN
35837
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_product_oordeel_belading');
35838
+ IF p_regels IS NULL OR jsonb_typeof(p_regels) <> 'array' THEN RAISE EXCEPTION 'p_regels moet een lijst zijn.' USING ERRCODE = '22023'; END IF;
35839
+ IF jsonb_array_length(p_regels) > 1000 THEN
35840
+ RAISE EXCEPTION 'Maximaal 1000 regels per aanroep (ontvangen: %).', jsonb_array_length(p_regels) USING ERRCODE = '22023';
35841
+ END IF;
35842
+ SELECT array_agg(DISTINCT (x->>'product_id')::uuid) INTO ids FROM jsonb_array_elements(p_regels) x
35843
+ WHERE nullif(btrim(x->>'product_id'), '') ~* '^[0-9a-f-]{36}$';
35844
+ IF ids IS NOT NULL THEN
35845
+ SELECT coalesce(jsonb_object_agg(g.product_id::text, to_jsonb(g)), '{}'::jsonb) INTO gmap
35846
+ FROM public.logistics_data_gereedheid(ids) g;
35847
+ END IF;
35848
+ -- Eigen ladingdrager van het product, alleen als die kolom in deze Shell bestaat; geen standaard.
35849
+ SELECT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass('public.products') AND attname = 'load_carrier_type' AND NOT attisdropped) INTO v_heeft_kol;
35850
+
35851
+ FOR e IN SELECT x FROM jsonb_array_elements(p_regels) x LOOP
35852
+ blok := '{}'; waar := '{}'; mist := '{}'; hmist := '{}'; c := NULL; v_eigen := NULL;
35853
+ v_pid := CASE WHEN nullif(btrim(e->>'product_id'), '') ~* '^[0-9a-f-]{36}$' THEN (e->>'product_id')::uuid END;
35854
+ gj := CASE WHEN v_pid IS NULL THEN NULL ELSE gmap->(v_pid::text) END;
35855
+ IF gj IS NULL THEN
35856
+ uit := uit || jsonb_build_array(jsonb_build_object('product_id', v_pid, 'oordeel', 'mist_gegevens', 'tekst', 'product onbekend',
35857
+ 'regels', jsonb_build_array('Product onbekend.')));
35858
+ CONTINUE;
35859
+ END IF;
35860
+ v_prod := coalesce(gj->>'product', 'Product');
35861
+ v_code := nullif(btrim(e->>'carrier_code'), '');
35862
+ IF v_code IS NULL AND v_heeft_kol THEN
35863
+ EXECUTE 'SELECT nullif(btrim(load_carrier_type::text), '''') FROM public.products WHERE id = $1' INTO v_eigen USING v_pid;
35864
+ v_code := v_eigen;
35865
+ END IF;
35866
+ IF v_code IS NOT NULL THEN
35867
+ 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
35868
+ FROM public.load_carrier_types t WHERE t.is_active AND t.code = v_code LIMIT 1;
35869
+ END IF;
35870
+ naam := coalesce(c.name, c.code, v_code, 'de ladingdrager');
35871
+ ppc := nullif(e->>'plants_per_crate', '')::numeric;
35872
+ cpl := nullif(e->>'crates_per_layer', '')::numeric;
35873
+ lpc := nullif(e->>'layers_per_cart', '')::numeric;
35874
+ IF coalesce(ppc, 0) <= 0 THEN mist := mist || 'planten per fust'::text; END IF;
35875
+ IF coalesce(cpl, 0) <= 0 THEN mist := mist || 'fusten per laag'::text; END IF;
35876
+ IF coalesce(lpc, 0) <= 0 THEN mist := mist || 'lagen per kar'::text; END IF;
35877
+ IF v_code IS NULL THEN
35878
+ mist := mist || 'ladingdrager'::text;
35879
+ ELSIF c.code IS NULL THEN
35880
+ blok := blok || format('ladingdrager %s staat niet in Beheer', v_code);
35881
+ ELSE
35882
+ fh := nullif(gj->'velden'->'fust_height_mm'->>'waarde', '')::numeric / 10.0;
35883
+ ph := nullif(gj->'velden'->'plant_height_cm'->>'waarde', '')::numeric;
35884
+ ls := public.logistics_max_fusten_per_laag(c.layer_length_cm, c.layer_width_cm,
35885
+ nullif(gj->'velden'->'fust_length_mm'->>'waarde', '')::numeric / 10.0, nullif(gj->'velden'->'fust_width_mm'->>'waarde', '')::numeric / 10.0);
35886
+ IF coalesce(lpc,0) > 0 AND coalesce(fh,0) > 0 AND coalesce(ph,0) > 0 AND coalesce(c.max_height_cm,0) > 0 THEN
35887
+ laag := greatest(fh, ph);
35888
+ IF lpc * laag > c.max_height_cm + 1e-9 THEN
35889
+ blok := blok || format('%s lagen × %s cm = %s cm, terwijl %s %s cm hoog is; er passen maximaal %s lagen',
35890
+ 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));
35891
+ END IF;
35892
+ ELSE
35893
+ IF coalesce(fh,0) <= 0 THEN hmist := hmist || 'fusthoogte'::text; END IF;
35894
+ IF coalesce(ph,0) <= 0 THEN hmist := hmist || 'planthoogte'::text; END IF;
35895
+ IF coalesce(c.max_height_cm,0) <= 0 THEN hmist := hmist || ('hoogte van ' || naam); END IF;
35896
+ END IF;
35897
+ IF ls IS NOT NULL AND coalesce(cpl,0) > ls THEN
35898
+ blok := blok || format('%s fust per laag, maar op een laag van %s passen er maximaal %s', cpl, naam, ls);
35899
+ END IF;
35900
+ IF coalesce(c.max_crates_per_layer,0) > 0 AND coalesce(cpl,0) > c.max_crates_per_layer THEN
35901
+ blok := blok || format('%s fust per laag, maar %s staat maximaal %s fust per laag toe', cpl, naam, c.max_crates_per_layer);
35902
+ END IF;
35903
+ IF cardinality(blok) = 0 AND cardinality(hmist) > 0 AND coalesce(c.layers_per_carrier,0) > 0 AND coalesce(lpc,0) > c.layers_per_carrier THEN
35904
+ waar := waar || format('%s lagen opgegeven, standaard %s; niet te controleren omdat de %s ontbreekt', lpc, c.layers_per_carrier, array_to_string(hmist, ', '));
35905
+ END IF;
35906
+ END IF;
35907
+ IF coalesce(gj->'ontbrekend', '[]'::jsonb) ? 'weight' THEN waar := waar || 'gewicht per fust ontbreekt'::text; END IF;
35908
+ IF cardinality(blok) > 0 THEN
35909
+ v_oordeel := 'onmogelijk'; v_tekst := format('%s past niet op %s.', v_prod, naam);
35910
+ ELSIF cardinality(mist) > 0 THEN
35911
+ v_oordeel := 'mist_gegevens'; v_tekst := format('%s: %s ontbreekt.', v_prod, array_to_string(mist, ', '));
35912
+ ELSIF cardinality(waar) > 0 THEN
35913
+ v_oordeel := 'waarschuwing'; v_tekst := format('%s: belading mogelijk, met waarschuwing.', v_prod);
35914
+ ELSE
35915
+ v_oordeel := 'compleet'; v_tekst := format('%s: belading past.', v_prod);
35916
+ END IF;
35917
+ uit := uit || jsonb_build_array(jsonb_build_object('product_id', v_pid, 'oordeel', v_oordeel, 'tekst', v_tekst,
35918
+ 'regels', to_jsonb(ARRAY(SELECT upper(left(z,1)) || substr(z,2) || '.' FROM unnest(blok || mist || waar) z))));
35919
+ END LOOP;
35920
+ RETURN uit;
35921
+ END $$;
35922
+ GRANT EXECUTE ON FUNCTION public.logistics_product_oordeel_belading(jsonb) TO authenticated, service_role;
35923
+
35924
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_207()
35925
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35926
+ DECLARE v_ok boolean := true; v_fout text := 'aanroep zonder fout'; v_cnt int;
35927
+ BEGIN
35928
+ SELECT count(*) INTO v_cnt FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
35929
+ WHERE n.nspname = 'public' AND p.proname LIKE 'logistics%' AND p.provolatile <> 'v' AND p.prosrc ~* 'create\s+temp';
35930
+ punt := 'geen tijdelijke tabel in niet-volatiele functie'; ok := v_cnt = 0; uitleg := format('%s functie(s) met CREATE TEMP', v_cnt); RETURN NEXT;
35931
+ BEGIN
35932
+ PERFORM public.logistics_product_oordeel_belading('[{"product_id":""}]'::jsonb);
35933
+ EXCEPTION WHEN SQLSTATE '0A000' THEN v_ok := false; v_fout := SQLERRM;
35934
+ WHEN OTHERS THEN v_fout := 'aanroep geweigerd door toegangscontrole of andere fout: ' || SQLERRM;
35935
+ END;
35936
+ punt := 'productoordeel belading aanroepbaar'; ok := v_ok; uitleg := v_fout; RETURN NEXT;
35937
+ END $$;
35938
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_207() TO authenticated, service_role;
35939
+ CREATE OR REPLACE FUNCTION public.logistics_product_oordeel_belading(p_regels jsonb)
35940
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
35941
+ DECLARE e jsonb; gj jsonb; gmap jsonb := '{}'::jsonb; c record; v_code text; v_eigen text; ids uuid[]; uit jsonb := '[]'::jsonb;
35942
+ ls int; fh numeric; ph numeric; cpl numeric; lpc numeric; ppc numeric; laag numeric; naam text; v_prod text;
35943
+ blok text[]; waar text[]; mist text[]; hmist text[]; v_pid uuid; v_oordeel text; v_tekst text; v_heeft_kol boolean;
35944
+ BEGIN
35945
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_product_oordeel_belading');
35946
+ IF p_regels IS NULL OR jsonb_typeof(p_regels) <> 'array' THEN RAISE EXCEPTION 'p_regels moet een lijst zijn.' USING ERRCODE = '22023'; END IF;
35947
+ IF jsonb_array_length(p_regels) > 1000 THEN
35948
+ RAISE EXCEPTION 'Maximaal 1000 regels per aanroep (ontvangen: %).', jsonb_array_length(p_regels) USING ERRCODE = '22023';
35949
+ END IF;
35950
+ SELECT array_agg(DISTINCT (x->>'product_id')::uuid) INTO ids FROM jsonb_array_elements(p_regels) x
35951
+ WHERE nullif(btrim(x->>'product_id'), '') ~* '^[0-9a-f-]{36}$';
35952
+ IF ids IS NOT NULL THEN
35953
+ SELECT coalesce(jsonb_object_agg(g.product_id::text, to_jsonb(g)), '{}'::jsonb) INTO gmap
35954
+ FROM public.logistics_data_gereedheid(ids) g;
35955
+ END IF;
35956
+ SELECT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass('public.products') AND attname = 'load_carrier_type' AND NOT attisdropped) INTO v_heeft_kol;
35957
+
35958
+ FOR e IN SELECT x FROM jsonb_array_elements(p_regels) x LOOP
35959
+ blok := '{}'; waar := '{}'; mist := '{}'; hmist := '{}'; v_eigen := NULL;
35960
+ v_pid := CASE WHEN nullif(btrim(e->>'product_id'), '') ~* '^[0-9a-f-]{36}$' THEN (e->>'product_id')::uuid END;
35961
+ gj := CASE WHEN v_pid IS NULL THEN NULL ELSE gmap->(v_pid::text) END;
35962
+ IF gj IS NULL THEN
35963
+ uit := uit || jsonb_build_array(jsonb_build_object('product_id', v_pid, 'oordeel', 'mist_gegevens', 'tekst', 'product onbekend',
35964
+ 'regels', jsonb_build_array('Product onbekend.')));
35965
+ CONTINUE;
35966
+ END IF;
35967
+ v_prod := coalesce(gj->>'product', 'Product');
35968
+ v_code := nullif(btrim(e->>'carrier_code'), '');
35969
+ IF v_code IS NULL AND v_heeft_kol THEN
35970
+ EXECUTE 'SELECT nullif(btrim(load_carrier_type::text), '''') FROM public.products WHERE id = $1' INTO v_eigen USING v_pid;
35971
+ v_code := v_eigen;
35972
+ END IF;
35973
+ 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
35974
+ FROM public.load_carrier_types t WHERE t.is_active AND t.code = v_code LIMIT 1;
35975
+ naam := coalesce(c.name, c.code, v_code, 'de ladingdrager');
35976
+ ppc := nullif(e->>'plants_per_crate', '')::numeric;
35977
+ cpl := nullif(e->>'crates_per_layer', '')::numeric;
35978
+ lpc := nullif(e->>'layers_per_cart', '')::numeric;
35979
+ IF coalesce(ppc, 0) <= 0 THEN mist := mist || 'planten per fust'::text; END IF;
35980
+ IF coalesce(cpl, 0) <= 0 THEN mist := mist || 'fusten per laag'::text; END IF;
35981
+ IF coalesce(lpc, 0) <= 0 THEN mist := mist || 'lagen per kar'::text; END IF;
35982
+ IF v_code IS NULL THEN
35983
+ mist := mist || 'ladingdrager'::text;
35984
+ ELSIF c.code IS NULL THEN
35985
+ blok := blok || format('ladingdrager %s staat niet in Beheer', v_code);
35986
+ ELSE
35987
+ fh := nullif(gj->'velden'->'fust_height_mm'->>'waarde', '')::numeric / 10.0;
35988
+ ph := nullif(gj->'velden'->'plant_height_cm'->>'waarde', '')::numeric;
35989
+ ls := public.logistics_max_fusten_per_laag(c.layer_length_cm, c.layer_width_cm,
35990
+ nullif(gj->'velden'->'fust_length_mm'->>'waarde', '')::numeric / 10.0, nullif(gj->'velden'->'fust_width_mm'->>'waarde', '')::numeric / 10.0);
35991
+ IF coalesce(lpc,0) > 0 AND coalesce(fh,0) > 0 AND coalesce(ph,0) > 0 AND coalesce(c.max_height_cm,0) > 0 THEN
35992
+ laag := greatest(fh, ph);
35993
+ IF lpc * laag > c.max_height_cm + 1e-9 THEN
35994
+ blok := blok || format('%s lagen × %s cm = %s cm, terwijl %s %s cm hoog is; er passen maximaal %s lagen',
35995
+ 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));
35996
+ END IF;
35997
+ ELSE
35998
+ IF coalesce(fh,0) <= 0 THEN hmist := hmist || 'fusthoogte'::text; END IF;
35999
+ IF coalesce(ph,0) <= 0 THEN hmist := hmist || 'planthoogte'::text; END IF;
36000
+ IF coalesce(c.max_height_cm,0) <= 0 THEN hmist := hmist || ('hoogte van ' || naam); END IF;
36001
+ END IF;
36002
+ IF ls IS NOT NULL AND coalesce(cpl,0) > ls THEN
36003
+ blok := blok || format('%s fust per laag, maar op een laag van %s passen er maximaal %s', cpl, naam, ls);
36004
+ END IF;
36005
+ IF coalesce(c.max_crates_per_layer,0) > 0 AND coalesce(cpl,0) > c.max_crates_per_layer THEN
36006
+ blok := blok || format('%s fust per laag, maar %s staat maximaal %s fust per laag toe', cpl, naam, c.max_crates_per_layer);
36007
+ END IF;
36008
+ IF cardinality(blok) = 0 AND cardinality(hmist) > 0 AND coalesce(c.layers_per_carrier,0) > 0 AND coalesce(lpc,0) > c.layers_per_carrier THEN
36009
+ waar := waar || format('%s lagen opgegeven, standaard %s; niet te controleren omdat de %s ontbreekt', lpc, c.layers_per_carrier, array_to_string(hmist, ', '));
36010
+ END IF;
36011
+ END IF;
36012
+ IF coalesce(gj->'ontbrekend', '[]'::jsonb) ? 'weight' THEN waar := waar || 'gewicht per fust ontbreekt'::text; END IF;
36013
+ IF cardinality(blok) > 0 THEN
36014
+ v_oordeel := 'onmogelijk'; v_tekst := format('%s past niet op %s.', v_prod, naam);
36015
+ ELSIF cardinality(mist) > 0 THEN
36016
+ v_oordeel := 'mist_gegevens'; v_tekst := format('%s: %s ontbreekt.', v_prod, array_to_string(mist, ', '));
36017
+ ELSIF cardinality(waar) > 0 THEN
36018
+ v_oordeel := 'waarschuwing'; v_tekst := format('%s: belading mogelijk, met waarschuwing.', v_prod);
36019
+ ELSE
36020
+ v_oordeel := 'compleet'; v_tekst := format('%s: belading past.', v_prod);
36021
+ END IF;
36022
+ uit := uit || jsonb_build_array(jsonb_build_object('product_id', v_pid, 'oordeel', v_oordeel, 'tekst', v_tekst,
36023
+ 'regels', to_jsonb(ARRAY(SELECT upper(left(z,1)) || substr(z,2) || '.' FROM unnest(blok || mist || waar) z))));
36024
+ END LOOP;
36025
+ RETURN uit;
36026
+ END $$;
36027
+ GRANT EXECUTE ON FUNCTION public.logistics_product_oordeel_belading(jsonb) TO authenticated, service_role;
36028
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_207()
36029
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
36030
+ DECLARE v_ok boolean := true; v_fout text := 'aanroep zonder fout'; v_cnt int; v_namen text;
36031
+ BEGIN
36032
+ SELECT count(*), string_agg(p.proname, ', ') INTO v_cnt, v_namen FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
36033
+ WHERE n.nspname = 'public' AND p.proname LIKE 'logistics%' AND p.proname NOT LIKE 'logistics_install_check%'
36034
+ AND p.provolatile <> 'v' AND p.prosrc ~* 'create\s+temp';
36035
+ punt := 'geen tijdelijke tabel in niet-volatiele functie'; ok := v_cnt = 0;
36036
+ uitleg := CASE WHEN v_cnt = 0 THEN 'geen' ELSE v_namen END; RETURN NEXT;
36037
+ BEGIN
36038
+ PERFORM public.logistics_product_oordeel_belading('[{"product_id":""}]'::jsonb);
36039
+ EXCEPTION WHEN SQLSTATE '0A000' THEN v_ok := false; v_fout := SQLERRM;
36040
+ WHEN OTHERS THEN v_fout := 'aanroep geweigerd door toegangscontrole of andere fout: ' || SQLERRM;
36041
+ END;
36042
+ punt := 'productoordeel belading aanroepbaar'; ok := v_ok; uitleg := v_fout; RETURN NEXT;
36043
+ END $$;
36044
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_207() TO authenticated, service_role;
36045
+
36046
+ -- =====================================================================
36047
+ -- 1.0.208 — Floriday-vulgraad per kar, karTekst, ladingdrager uit hoofdverpakking,
36048
+ -- oordeel fysiek vóór mist_gegevens, snellere pickwachtrij, verouderde karren (quarantaine).
36049
+ -- Additief en idempotent; geen REVOKE/DROP.
36050
+ -- =====================================================================
36051
+
36052
+ DO $do$
36053
+ BEGIN
36054
+ IF to_regclass('public.logistics_floriday_carrier_map') IS NULL THEN RETURN; END IF;
36055
+ IF NOT EXISTS (
36056
+ SELECT 1 FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
36057
+ WHERE i.indrelid = 'public.logistics_floriday_carrier_map'::regclass AND i.indisunique
36058
+ AND i.indnatts = 1 AND a.attname = 'floriday_code') THEN
36059
+ BEGIN
36060
+ CREATE UNIQUE INDEX IF NOT EXISTS logistics_floriday_carrier_map_code_uq ON public.logistics_floriday_carrier_map (floriday_code);
36061
+ EXCEPTION WHEN unique_violation THEN
36062
+ RAISE NOTICE 'logistiek-engine: dubbele Floriday-codes in de vertaaltabel; install_check meldt welke.';
36063
+ END;
36064
+ END IF;
36065
+ END $do$;
36066
+
36067
+ DO $do$
36068
+ BEGIN
36069
+ IF to_regclass('public.logistics_floriday_carrier_map') IS NULL THEN RETURN; END IF;
36070
+ INSERT INTO public.logistics_floriday_carrier_map (floriday_code, carrier_code)
36071
+ SELECT v.f, v.c FROM (VALUES
36072
+ ('DANISH_TROLLEY','danish_container'), ('DANISH_TROLLEY_NON_CC_RFID','danish_container'),
36073
+ ('DANISH_TROLLEY_WATER_TRAY','danish_container'), ('ONE_THIRD_DANISH_TROLLEY','danish_container'),
36074
+ ('AUCTION_TROLLEY','trolley'),
36075
+ ('EURO_TROLLEY','euro_rolcontainer'), ('WATERING_CONTAINER','euro_rolcontainer'),
36076
+ ('EURO_PALLET','euro_pallet'), ('HT_EURO_PALLET','euro_pallet'),
36077
+ ('PALLET','pallet'), ('BLOCK_PALLET','pallet'), ('IFCO_PALLET','pallet'),
36078
+ ('CHEP_PALLET_BLUE','pallet'), ('CHEP_PALLET_RED','pallet'),
36079
+ ('NONE','loose')) v(f, c)
36080
+ WHERE NOT EXISTS (SELECT 1 FROM public.logistics_floriday_carrier_map m WHERE upper(m.floriday_code) = v.f);
36081
+ END $do$;
36082
+
36083
+ CREATE OR REPLACE FUNCTION public.logistics_ladingdrager_resolve(p_code text)
36084
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
36085
+ DECLARE v text := nullif(btrim(p_code), ''); n int; m text;
36086
+ BEGIN
36087
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_ladingdrager_resolve');
36088
+ IF v IS NULL OR upper(v) = 'NONE' THEN
36089
+ RETURN jsonb_build_object('code', NULL, 'invoer', v, 'bron', NULL, 'fout', 'ladingdrager ontbreekt');
36090
+ END IF;
36091
+ IF EXISTS (SELECT 1 FROM public.load_carrier_types t WHERE t.is_active AND t.code = v) THEN
36092
+ RETURN jsonb_build_object('code', v, 'invoer', v, 'bron', 'beheer', 'fout', NULL);
36093
+ END IF;
36094
+ IF to_regclass('public.logistics_floriday_carrier_map') IS NULL THEN
36095
+ RETURN jsonb_build_object('code', NULL, 'invoer', v, 'bron', NULL, 'fout', format('ladingdrager %s onbekend in Beheer', v));
36096
+ END IF;
36097
+ SELECT count(DISTINCT c.carrier_code), min(c.carrier_code) INTO n, m
36098
+ FROM public.logistics_floriday_carrier_map c WHERE upper(c.floriday_code) = upper(v);
36099
+ IF coalesce(n, 0) = 0 THEN
36100
+ RETURN jsonb_build_object('code', NULL, 'invoer', v, 'bron', NULL, 'fout', format('ladingdrager %s onbekend in Beheer', v));
36101
+ ELSIF n > 1 THEN
36102
+ RETURN jsonb_build_object('code', NULL, 'invoer', v, 'bron', 'floriday', 'fout', format('ladingdrager niet eenduidig (%s)', v));
36103
+ ELSIF m = 'loose' THEN
36104
+ RETURN jsonb_build_object('code', NULL, 'invoer', v, 'bron', 'floriday', 'fout', 'ladingdrager ontbreekt');
36105
+ ELSIF NOT EXISTS (SELECT 1 FROM public.load_carrier_types t WHERE t.is_active AND t.code = m) THEN
36106
+ RETURN jsonb_build_object('code', NULL, 'invoer', v, 'bron', 'floriday', 'fout', format('ladingdrager %s onbekend in Beheer', v));
36107
+ END IF;
36108
+ RETURN jsonb_build_object('code', m, 'invoer', v, 'bron', 'floriday', 'fout', NULL);
36109
+ END $$;
36110
+ GRANT EXECUTE ON FUNCTION public.logistics_ladingdrager_resolve(text) TO authenticated, service_role;
36111
+
36112
+ CREATE OR REPLACE FUNCTION public.logistics_product_hoofdverpakking(p_product_id uuid)
36113
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
36114
+ DECLARE v_lc text; v_found boolean := false; v_heeft_lc boolean;
36115
+ BEGIN
36116
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_product_hoofdverpakking');
36117
+ IF to_regclass('public.product_packings') IS NULL THEN RETURN NULL; END IF;
36118
+ SELECT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.product_packings'::regclass AND attname = 'load_carrier' AND NOT attisdropped) INTO v_heeft_lc;
36119
+ IF v_heeft_lc THEN
36120
+ EXECUTE 'SELECT true, nullif(btrim(load_carrier::text), '''') FROM public.product_packings WHERE product_id = $1 AND is_primary
36121
+ ORDER BY sort_order NULLS LAST, created_at LIMIT 1' INTO v_found, v_lc USING p_product_id;
36122
+ ELSE
36123
+ EXECUTE 'SELECT true FROM public.product_packings WHERE product_id = $1 AND is_primary LIMIT 1' INTO v_found USING p_product_id;
36124
+ END IF;
36125
+ RETURN jsonb_build_object('aanwezig', coalesce(v_found, false), 'ladingdrager', v_lc);
36126
+ END $$;
36127
+ GRANT EXECUTE ON FUNCTION public.logistics_product_hoofdverpakking(uuid) TO authenticated, service_role;
36128
+
36129
+ CREATE OR REPLACE FUNCTION public.logistics_max_fusten_per_laag_alle(p_fust_l numeric, p_fust_b numeric)
36130
+ RETURNS integer LANGUAGE sql STABLE SET search_path TO 'public' AS $$
36131
+ SELECT max(public.logistics_max_fusten_per_laag(t.layer_length_cm, t.layer_width_cm, p_fust_l, p_fust_b))
36132
+ FROM public.load_carrier_types t
36133
+ WHERE t.is_active AND coalesce(t.layer_length_cm, 0) > 0 AND coalesce(t.layer_width_cm, 0) > 0
36134
+ $$;
36135
+ GRANT EXECUTE ON FUNCTION public.logistics_max_fusten_per_laag_alle(numeric, numeric) TO authenticated, service_role;
36136
+
36137
+ CREATE OR REPLACE FUNCTION public.logistics_product_oordeel_belading(p_regels jsonb)
36138
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
36139
+ DECLARE e jsonb; gj jsonb; gmap jsonb := '{}'::jsonb; c record; rc jsonb; hv jsonb; v_code text; v_eigen text; ids uuid[]; uit jsonb := '[]'::jsonb;
36140
+ ls int; fh numeric; ph numeric; cpl numeric; lpc numeric; ppc numeric; laag numeric; naam text; v_prod text; fl numeric; fb numeric;
36141
+ blok text[]; waar text[]; mist text[]; hmist text[]; v_pid uuid; v_oordeel text; v_tekst text; v_heeft_kol boolean; v_cfout text;
36142
+ BEGIN
36143
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_product_oordeel_belading');
36144
+ IF p_regels IS NULL OR jsonb_typeof(p_regels) <> 'array' THEN RAISE EXCEPTION 'p_regels moet een lijst zijn.' USING ERRCODE = '22023'; END IF;
36145
+ IF jsonb_array_length(p_regels) > 1000 THEN
36146
+ RAISE EXCEPTION 'Maximaal 1000 regels per aanroep (ontvangen: %).', jsonb_array_length(p_regels) USING ERRCODE = '22023';
36147
+ END IF;
36148
+ SELECT array_agg(DISTINCT (x->>'product_id')::uuid) INTO ids FROM jsonb_array_elements(p_regels) x
36149
+ WHERE nullif(btrim(x->>'product_id'), '') ~* '^[0-9a-f-]{36}$';
36150
+ IF ids IS NOT NULL THEN
36151
+ SELECT coalesce(jsonb_object_agg(g.product_id::text, to_jsonb(g)), '{}'::jsonb) INTO gmap
36152
+ FROM public.logistics_data_gereedheid(ids) g;
36153
+ END IF;
36154
+ SELECT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass('public.products') AND attname = 'load_carrier_type' AND NOT attisdropped) INTO v_heeft_kol;
36155
+
36156
+ FOR e IN SELECT x FROM jsonb_array_elements(p_regels) x LOOP
36157
+ blok := '{}'; waar := '{}'; mist := '{}'; hmist := '{}'; v_eigen := NULL; c := NULL; v_cfout := NULL;
36158
+ v_pid := CASE WHEN nullif(btrim(e->>'product_id'), '') ~* '^[0-9a-f-]{36}$' THEN (e->>'product_id')::uuid END;
36159
+ gj := CASE WHEN v_pid IS NULL THEN NULL ELSE gmap->(v_pid::text) END;
36160
+ IF gj IS NULL THEN
36161
+ uit := uit || jsonb_build_array(jsonb_build_object('product_id', v_pid, 'oordeel', 'mist_gegevens', 'tekst', 'product onbekend',
36162
+ 'regels', jsonb_build_array('Product onbekend.')));
36163
+ CONTINUE;
36164
+ END IF;
36165
+ v_prod := coalesce(gj->>'product', 'Product');
36166
+ hv := public.logistics_product_hoofdverpakking(v_pid);
36167
+ v_code := nullif(btrim(e->>'carrier_code'), '');
36168
+ IF v_code IS NULL THEN v_code := nullif(hv->>'ladingdrager', ''); END IF;
36169
+ IF v_code IS NULL AND v_heeft_kol THEN
36170
+ EXECUTE 'SELECT nullif(btrim(load_carrier_type::text), '''') FROM public.products WHERE id = $1' INTO v_eigen USING v_pid;
36171
+ v_code := v_eigen;
36172
+ END IF;
36173
+ rc := public.logistics_ladingdrager_resolve(v_code);
36174
+ v_cfout := rc->>'fout';
36175
+ IF rc->>'code' IS NOT NULL THEN
36176
+ 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
36177
+ FROM public.load_carrier_types t WHERE t.is_active AND t.code = rc->>'code' LIMIT 1;
36178
+ END IF;
36179
+ naam := coalesce(c.name, c.code, 'de ladingdrager');
36180
+ ppc := nullif(e->>'plants_per_crate', '')::numeric;
36181
+ cpl := nullif(e->>'crates_per_layer', '')::numeric;
36182
+ lpc := nullif(e->>'layers_per_cart', '')::numeric;
36183
+ fh := nullif(gj->'velden'->'fust_height_mm'->>'waarde', '')::numeric / 10.0;
36184
+ ph := nullif(gj->'velden'->'plant_height_cm'->>'waarde', '')::numeric;
36185
+ fl := nullif(gj->'velden'->'fust_length_mm'->>'waarde', '')::numeric / 10.0;
36186
+ fb := nullif(gj->'velden'->'fust_width_mm'->>'waarde', '')::numeric / 10.0;
36187
+ IF hv IS NOT NULL AND NOT coalesce((hv->>'aanwezig')::boolean, false) THEN mist := mist || 'belading niet ingesteld'::text; END IF;
36188
+ IF coalesce(ppc, 0) <= 0 THEN mist := mist || 'planten per fust'::text; END IF;
36189
+ IF coalesce(cpl, 0) <= 0 THEN mist := mist || 'fusten per laag'::text; END IF;
36190
+ IF coalesce(lpc, 0) <= 0 THEN mist := mist || 'lagen per kar'::text; END IF;
36191
+
36192
+ IF c.code IS NULL THEN
36193
+ mist := mist || coalesce(v_cfout, 'ladingdrager ontbreekt');
36194
+ ls := public.logistics_max_fusten_per_laag_alle(fl, fb);
36195
+ IF ls IS NOT NULL AND coalesce(cpl,0) > ls THEN
36196
+ blok := blok || format('%s fust per laag, maar op geen enkele ladingdrager in Beheer passen er meer dan %s', cpl, ls);
36197
+ END IF;
36198
+ ELSE
36199
+ ls := public.logistics_max_fusten_per_laag(c.layer_length_cm, c.layer_width_cm, fl, fb);
36200
+ IF coalesce(lpc,0) > 0 AND coalesce(fh,0) > 0 AND coalesce(ph,0) > 0 AND coalesce(c.max_height_cm,0) > 0 THEN
36201
+ laag := greatest(fh, ph);
36202
+ IF lpc * laag > c.max_height_cm + 1e-9 THEN
36203
+ blok := blok || format('%s lagen × %s cm = %s cm, terwijl %s %s cm hoog is; er passen maximaal %s lagen',
36204
+ 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));
36205
+ END IF;
36206
+ ELSE
36207
+ IF coalesce(fh,0) <= 0 THEN hmist := hmist || 'fusthoogte'::text; END IF;
36208
+ IF coalesce(ph,0) <= 0 THEN hmist := hmist || 'planthoogte'::text; END IF;
36209
+ IF coalesce(c.max_height_cm,0) <= 0 THEN hmist := hmist || ('hoogte van ' || naam); END IF;
36210
+ END IF;
36211
+ IF ls IS NOT NULL AND coalesce(cpl,0) > ls THEN
36212
+ blok := blok || format('%s fust per laag, maar op een laag van %s passen er maximaal %s', cpl, naam, ls);
36213
+ END IF;
36214
+ IF coalesce(c.max_crates_per_layer,0) > 0 AND coalesce(cpl,0) > c.max_crates_per_layer THEN
36215
+ blok := blok || format('%s fust per laag, maar %s staat maximaal %s fust per laag toe', cpl, naam, c.max_crates_per_layer);
36216
+ END IF;
36217
+ IF cardinality(blok) = 0 AND cardinality(hmist) > 0 AND coalesce(c.layers_per_carrier,0) > 0 AND coalesce(lpc,0) > c.layers_per_carrier THEN
36218
+ waar := waar || format('%s lagen opgegeven, standaard %s; niet te controleren omdat de %s ontbreekt', lpc, c.layers_per_carrier, array_to_string(hmist, ', '));
36219
+ END IF;
36220
+ END IF;
36221
+ IF coalesce(gj->'ontbrekend', '[]'::jsonb) ? 'weight' THEN waar := waar || 'gewicht per fust ontbreekt'::text; END IF;
36222
+ IF cardinality(blok) > 0 THEN
36223
+ v_oordeel := 'onmogelijk'; v_tekst := format('%s past niet op %s.', v_prod, CASE WHEN c.code IS NULL THEN 'een ladingdrager' ELSE naam END);
36224
+ ELSIF cardinality(mist) > 0 THEN
36225
+ v_oordeel := 'mist_gegevens'; v_tekst := format('%s: %s.', v_prod, array_to_string(mist, ', '));
36226
+ ELSIF cardinality(waar) > 0 THEN
36227
+ v_oordeel := 'waarschuwing'; v_tekst := format('%s: belading mogelijk, met waarschuwing.', v_prod);
36228
+ ELSE
36229
+ v_oordeel := 'compleet'; v_tekst := format('%s: belading past.', v_prod);
36230
+ END IF;
36231
+ uit := uit || jsonb_build_array(jsonb_build_object('product_id', v_pid, 'oordeel', v_oordeel, 'tekst', v_tekst,
36232
+ 'ladingdrager', rc->>'code', 'ladingdragerInvoer', rc->>'invoer', 'ladingdragerBron', rc->>'bron',
36233
+ 'regels', to_jsonb(ARRAY(SELECT upper(left(z,1)) || substr(z,2) || '.' FROM unnest(blok || mist || waar) z))));
36234
+ END LOOP;
36235
+ RETURN uit;
36236
+ END $$;
36237
+ GRANT EXECUTE ON FUNCTION public.logistics_product_oordeel_belading(jsonb) TO authenticated, service_role;
36238
+
36239
+ DO $do$
36240
+ DECLARE d text; n text;
36241
+ BEGIN
36242
+ IF to_regprocedure('public.logistics_product_oordeel_many(uuid[])') IS NULL THEN RETURN; END IF;
36243
+ d := pg_get_functiondef('public.logistics_product_oordeel_many(uuid[])'::regprocedure);
36244
+ IF position('belading niet ingesteld' IN d) > 0 THEN RETURN; END IF;
36245
+ n := replace(d,
36246
+ $a$ mist := array_remove(coalesce(r.ontbrekend, '{}'), 'weight');$a$,
36247
+ $a$ mist := array_remove(coalesce(r.ontbrekend, '{}'), 'weight');
36248
+ IF public.logistics_product_hoofdverpakking(r.product_id) IS NOT NULL
36249
+ AND NOT coalesce((public.logistics_product_hoofdverpakking(r.product_id)->>'aanwezig')::boolean, false) THEN
36250
+ mist := mist || 'belading niet ingesteld'::text;
36251
+ END IF;$a$);
36252
+ n := replace(n,
36253
+ $a$ oordeel := 'mist_gegevens'; tekst := r.uitleg;$a$,
36254
+ $a$ oordeel := 'mist_gegevens';
36255
+ tekst := CASE WHEN 'belading niet ingesteld' = ANY(mist) THEN format('%s: belading niet ingesteld.', r.product) ELSE r.uitleg END;$a$);
36256
+ IF n = d THEN RAISE NOTICE 'logistiek-engine: patroon oordeel_many niet gevonden'; RETURN; END IF;
36257
+ EXECUTE n;
36258
+ END $do$;
36259
+
36260
+ CREATE OR REPLACE FUNCTION public.logistics_cart_vulgraad_core(p_cart_id uuid)
36261
+ RETURNS numeric LANGUAGE plpgsql STABLE SET search_path TO 'public' AS $$
36262
+ DECLARE m jsonb := public.logistics_laag_menging_core(); v_n int; v_m int; v_tot numeric; v_ontbreekt boolean; v_groepen int;
36263
+ BEGIN
36264
+ SELECT bool_or(coalesce(ci.crates_per_layer, 0) <= 0 OR coalesce(ci.layers_per_cart, 0) <= 0) INTO v_ontbreekt
36265
+ FROM logistics_active.order_cart_items ci LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
36266
+ WHERE ci.cart_id = p_cart_id AND oi.quarantined_at IS NULL;
36267
+ IF v_ontbreekt IS NULL OR v_ontbreekt THEN RETURN NULL; END IF;
36268
+ IF m->>'menging' = 'per_regel' THEN
36269
+ SELECT sum(ceil(ci.crates::numeric / ci.crates_per_layer - 1e-9) / ci.layers_per_cart) INTO v_tot
36270
+ FROM logistics_active.order_cart_items ci LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
36271
+ WHERE ci.cart_id = p_cart_id AND oi.quarantined_at IS NULL;
36272
+ RETURN v_tot;
36273
+ END IF;
36274
+ v_n := nullif(m->>'klantenPerLaag', '')::int;
36275
+ v_m := nullif(m->>'beladingenPerLaag', '')::int;
36276
+ WITH r AS (
36277
+ SELECT ci.crates::numeric / ci.crates_per_layer f, ci.layers_per_cart::numeric lpc,
36278
+ coalesce(o.customer_id::text, oi.order_id::text, 'regel:' || ci.order_item_id::text) klant, ci.order_item_id::text regel
36279
+ FROM logistics_active.order_cart_items ci
36280
+ LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
36281
+ LEFT JOIN public.orders o ON o.id = oi.order_id
36282
+ WHERE ci.cart_id = p_cart_id AND oi.quarantined_at IS NULL),
36283
+ g AS (
36284
+ SELECT r.*, CASE
36285
+ WHEN v_n IS NULL AND v_m IS NULL THEN '1'
36286
+ WHEN v_m IS NOT NULL THEN r.klant || ':' || ((row_number() OVER (PARTITION BY r.klant ORDER BY r.regel) - 1) / v_m)
36287
+ ELSE ((dense_rank() OVER (ORDER BY r.klant) - 1) / v_n)::text END grp
36288
+ FROM r),
36289
+ u AS (SELECT grp, sum(f) lagen, sum(f / lpc) vul, min(lpc) minl FROM g GROUP BY grp)
36290
+ SELECT count(*), CASE WHEN count(*) <= 1 THEN sum(vul) ELSE sum(ceil(lagen - 1e-9) / minl) END INTO v_groepen, v_tot FROM u;
36291
+ RETURN v_tot;
36292
+ END $$;
36293
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_vulgraad_core(uuid) TO authenticated, service_role;
36294
+
36295
+ CREATE OR REPLACE FUNCTION public.logistics_kar_blokkade_vul(p_kar text, p_vul numeric)
36296
+ RETURNS jsonb LANGUAGE sql IMMUTABLE SET search_path TO 'public' AS $$
36297
+ SELECT CASE WHEN p_vul IS NOT NULL AND p_vul > 1.001
36298
+ THEN jsonb_build_object('teHoog', true, 'blokkade', jsonb_build_object('code', 'kar_te_hoog',
36299
+ 'tekst', format('Kar %s is voor %s %% gevuld, meer dan 100 %%. Deel de order opnieuw in of pas de belading aan.',
36300
+ coalesce(nullif(btrim(p_kar), ''), '?'), round(100 * p_vul)::int)))
36301
+ ELSE jsonb_build_object('teHoog', false, 'blokkade', NULL) END
36302
+ $$;
36303
+ GRANT EXECUTE ON FUNCTION public.logistics_kar_blokkade_vul(text, numeric) TO authenticated, service_role;
36304
+
36305
+ CREATE OR REPLACE FUNCTION public.logistics_cart_capaciteit_core(p_cart_id uuid)
36306
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path TO 'public' AS $$
36307
+ WITH c AS (SELECT oc.cart_number, oc.total_layers_used, oc.max_layers FROM logistics_active.order_carts oc WHERE oc.id = p_cart_id),
36308
+ i AS (SELECT max(nullif(ci.layers_per_cart, 0)) lpc
36309
+ FROM logistics_active.order_cart_items ci LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
36310
+ WHERE ci.cart_id = p_cart_id AND oi.quarantined_at IS NULL),
36311
+ v AS (SELECT c.cart_number,
36312
+ coalesce(public.logistics_cart_lagen_core(p_cart_id), c.total_layers_used::numeric) lagen,
36313
+ coalesce(i.lpc::numeric, nullif(c.max_layers, 0)::numeric) cap,
36314
+ public.logistics_cart_vulgraad_core(p_cart_id) vul,
36315
+ CASE WHEN i.lpc IS NOT NULL THEN 'product' WHEN coalesce(c.max_layers, 0) > 0 THEN 'ladingdrager' END bron
36316
+ FROM c, i)
36317
+ SELECT jsonb_build_object('layersUsed', v.lagen, 'maxLayers', v.cap, 'capaciteitBron', v.bron,
36318
+ 'laagMenging', public.logistics_laag_menging_core(),
36319
+ 'vulgraad', round(v.vul, 4),
36320
+ 'vulgraadPct', coalesce(round(100 * v.vul), CASE WHEN coalesce(v.cap, 0) > 0 AND v.lagen IS NOT NULL THEN round(100 * v.lagen / v.cap) END),
36321
+ 'layerUtilizationPct', coalesce(round(100 * v.vul), CASE WHEN coalesce(v.cap, 0) > 0 AND v.lagen IS NOT NULL THEN round(100 * v.lagen / v.cap) END))
36322
+ || CASE WHEN v.vul IS NOT NULL THEN public.logistics_kar_blokkade_vul(v.cart_number::text, v.vul)
36323
+ ELSE public.logistics_kar_blokkade(v.cart_number::text, v.lagen, v.cap) END
36324
+ FROM v
36325
+ $$;
36326
+
36327
+ CREATE OR REPLACE FUNCTION public.logistics_kar_tekst(p_carrier_code text, p_lagen numeric, p_pct numeric)
36328
+ RETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
36329
+ SELECT CASE WHEN p_lagen IS NULL THEN NULL ELSE
36330
+ replace(trim_scale(round(p_lagen, 1))::text, '.', ',') || ' lagen · '
36331
+ || coalesce((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), 'Ladingdrager')
36332
+ || CASE WHEN coalesce(public.logistics_extra_platen(p_lagen), 0) = 0 THEN ''
36333
+ WHEN public.logistics_extra_platen(p_lagen) = 1 THEN ' + 1 plaat'
36334
+ ELSE ' + ' || public.logistics_extra_platen(p_lagen) || ' platen' END
36335
+ || CASE WHEN p_pct IS NULL THEN '' ELSE ' · ' || round(p_pct)::int || ' %' END END
36336
+ $$;
36337
+ GRANT EXECUTE ON FUNCTION public.logistics_kar_tekst(text, numeric, numeric) TO authenticated, service_role;
36338
+
36339
+ CREATE OR REPLACE FUNCTION public.logistics_cart_platen_verrijk(p_cart jsonb)
36340
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
36341
+ DECLARE v_id uuid; k jsonb; c jsonb := p_cart; v_l numeric; v_pct numeric;
36342
+ BEGIN
36343
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_cart_platen_verrijk');
36344
+ BEGIN v_id := nullif(coalesce(p_cart->>'cartId', p_cart->>'cart_id'), '')::uuid; EXCEPTION WHEN invalid_text_representation THEN v_id := NULL; END;
36345
+ IF v_id IS NOT NULL THEN k := public.logistics_cart_capaciteit_core(v_id); END IF;
36346
+ IF k IS NOT NULL THEN
36347
+ c := c || jsonb_build_object('layersUsed', k->'layersUsed', 'maxLayers', k->'maxLayers', 'capaciteitBron', k->'capaciteitBron',
36348
+ 'vulgraadPct', k->'vulgraadPct', 'layerUtilizationPct', k->'layerUtilizationPct');
36349
+ END IF;
36350
+ v_l := nullif(c->>'layersUsed', '')::numeric;
36351
+ v_pct := coalesce(nullif(c->>'vulgraadPct', '')::numeric, nullif(c->>'layerUtilizationPct', '')::numeric, nullif(c->>'occupancyPct', '')::numeric);
36352
+ RETURN c || jsonb_build_object(
36353
+ 'additionalLayers', public.logistics_extra_platen(v_l),
36354
+ 'ladingdragerTekst', public.logistics_ladingdrager_tekst(c->>'carrierType', v_l, v_pct),
36355
+ 'karTekst', public.logistics_kar_tekst(c->>'carrierType', v_l, v_pct))
36356
+ || CASE WHEN k IS NOT NULL THEN jsonb_build_object('teHoog', coalesce(k->'teHoog', 'false'::jsonb), 'blokkade', k->'blokkade')
36357
+ ELSE public.logistics_kar_blokkade(coalesce(c->>'cartNumber', c->>'cart_number', c->>'seq'), v_l, nullif(c->>'maxLayers', '')::numeric) END;
36358
+ END $$;
36359
+
36360
+ DO $do$
36361
+ DECLARE d text; n text;
36362
+ BEGIN
36363
+ d := pg_get_functiondef('public.logistics_product_oordeel_belading(jsonb)'::regprocedure);
36364
+ IF position('c := NULL;' IN d) = 0 THEN RETURN; END IF;
36365
+ n := replace(d, 'v_eigen := NULL; c := NULL; v_cfout := NULL;',
36366
+ 'v_eigen := NULL; v_cfout := NULL;
36367
+ SELECT NULL::text AS code, NULL::text AS name, NULL::numeric AS layer_length_cm, NULL::numeric AS layer_width_cm,
36368
+ NULL::numeric AS max_height_cm, NULL::integer AS max_crates_per_layer, NULL::integer AS layers_per_carrier INTO c;');
36369
+ EXECUTE n;
36370
+ END $do$;
36371
+
36372
+ CREATE OR REPLACE FUNCTION public.logistics_pick_cart_breakdown_fast(_order_id uuid)
36373
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $function$
36374
+ DECLARE
36375
+ v_orders uuid[]; v_ref text; v_status jsonb;
36376
+ v_ready boolean := false; v_released boolean := false;
36377
+ v_total integer := 0; v_full integer := 0; v_partial integer := 0;
36378
+ v_shared integer := 0; v_crates integer := 0;
36379
+ BEGIN
36380
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_pick_cart_breakdown_fast');
36381
+ v_orders := public.pick_buffer_order_ids(_order_id);
36382
+ IF v_orders IS NULL OR cardinality(v_orders) = 0 THEN v_orders := ARRAY[_order_id]; END IF;
36383
+ SELECT o.order_number INTO v_ref FROM public.orders o WHERE o.id = _order_id;
36384
+ -- 1.0.208: alleen de kern; planState (logistics_plan_state_for) wordt hier niet gebruikt.
36385
+ v_status := public.logistics_cart_plan_status_core(v_ref);
36386
+ v_ready := COALESCE((v_status->>'ready')::boolean, false);
36387
+ v_released := COALESCE((v_status->>'released')::boolean, false);
36388
+ IF v_ready THEN
36389
+ v_total := COALESCE((v_status->>'cartTotal')::integer, (v_status->>'totalCarts')::integer, 0);
36390
+ SELECT
36391
+ count(*) FILTER (WHERE COALESCE((c->>'isFullCart')::boolean, false)),
36392
+ count(*) FILTER (WHERE NOT COALESCE((c->>'isFullCart')::boolean, false)),
36393
+ count(*) FILTER (WHERE COALESCE((c->>'isShared')::boolean, false))
36394
+ INTO v_full, v_partial, v_shared
36395
+ FROM jsonb_array_elements(COALESCE(v_status->'carts', '[]'::jsonb)) c;
36396
+ IF v_total = 0 THEN v_total := v_full + v_partial; END IF;
36397
+ IF v_total > 0 AND v_full + v_partial <> v_total THEN
36398
+ v_partial := GREATEST(v_total - v_full, 0);
36399
+ END IF;
36400
+ END IF;
36401
+ SELECT COALESCE(sum(ci.crates), 0)::integer INTO v_crates
36402
+ FROM public.order_cart_items ci
36403
+ JOIN logistics_active.order_items oi ON oi.id = ci.order_item_id
36404
+ WHERE oi.order_id = ANY(v_orders);
36405
+ RETURN jsonb_build_object(
36406
+ 'totalCarts', COALESCE(v_total, 0), 'fullCarts', COALESCE(v_full, 0), 'partialCarts', COALESCE(v_partial, 0),
36407
+ 'sharedCarts', COALESCE(v_shared, 0), 'crates', COALESCE(v_crates, 0),
36408
+ 'cartPlanReady', v_ready, 'cartPlanReleased', v_released, 'cartPlanReason', v_status->>'reason'
36409
+ ) || public.logistics_pick_mode();
36410
+ END; $function$;
36411
+ GRANT EXECUTE ON FUNCTION public.logistics_pick_cart_breakdown_fast(uuid) TO authenticated, service_role;
36412
+
36413
+ DO $do$
36414
+ DECLARE d text; n text;
36415
+ BEGIN
36416
+ -- Kern
36417
+ d := pg_get_functiondef('public.logistics_sync_pick_queue_core()'::regprocedure);
36418
+ IF position('v_kand uuid[]' IN d) = 0 THEN
36419
+ n := replace(d, 'v_wait timestamptz; v_reason text; v_ok boolean;',
36420
+ 'v_wait timestamptz; v_reason text; v_ok boolean; v_kand uuid[]; v_rel uuid[];');
36421
+ n := replace(n, E' SELECT min(d) INTO v_pickday FROM (',
36422
+ E' -- 1.0.208: controles per order één keer voor alle orders samen.\n'
36423
+ || E' SELECT coalesce(array_agg(ord.id), ''{}'') INTO v_kand FROM public.orders ord\n'
36424
+ || E' WHERE ord.order_number IS NOT NULL\n'
36425
+ || E' AND EXISTS (SELECT 1 FROM public.logistics_tasks lt WHERE lt.order_ref = ord.order_number AND lt.task_type = ''pick.execute''\n'
36426
+ || E' AND lt.status NOT IN (''done'',''cancelled'',''superseded'',''skipped''))\n'
36427
+ || E' AND public.logistics_order_is_topick(ord.id, ord.order_number)\n'
36428
+ || E' AND NOT public.logistics_order_handled(ord.id);\n'
36429
+ || E' SELECT coalesce(array_agg(ord.id), ''{}'') INTO v_rel FROM public.orders ord\n'
36430
+ || E' WHERE ord.id = ANY(v_kand) AND public.logistics_order_released(ord.order_number);\n'
36431
+ || E' SELECT min(d) INTO v_pickday FROM (');
36432
+ n := regexp_replace(n, 'public\.logistics_order_is_topick\(ord\.id, ord\.order_number\)\s+AND NOT public\.logistics_order_handled\(ord\.id\)\s+AND public\.logistics_order_released\(ord\.order_number\)',
36433
+ 'ord.id = ANY(v_rel)', 'g');
36434
+ n := regexp_replace(n, 'public\.logistics_order_is_topick\(ord\.id, ord\.order_number\)\s+AND NOT public\.logistics_order_handled\(ord\.id\)\s+AND \(v_forced IS NOT NULL OR public\.logistics_order_released\(ord\.order_number\)\)',
36435
+ 'ord.id = ANY(v_kand) AND (v_forced IS NOT NULL OR ord.id = ANY(v_rel))', 'g');
36436
+ -- de eerste vervanging hierboven mag de nieuwe v_kand-berekening niet raken: die heeft geen released-regel erachter.
36437
+ n := replace(n, 'v_breakdown := public.pick_cart_breakdown(r_unit.lead_id);', 'v_breakdown := public.logistics_pick_cart_breakdown_fast(r_unit.lead_id);');
36438
+ EXECUTE n;
36439
+ END IF;
36440
+
36441
+ -- Achterstand
36442
+ d := pg_get_functiondef('public.logistics_sync_pick_queue_backlog()'::regprocedure);
36443
+ IF position('v_bl uuid[]' IN d) = 0 THEN
36444
+ n := replace(d, 'v_ins integer := 0; v_upd integer := 0; v_orders integer := 0;',
36445
+ 'v_ins integer := 0; v_upd integer := 0; v_orders integer := 0; v_bl uuid[];');
36446
+ n := replace(n, E' FOR r_unit IN\n WITH k AS (',
36447
+ E' -- 1.0.208: achterstand in één keer voor alle orders (zelfde regel als logistics_order_is_backlog).\n'
36448
+ || E' SELECT coalesce(array_agg(b.order_id), ''{}'') INTO v_bl\n'
36449
+ || E' FROM public.logistics_order_backlog_info_many(ARRAY(SELECT o.id FROM public.orders o WHERE o.order_number IS NOT NULL)) b\n'
36450
+ || E' JOIN public.orders o ON o.id = b.order_id\n'
36451
+ || E' WHERE b.info IS NOT NULL AND (b.info->>''dagen'')::int <= v_max AND public.logistics_order_released(o.order_number);\n'
36452
+ || E' FOR r_unit IN\n WITH k AS (');
36453
+ n := replace(n, 'public.logistics_order_is_backlog(o.id)', 'o.id = ANY(v_bl)');
36454
+ n := replace(n, 'v_breakdown := public.pick_cart_breakdown(r_unit.lead_id);', 'v_breakdown := public.logistics_pick_cart_breakdown_fast(r_unit.lead_id);');
36455
+ EXECUTE n;
36456
+ END IF;
36457
+ END $do$;
36458
+
36459
+ DO $do$
36460
+ DECLARE d text; n text;
36461
+ BEGIN
36462
+ d := pg_get_functiondef('public.logistics_cart_plan_status_core_v152(text)'::regprocedure);
36463
+ n := replace(d, 'public.logistics_cart_plan_status_core_v152(', 'public.logistics_cart_plan_status_slim(');
36464
+ n := replace(n, '''logistics_cart_plan_status_core_v152''', '''logistics_cart_plan_status_slim''');
36465
+ n := replace(n, 'public.logistics_cart_json_fixed(c)',
36466
+ 'c || coalesce(public.logistics_cart_fixed_numbering(nullif(c->>''cartId'', '''')::uuid), ''{}''::jsonb)');
36467
+ IF position('logistics_cart_json_fixed' IN n) > 0 THEN RAISE EXCEPTION 'vervanging json_fixed mislukt'; END IF;
36468
+ EXECUTE n;
36469
+
36470
+ d := pg_get_functiondef('public.logistics_pick_cart_breakdown_fast(uuid)'::regprocedure);
36471
+ IF position('logistics_cart_plan_status_slim' IN d) = 0 THEN
36472
+ EXECUTE replace(d, 'v_status := public.logistics_cart_plan_status_core(v_ref);', 'v_status := public.logistics_cart_plan_status_slim(v_ref);');
36473
+ END IF;
36474
+ END $do$;
36475
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_plan_status_slim(text) TO service_role;
36476
+
36477
+ DO $do$
36478
+ DECLARE d text; n text; v_extra text := ' || jsonb_build_object(''cartCount'', 1, ''perCart'', true, ''pickMode'', ''cart'', ''cartPlanReady'', true)';
36479
+ BEGIN
36480
+ d := pg_get_functiondef('public.logistics_sync_pick_queue_backlog()'::regprocedure);
36481
+ IF position('''cartPlanReady'', true)' IN d) = 0 THEN
36482
+ n := replace(d, E'COALESCE(q.payload, ''{}''::jsonb) || v_payload);', E'COALESCE(q.payload, ''{}''::jsonb) || v_payload' || v_extra || ');');
36483
+ IF n = d THEN RAISE EXCEPTION 'patroon achterstand niet gevonden'; END IF;
36484
+ EXECUTE n;
36485
+ END IF;
36486
+ d := pg_get_functiondef('public.logistics_sync_pick_queue_core()'::regprocedure);
36487
+ IF position('''cartPlanReady'', true)' IN d) = 0 THEN
36488
+ n := replace(d, E'COALESCE(v_wait, v_due, q.due_at), COALESCE(q.payload, ''{}''::jsonb) || v_cart_payload);',
36489
+ E'COALESCE(v_wait, v_due, q.due_at), COALESCE(q.payload, ''{}''::jsonb) || v_cart_payload' || v_extra || ');');
36490
+ IF n = d THEN RAISE EXCEPTION 'patroon kern niet gevonden'; END IF;
36491
+ EXECUTE n;
36492
+ END IF;
36493
+ END $do$;
36494
+
36495
+ CREATE OR REPLACE FUNCTION public.logistics_cart_verouderd_core(p_cart_id uuid)
36496
+ RETURNS jsonb LANGUAGE plpgsql STABLE SET search_path TO 'public' AS $$
36497
+ DECLARE v_nr text; v_n int;
36498
+ BEGIN
36499
+ IF p_cart_id IS NULL THEN RETURN NULL; END IF;
36500
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'order_items' AND column_name = 'quarantined_at') THEN
36501
+ RETURN NULL;
36502
+ END IF;
36503
+ SELECT count(*) INTO v_n FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
36504
+ WHERE ci.cart_id = p_cart_id AND oi.quarantined_at IS NOT NULL;
36505
+ IF coalesce(v_n, 0) = 0 THEN RETURN NULL; END IF;
36506
+ SELECT cart_number::text INTO v_nr FROM public.order_carts WHERE id = p_cart_id;
36507
+ RETURN jsonb_build_object('verouderd', true, 'regelsInQuarantaine', v_n,
36508
+ 'blokkade', jsonb_build_object('code', 'kar_verouderd',
36509
+ 'tekst', format('Kar %s heeft %s regel(s) die Verkoop in quarantaine heeft gezet. Deel de niet-geprinte karren opnieuw in.', coalesce(v_nr, '?'), v_n)));
36510
+ END $$;
36511
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_verouderd_core(uuid) TO service_role;
36512
+
36513
+ CREATE OR REPLACE FUNCTION public.logistics_cart_te_hoog(p_cart_id uuid)
36514
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
36515
+ DECLARE k jsonb; v jsonb;
36516
+ BEGIN
36517
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_cart_te_hoog');
36518
+ k := public.logistics_cart_capaciteit_core(p_cart_id);
36519
+ IF k IS NOT NULL AND coalesce((k->>'teHoog')::boolean, false) THEN
36520
+ RETURN jsonb_build_object('teHoog', true, 'blokkade', k->'blokkade');
36521
+ END IF;
36522
+ -- 1.0.208: verouderde kar (regels in quarantaine) krijgt ook geen stickers/verzendbrief/pakbon.
36523
+ v := public.logistics_cart_verouderd_core(p_cart_id);
36524
+ IF v IS NOT NULL THEN
36525
+ RETURN jsonb_build_object('teHoog', false, 'indelingVerouderd', true, 'blokkade', v->'blokkade');
36526
+ END IF;
36527
+ RETURN jsonb_build_object('teHoog', false, 'blokkade', NULL);
36528
+ END $$;
36529
+
36530
+ DO $do$
36531
+ DECLARE d text; n text;
36532
+ BEGIN
36533
+ d := pg_get_functiondef('public.logistics_cart_platen_verrijk(jsonb)'::regprocedure);
36534
+ IF position('logistics_cart_verouderd_core' IN d) = 0 THEN
36535
+ n := replace(d,
36536
+ $a$ || CASE WHEN k IS NOT NULL THEN jsonb_build_object('teHoog', coalesce(k->'teHoog', 'false'::jsonb), 'blokkade', k->'blokkade')$a$,
36537
+ $a$ || CASE WHEN v_id IS NOT NULL AND public.logistics_cart_verouderd_core(v_id) IS NOT NULL
36538
+ AND NOT coalesce((k->>'teHoog')::boolean, false)
36539
+ THEN jsonb_build_object('teHoog', false, 'indelingVerouderd', true, 'blokkade', public.logistics_cart_verouderd_core(v_id)->'blokkade')
36540
+ WHEN k IS NOT NULL THEN jsonb_build_object('teHoog', coalesce(k->'teHoog', 'false'::jsonb), 'blokkade', k->'blokkade')$a$);
36541
+ IF n = d THEN RAISE EXCEPTION 'patroon platen_verrijk niet gevonden'; END IF;
36542
+ EXECUTE n;
36543
+ END IF;
36544
+ END $do$;
36545
+
36546
+ CREATE OR REPLACE FUNCTION public.logistics_order_indeling_verouderd_core(p_order_id uuid)
36547
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path TO 'public' AS $$
36548
+ WITH a AS (
36549
+ SELECT oi.id, coalesce(oi.product_name, 'regel') naam, ci.cart_id,
36550
+ (oi.plants_per_crate IS DISTINCT FROM ci.plants_per_crate
36551
+ OR oi.crates_per_layer IS DISTINCT FROM ci.crates_per_layer
36552
+ OR oi.layers_per_cart IS DISTINCT FROM ci.layers_per_cart) afwijkend
36553
+ FROM public.order_items oi
36554
+ JOIN logistics_active.order_cart_items ci ON ci.order_item_id = oi.id
36555
+ WHERE oi.order_id = p_order_id AND oi.packing_locked_at IS NOT NULL AND oi.quarantined_at IS NULL),
36556
+ q AS (
36557
+ SELECT DISTINCT ci.cart_id, coalesce(oi.product_name, 'regel') naam
36558
+ FROM public.order_items oi
36559
+ JOIN public.order_cart_items ci ON ci.order_item_id = oi.id
36560
+ JOIN logistics_active.order_carts c ON c.id = ci.cart_id
36561
+ WHERE oi.order_id = p_order_id AND oi.quarantined_at IS NOT NULL)
36562
+ SELECT CASE
36563
+ WHEN EXISTS (SELECT 1 FROM q) THEN jsonb_build_object(
36564
+ 'verouderd', true, 'blokkeert', true, 'reden', 'quarantaine',
36565
+ 'producten', (SELECT to_jsonb(array_agg(DISTINCT naam)) FROM q),
36566
+ 'karren', (SELECT to_jsonb(array_agg(DISTINCT cart_id)) FROM q),
36567
+ 'vastliggendeKarren', (SELECT count(DISTINCT q2.cart_id) FROM q q2 WHERE public.logistics_cart_vast_core(q2.cart_id)),
36568
+ 'tekst', 'Er staan regels op de karren die Verkoop in quarantaine heeft gezet. Deel de niet-geprinte karren opnieuw in.')
36569
+ WHEN (SELECT bool_or(afwijkend) FROM a) THEN jsonb_build_object(
36570
+ 'verouderd', true, 'blokkeert', false, 'reden', 'belading',
36571
+ 'producten', (SELECT to_jsonb(array_agg(DISTINCT naam) FILTER (WHERE afwijkend)) FROM a),
36572
+ 'vastliggendeKarren', (SELECT count(DISTINCT a2.cart_id) FROM a a2 WHERE public.logistics_cart_vast_core(a2.cart_id)),
36573
+ 'tekst', 'De belading is gewijzigd, maar de karindeling is nog oud. Deel de niet-geprinte karren opnieuw in.')
36574
+ ELSE NULL END
36575
+ $$;
36576
+
36577
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_208()
36578
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
36579
+ SELECT 'vulgraad per kar', to_regprocedure('public.logistics_cart_vulgraad_core(uuid)') IS NOT NULL, 'Vulgraad = som per regel van lagen ÷ lagen per kar (Floriday)'
36580
+ UNION ALL SELECT 'karTekst', to_regprocedure('public.logistics_kar_tekst(text,numeric,numeric)') IS NOT NULL, 'Tekst per kar: lagen · ladingdrager + platen · %'
36581
+ UNION ALL SELECT 'ladingdrager-vertaling', to_regprocedure('public.logistics_ladingdrager_resolve(text)') IS NOT NULL, 'Floriday- en Beheer-codes'
36582
+ UNION ALL SELECT 'vertaling eenduidig', NOT EXISTS (SELECT 1 FROM public.logistics_floriday_carrier_map GROUP BY floriday_code HAVING count(*) > 1), 'Elke Floriday-code naar precies één Beheer-type'
36583
+ UNION ALL SELECT 'hoofdverpakking', to_regprocedure('public.logistics_product_hoofdverpakking(uuid)') IS NOT NULL, 'Ladingdrager uit hoofdverpakking'
36584
+ UNION ALL SELECT 'pickwachtrij set-based', position('v_kand' IN pg_get_functiondef('public.logistics_sync_pick_queue_core()'::regprocedure)) > 0
36585
+ AND position('v_bl' IN pg_get_functiondef('public.logistics_sync_pick_queue_backlog()'::regprocedure)) > 0, 'Controles één keer voor alle orders'
36586
+ UNION ALL SELECT 'verouderde kar', to_regprocedure('public.logistics_cart_verouderd_core(uuid)') IS NOT NULL
36587
+ AND position('logistics_cart_verouderd_core' IN pg_get_functiondef('public.logistics_cart_te_hoog(uuid)'::regprocedure)) > 0, 'Kar met quarantaineregels krijgt geen documenten'
36588
+ UNION ALL SELECT 'guards 1.0.208', position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_ladingdrager_resolve(text)'::regprocedure)) > 0
36589
+ AND position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_product_hoofdverpakking(uuid)'::regprocedure)) > 0, 'Nieuwe functies controleren de aanroeper'
36590
+ $$;
36591
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_208() TO authenticated, service_role;