@flowselections/logistiek-engine 1.0.214 → 1.0.219

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.
@@ -36952,3 +36952,699 @@ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY D
36952
36952
  UNION ALL SELECT 'guard', to_regprocedure('public.logistics_acc_guard(text,text)') IS NOT NULL, 'Toegangscontrole aanwezig'
36953
36953
  $$;
36954
36954
  GRANT EXECUTE ON FUNCTION public.logistics_install_check_213() TO authenticated, service_role;
36955
+
36956
+ -- ===== 1.0.215 =====
36957
+
36958
+ CREATE OR REPLACE FUNCTION public.logistics_product_hoofdverpakking(p_product_id uuid)
36959
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
36960
+ AS $function$
36961
+ DECLARE v_lc text; v_found boolean := false; v_heeft_lc boolean; v_n int; v_sid uuid; v_lijst text;
36962
+ BEGIN
36963
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_product_hoofdverpakking');
36964
+ IF to_regclass('public.product_packings') IS NULL THEN RETURN NULL; END IF;
36965
+ SELECT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.product_packings'::regclass AND attname = 'load_carrier' AND NOT attisdropped) INTO v_heeft_lc;
36966
+ IF NOT v_heeft_lc THEN
36967
+ EXECUTE 'SELECT true FROM public.product_packings WHERE product_id = $1 AND is_primary LIMIT 1' INTO v_found USING p_product_id;
36968
+ RETURN jsonb_build_object('aanwezig', coalesce(v_found, false), 'ladingdrager', NULL, 'bron', CASE WHEN v_found THEN 'product' END);
36969
+ END IF;
36970
+ -- Stap 2: primaire verpakking van het product zelf.
36971
+ EXECUTE 'SELECT true, nullif(btrim(load_carrier::text), '''') FROM public.product_packings WHERE product_id = $1 AND is_primary
36972
+ ORDER BY sort_order NULLS LAST, created_at LIMIT 1' INTO v_found, v_lc USING p_product_id;
36973
+ IF coalesce(v_found, false) AND v_lc IS NOT NULL THEN
36974
+ RETURN jsonb_build_object('aanwezig', true, 'ladingdrager', v_lc, 'bron', 'product');
36975
+ END IF;
36976
+ -- Stap 3 (1.0.215): eigen artikel → voorraadartikel met products.floriday_trade_item_id = dit product-id.
36977
+ IF EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.products'::regclass AND attname = 'floriday_trade_item_id' AND NOT attisdropped) THEN
36978
+ EXECUTE $q$
36979
+ SELECT count(DISTINCT x.lc), min(x.lc), (array_agg(x.pid ORDER BY x.pid))[1], string_agg(DISTINCT x.lc, ', ')
36980
+ FROM (SELECT DISTINCT ON (s.id) s.id pid, nullif(btrim(pp.load_carrier::text), '') lc
36981
+ FROM public.products s JOIN public.product_packings pp ON pp.product_id = s.id AND pp.is_primary
36982
+ WHERE s.floriday_trade_item_id = $1::text AND s.id <> $1
36983
+ ORDER BY s.id, pp.sort_order NULLS LAST, pp.created_at) x
36984
+ WHERE x.lc IS NOT NULL$q$
36985
+ INTO v_n, v_lc, v_sid, v_lijst USING p_product_id;
36986
+ IF coalesce(v_n, 0) = 1 THEN
36987
+ RETURN jsonb_build_object('aanwezig', true, 'ladingdrager', v_lc, 'bron', 'voorraadartikel', 'voorraadartikelId', v_sid);
36988
+ ELSIF coalesce(v_n, 0) > 1 THEN
36989
+ RETURN jsonb_build_object('aanwezig', false, 'ladingdrager', NULL, 'bron', 'voorraadartikel',
36990
+ 'fout', format('gekoppelde voorraadartikelen hebben verschillende ladingdragers (%s)', v_lijst));
36991
+ END IF;
36992
+ END IF;
36993
+ RETURN jsonb_build_object('aanwezig', coalesce(v_found, false), 'ladingdrager', NULL, 'bron', CASE WHEN v_found THEN 'product' END);
36994
+ END $function$;
36995
+ GRANT EXECUTE ON FUNCTION public.logistics_product_hoofdverpakking(uuid) TO authenticated, service_role;
36996
+
36997
+ -- Gedeelde keuze per planregel: regel → product → voorraadartikel → Floriday-verpakking → werkwijze (waarschuwing).
36998
+ -- p_regels: [{ "key": text, "regel_code": text|null, "product_id": uuid|null }], max 2000.
36999
+ CREATE OR REPLACE FUNCTION public.logistics_ladingdrager_kies(p_regels jsonb)
37000
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
37001
+ AS $function$
37002
+ DECLARE e jsonb; uit jsonb := '{}'; rc jsonb; hv jsonb; v_pid uuid; v_key text; v_def text; v_ti text; v_lc text;
37003
+ v_cache boolean := to_regclass('public.floriday_trade_items_cache') IS NOT NULL;
37004
+ BEGIN
37005
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_ladingdrager_kies');
37006
+ IF p_regels IS NULL OR jsonb_typeof(p_regels) <> 'array' THEN RETURN '{}'; END IF;
37007
+ IF jsonb_array_length(p_regels) > 2000 THEN
37008
+ RAISE EXCEPTION 'Maximaal 2000 regels per aanroep.' USING ERRCODE = '22023';
37009
+ END IF;
37010
+ SELECT nullif(btrim(coalesce(s.settings->'karrenindeling'->'fields'->>'default_carrier_type', s.settings->'karrenindeling'->>'default_carrier_type')), '')
37011
+ INTO v_def FROM public.logistics_rule_configs s WHERE s.is_active ORDER BY s.updated_at DESC LIMIT 1;
37012
+ IF v_def IS NULL OR NOT EXISTS (SELECT 1 FROM public.load_carrier_types t WHERE t.is_active AND t.code = v_def) THEN
37013
+ SELECT t.code INTO v_def FROM public.load_carrier_types t WHERE t.is_active AND t.is_default ORDER BY t.sort_order LIMIT 1;
37014
+ END IF;
37015
+ FOR e IN SELECT x FROM jsonb_array_elements(p_regels) x LOOP
37016
+ v_key := e->>'key';
37017
+ CONTINUE WHEN v_key IS NULL;
37018
+ v_pid := CASE WHEN nullif(btrim(e->>'product_id'), '') ~* '^[0-9a-f-]{36}$' THEN (e->>'product_id')::uuid END;
37019
+ -- 1. orderregel
37020
+ rc := public.logistics_ladingdrager_resolve(e->>'regel_code');
37021
+ IF rc->>'code' IS NOT NULL THEN
37022
+ uit := uit || jsonb_build_object(v_key, jsonb_build_object('code', rc->>'code', 'bron', 'orderregel', 'invoer', rc->>'invoer'));
37023
+ CONTINUE;
37024
+ END IF;
37025
+ -- 2/3. product, dan voorraadartikel
37026
+ IF v_pid IS NOT NULL THEN
37027
+ hv := public.logistics_product_hoofdverpakking(v_pid);
37028
+ IF nullif(hv->>'ladingdrager', '') IS NOT NULL THEN
37029
+ rc := public.logistics_ladingdrager_resolve(hv->>'ladingdrager');
37030
+ IF rc->>'code' IS NOT NULL THEN
37031
+ uit := uit || jsonb_build_object(v_key, jsonb_build_object('code', rc->>'code', 'bron', hv->>'bron', 'invoer', rc->>'invoer',
37032
+ 'voorraadartikelId', hv->'voorraadartikelId'));
37033
+ CONTINUE;
37034
+ ELSIF upper(hv->>'ladingdrager') = 'NONE' OR rc->>'fout' = 'ladingdrager ontbreekt' THEN
37035
+ uit := uit || jsonb_build_object(v_key, jsonb_build_object('code', NULL, 'bron', hv->>'bron', 'fout', 'ladingdrager ontbreekt (geen ladingdrager)'));
37036
+ CONTINUE;
37037
+ END IF;
37038
+ ELSIF hv->>'fout' IS NOT NULL THEN
37039
+ uit := uit || jsonb_build_object(v_key, jsonb_build_object('code', NULL, 'bron', hv->>'bron', 'fout', hv->>'fout'));
37040
+ CONTINUE;
37041
+ END IF;
37042
+ -- 3b. Floriday-verpakking van het artikel (bestaand gedrag sinds 1.0.190).
37043
+ IF v_cache THEN
37044
+ v_lc := NULL;
37045
+ SELECT p.floriday_trade_item_id INTO v_ti FROM public.products p WHERE p.id = v_pid;
37046
+ IF v_ti IS NOT NULL THEN
37047
+ EXECUTE $q$SELECT c->>'loadCarrier' FROM public.floriday_trade_items_cache t,
37048
+ jsonb_array_elements(coalesce(t.data->'packingConfigurations', '[]')) c
37049
+ WHERE t.floriday_id = $1 AND NOT coalesce(t.is_deleted, false) AND nullif(c->>'loadCarrier', '') IS NOT NULL
37050
+ ORDER BY coalesce((c->>'primary')::boolean, false) DESC LIMIT 1$q$ INTO v_lc USING v_ti;
37051
+ rc := public.logistics_ladingdrager_resolve(v_lc);
37052
+ IF rc->>'code' IS NOT NULL THEN
37053
+ uit := uit || jsonb_build_object(v_key, jsonb_build_object('code', rc->>'code', 'bron', 'floriday_verpakking', 'invoer', rc->>'invoer'));
37054
+ CONTINUE;
37055
+ END IF;
37056
+ END IF;
37057
+ END IF;
37058
+ END IF;
37059
+ -- 4. werkwijze, zichtbaar
37060
+ uit := uit || jsonb_build_object(v_key, jsonb_build_object('code', v_def, 'bron', CASE WHEN v_def IS NOT NULL THEN 'werkwijze' END,
37061
+ 'waarschuwing', CASE WHEN v_def IS NOT NULL THEN 'ladingdrager uit de werkwijze, niet uit het artikel' END,
37062
+ 'fout', CASE WHEN v_def IS NULL THEN 'ladingdrager ontbreekt' END));
37063
+ END LOOP;
37064
+ RETURN uit;
37065
+ END $function$;
37066
+ GRANT EXECUTE ON FUNCTION public.logistics_ladingdrager_kies(jsonb) TO authenticated, service_role;
37067
+
37068
+ -- Kar die volgens de huidige regels anders zou worden ingedeeld (geen blokkade).
37069
+ CREATE OR REPLACE FUNCTION public.logistics_cart_regels_verouderd_core(p_cart_id uuid)
37070
+ RETURNS jsonb LANGUAGE plpgsql STABLE SET search_path TO 'public'
37071
+ AS $function$
37072
+ DECLARE v_cart record; v_reg jsonb; k jsonb; v_codes text[]; v_naam text; v_max int; v_lagen numeric; v_lh numeric;
37073
+ BEGIN
37074
+ IF p_cart_id IS NULL THEN RETURN NULL; END IF;
37075
+ SELECT c.id, c.cart_number, c.carrier_type, c.total_layers_used INTO v_cart FROM public.order_carts c WHERE c.id = p_cart_id;
37076
+ IF v_cart.id IS NULL THEN RETURN NULL; END IF;
37077
+ IF nullif(v_cart.carrier_type, '') IS NULL OR v_cart.carrier_type IN ('loose', 'none', 'NONE') THEN
37078
+ RETURN jsonb_build_object('reden', 'geen_ladingdrager', 'tekst', 'Kar staat zonder ladingdrager; een product zonder ladingdrager kan niet op een kar.');
37079
+ END IF;
37080
+ SELECT coalesce(jsonb_agg(jsonb_build_object('key', ci.id::text, 'regel_code', oi.load_carrier_type, 'product_id', coalesce(ci.product_id, oi.product_id))), '[]')
37081
+ INTO v_reg
37082
+ FROM public.order_cart_items ci LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
37083
+ WHERE ci.cart_id = p_cart_id AND coalesce(ci.crates, 0) > 0;
37084
+ IF jsonb_array_length(v_reg) > 0 THEN
37085
+ k := public.logistics_ladingdrager_kies(v_reg);
37086
+ SELECT array_agg(DISTINCT x.value->>'code') FILTER (WHERE x.value->>'code' IS NOT NULL AND x.value->>'bron' <> 'werkwijze')
37087
+ INTO v_codes FROM jsonb_each(k) x;
37088
+ IF coalesce(cardinality(v_codes), 0) > 0 AND NOT (v_cart.carrier_type = ANY(v_codes)) THEN
37089
+ SELECT coalesce(t.name, t.code) INTO v_naam FROM public.load_carrier_types t WHERE t.code = v_codes[1] LIMIT 1;
37090
+ RETURN jsonb_build_object('reden', 'andere_ladingdrager', 'ladingdrager', v_codes[1],
37091
+ 'tekst', format('Volgens het artikel hoort deze kar op %s, niet op %s.', coalesce(v_naam, v_codes[1]),
37092
+ coalesce((SELECT coalesce(t.name, t.code) FROM public.load_carrier_types t WHERE t.code = v_cart.carrier_type LIMIT 1), v_cart.carrier_type)));
37093
+ END IF;
37094
+ END IF;
37095
+ -- Fysieke hoogte: lagen × laaghoogte (max(fusthoogte, planthoogte)) boven de hoogte van de ladingdrager.
37096
+ SELECT t.max_height_cm INTO v_max FROM public.load_carrier_types t WHERE t.code = v_cart.carrier_type ORDER BY t.is_active DESC LIMIT 1;
37097
+ IF v_max IS NOT NULL AND v_max > 0 THEN
37098
+ SELECT max(greatest(coalesce(m.fust_height_mm / 10.0, 0), coalesce(m.plant_height_cm, 0))) INTO v_lh
37099
+ FROM public.logistics_product_maten((SELECT array_agg(DISTINCT ci.product_id) FROM public.order_cart_items ci
37100
+ WHERE ci.cart_id = p_cart_id AND ci.product_id IS NOT NULL)) m
37101
+ WHERE m.fust_height_mm IS NOT NULL AND m.plant_height_cm IS NOT NULL;
37102
+ v_lagen := coalesce(v_cart.total_layers_used,
37103
+ (SELECT sum(ci.crates::numeric / nullif(ci.crates_per_layer, 0)) FROM public.order_cart_items ci WHERE ci.cart_id = p_cart_id));
37104
+ IF v_lh IS NOT NULL AND v_lh > 0 AND v_lagen IS NOT NULL
37105
+ AND ceil(v_lagen - 0.001) > floor(v_max * (1 + public.logistics_laag_tolerantie()) / v_lh) THEN
37106
+ RETURN jsonb_build_object('reden', 'te_veel_lagen', 'tekst',
37107
+ format('%s lagen van %s cm passen niet in %s cm; maximaal %s lagen.', trim_scale(round(v_lagen, 1)), trim_scale(round(v_lh, 1)), v_max,
37108
+ floor(v_max * (1 + public.logistics_laag_tolerantie()) / v_lh)));
37109
+ END IF;
37110
+ END IF;
37111
+ RETURN NULL;
37112
+ END $function$;
37113
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_regels_verouderd_core(uuid) TO service_role;
37114
+
37115
+ -- ===== 1.0.215 =====
37116
+
37117
+ CREATE OR REPLACE FUNCTION public.logistics_cart_verouderd_core(p_cart_id uuid)
37118
+ RETURNS jsonb LANGUAGE plpgsql STABLE SET search_path TO 'public'
37119
+ AS $function$
37120
+ DECLARE v_nr text; v_n int; v_live int; v_afw text[]; v_r jsonb;
37121
+ BEGIN
37122
+ IF p_cart_id IS NULL THEN RETURN NULL; END IF;
37123
+ SELECT cart_number::text INTO v_nr FROM public.order_carts WHERE id = p_cart_id;
37124
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'order_items' AND column_name = 'quarantined_at') THEN
37125
+ SELECT count(*) INTO v_n FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
37126
+ WHERE ci.cart_id = p_cart_id AND oi.quarantined_at IS NOT NULL;
37127
+ IF coalesce(v_n, 0) > 0 THEN
37128
+ RETURN jsonb_build_object('verouderd', true, 'reden', 'quarantaine', 'regelsInQuarantaine', v_n,
37129
+ 'tekst', format('Kar %s heeft %s regel(s) in quarantaine.', coalesce(v_nr, '?'), v_n),
37130
+ 'blokkade', jsonb_build_object('code', 'kar_verouderd',
37131
+ '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)));
37132
+ END IF;
37133
+ SELECT count(*) INTO v_live FROM public.order_cart_items ci LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
37134
+ WHERE ci.cart_id = p_cart_id AND coalesce(ci.crates, 0) > 0 AND oi.quarantined_at IS NULL;
37135
+ ELSE
37136
+ SELECT count(*) INTO v_live FROM public.order_cart_items ci WHERE ci.cart_id = p_cart_id AND coalesce(ci.crates, 0) > 0;
37137
+ END IF;
37138
+ IF coalesce(v_live, 0) = 0 THEN
37139
+ RETURN jsonb_build_object('verouderd', true, 'reden', 'leeg', 'tekst', format('Kar %s heeft geen regels meer.', coalesce(v_nr, '?')),
37140
+ 'blokkade', jsonb_build_object('code', 'kar_leeg',
37141
+ 'tekst', format('Kar %s heeft geen regels meer. Deel de niet-geprinte karren opnieuw in.', coalesce(v_nr, '?'))));
37142
+ END IF;
37143
+ v_afw := public.logistics_cart_aantal_afwijking_core(p_cart_id);
37144
+ IF coalesce(cardinality(v_afw), 0) > 0 THEN
37145
+ RETURN jsonb_build_object('verouderd', true, 'reden', 'aantal', 'producten', to_jsonb(v_afw),
37146
+ 'tekst', format('De aantallen wijken af van de order (%s).', array_to_string(v_afw, ', ')),
37147
+ 'blokkade', jsonb_build_object('code', 'kar_verouderd',
37148
+ 'tekst', format('Kar %s: de aantallen wijken af van de order (%s). Deel de niet-geprinte karren opnieuw in.', coalesce(v_nr, '?'), array_to_string(v_afw, ', '))));
37149
+ END IF;
37150
+ -- 1.0.215: huidige regels zouden de kar anders indelen → alleen melden, geen blokkade.
37151
+ v_r := public.logistics_cart_regels_verouderd_core(p_cart_id);
37152
+ IF v_r IS NOT NULL THEN
37153
+ RETURN jsonb_build_object('verouderd', true, 'reden', v_r->>'reden', 'tekst', v_r->>'tekst', 'blokkade', NULL);
37154
+ END IF;
37155
+ RETURN NULL;
37156
+ END $function$;
37157
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_verouderd_core(uuid) TO service_role;
37158
+
37159
+ DO $do$
37160
+ DECLARE d text; n text;
37161
+ BEGIN
37162
+ d := pg_get_functiondef('public.logistics_cart_platen_verrijk(jsonb)'::regprocedure);
37163
+ IF position('verouderdReden' IN d) = 0 THEN
37164
+ n := replace(d,
37165
+ $a$THEN jsonb_build_object('teHoog', false, 'indelingVerouderd', true, 'blokkade', public.logistics_cart_verouderd_core(v_id)->'blokkade')$a$,
37166
+ $a$THEN jsonb_build_object('teHoog', false, 'indelingVerouderd', true, 'blokkade', public.logistics_cart_verouderd_core(v_id)->'blokkade',
37167
+ 'verouderdReden', public.logistics_cart_verouderd_core(v_id)->>'tekst')$a$);
37168
+ IF n = d THEN RAISE EXCEPTION 'patroon platen_verrijk niet gevonden'; END IF;
37169
+ EXECUTE n;
37170
+ END IF;
37171
+ END $do$;
37172
+
37173
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_215()
37174
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
37175
+ SELECT 'hoofdverpakking via voorraadartikel', position('floriday_trade_item_id' IN pg_get_functiondef('public.logistics_product_hoofdverpakking(uuid)'::regprocedure)) > 0,
37176
+ 'Eigen artikel: belading van het voorraadartikel (products.floriday_trade_item_id)'
37177
+ UNION ALL SELECT 'gedeelde ladingdragerkeuze', to_regprocedure('public.logistics_ladingdrager_kies(jsonb)') IS NOT NULL,
37178
+ 'Orderregel → artikel → voorraadartikel → Floriday-verpakking → werkwijze met waarschuwing'
37179
+ UNION ALL SELECT 'kar verouderd volgens regels', position('logistics_cart_regels_verouderd_core' IN pg_get_functiondef('public.logistics_cart_verouderd_core(uuid)'::regprocedure)) > 0,
37180
+ 'Geen/andere ladingdrager of te veel lagen → verouderd: opnieuw indelen (geen blokkade)'
37181
+ UNION ALL SELECT 'reden op kar', position('verouderdReden' IN pg_get_functiondef('public.logistics_cart_platen_verrijk(jsonb)'::regprocedure)) > 0, 'Reden zichtbaar per kar'
37182
+ UNION ALL SELECT 'guard', position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_ladingdrager_kies(jsonb)'::regprocedure)) > 0, 'Keuze controleert de aanroeper'
37183
+ $$;
37184
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_215() TO authenticated, service_role;
37185
+
37186
+ -- ===================== 1.0.217 =====================
37187
+
37188
+ -- Hulp: één ref → canoniek artikel (Voorraad), fouten als reden (geen stille keuze).
37189
+ CREATE OR REPLACE FUNCTION public.logistics_canon_ref(p_ref text)
37190
+ RETURNS TABLE(pid uuid, tkey text, reden text)
37191
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $function$
37192
+ BEGIN
37193
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_canon_ref');
37194
+ pid := NULL; tkey := NULL; reden := NULL;
37195
+ IF p_ref IS NULL OR btrim(p_ref) = '' THEN reden := 'artikel onbekend'; RETURN NEXT; RETURN; END IF;
37196
+ BEGIN
37197
+ SELECT c.product_id, c.trade_item_key INTO pid, tkey FROM public.voorraad_canoniek_artikel(p_ref::text) c LIMIT 1;
37198
+ EXCEPTION WHEN SQLSTATE 'VR010' THEN reden := SQLERRM;
37199
+ WHEN others THEN reden := 'artikel niet te herleiden: ' || SQLERRM;
37200
+ END;
37201
+ IF reden IS NULL AND tkey IS NULL THEN reden := 'artikel onbekend'; END IF;
37202
+ RETURN NEXT;
37203
+ END $function$;
37204
+ GRANT EXECUTE ON FUNCTION public.logistics_canon_ref(text) TO authenticated, service_role;
37205
+
37206
+ -- 1. Voorraadcontrole nieuwe order: canoniek artikel (Voorraad) + trade_item_stock (fysiek − gereserveerd).
37207
+ CREATE OR REPLACE FUNCTION public.logistics_check_order_allowed(p_lines jsonb, p_strategy_id uuid DEFAULT NULL::uuid)
37208
+ RETURNS jsonb
37209
+ LANGUAGE plpgsql
37210
+ STABLE SECURITY DEFINER
37211
+ SET search_path TO 'public'
37212
+ AS $function$
37213
+ DECLARE
37214
+ v_allow boolean := public.logistics_backorder_allowed(p_strategy_id);
37215
+ v_canon boolean := to_regprocedure('public.voorraad_canoniek_artikel(text)') IS NOT NULL;
37216
+ v_shortages jsonb := '[]'::jsonb;
37217
+ r record;
37218
+ BEGIN
37219
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_check_order_allowed');
37220
+ IF p_lines IS NULL OR jsonb_typeof(p_lines) <> 'array' THEN
37221
+ RETURN jsonb_build_object('allowed', true, 'allowBackorder', v_allow, 'shortages', '[]'::jsonb);
37222
+ END IF;
37223
+
37224
+ IF NOT v_canon THEN
37225
+ -- Terugval zonder Voorraad-module: oude regel, bron zichtbaar.
37226
+ SELECT COALESCE(jsonb_agg(s ORDER BY s->>'product'), '[]'::jsonb) INTO v_shortages
37227
+ FROM (
37228
+ SELECT jsonb_build_object('productId', p.id, 'product', COALESCE(p.product, l2.ref),
37229
+ 'wanted', l2.wanted, 'available', COALESCE(p.quantity, 0),
37230
+ 'missing', l2.wanted - COALESCE(p.quantity, 0), 'bron', 'products.quantity') AS s
37231
+ FROM (SELECT COALESCE(e->>'productId', e->>'product_id', e->>'productRef', e->>'ref') AS ref,
37232
+ SUM(COALESCE((e->>'quantity')::numeric, 0)) AS wanted
37233
+ FROM jsonb_array_elements(p_lines) e GROUP BY 1) l2
37234
+ LEFT JOIN LATERAL (SELECT * FROM public.products p
37235
+ WHERE p.id = public.logistics_try_uuid(l2.ref) OR p.article_number = l2.ref
37236
+ OR p.barcode = l2.ref OR p.floriday_trade_item_id = l2.ref OR p.trade_item_key = l2.ref
37237
+ ORDER BY (p.id = public.logistics_try_uuid(l2.ref)) DESC NULLS LAST LIMIT 1) p ON true
37238
+ WHERE l2.wanted > COALESCE(p.quantity, 0)) q;
37239
+ ELSE
37240
+ FOR r IN
37241
+ SELECT min(a.ref) AS ref, (array_agg(a.pid))[1] AS pid, a.tkey, min(a.reden) AS reden, SUM(a.wanted) AS wanted,
37242
+ (SELECT GREATEST(COALESCE(SUM(s.physical_stock), 0) - COALESCE(SUM(s.reserved_stock), 0), 0)
37243
+ FROM public.trade_item_stock s WHERE s.trade_item_key = a.tkey AND s.archived_at IS NULL) AS available
37244
+ FROM (SELECT l2.ref, c.pid, c.tkey, c.reden, l2.wanted
37245
+ FROM (SELECT COALESCE(e->>'productId', e->>'product_id', e->>'productRef', e->>'ref') AS ref,
37246
+ SUM(COALESCE((e->>'quantity')::numeric, 0)) AS wanted
37247
+ FROM jsonb_array_elements(p_lines) e GROUP BY 1) l2,
37248
+ LATERAL public.logistics_canon_ref(l2.ref) c) a GROUP BY a.tkey, CASE WHEN a.tkey IS NULL THEN a.ref END
37249
+ LOOP
37250
+ IF r.reden IS NOT NULL OR r.wanted > COALESCE(r.available, 0) THEN
37251
+ v_shortages := v_shortages || jsonb_build_object(
37252
+ 'productId', r.pid,
37253
+ 'product', COALESCE((SELECT p.product FROM public.products p WHERE p.id = r.pid), r.ref),
37254
+ 'wanted', r.wanted,
37255
+ 'available', CASE WHEN r.reden IS NULL THEN COALESCE(r.available, 0) ELSE 0 END,
37256
+ 'missing', r.wanted - CASE WHEN r.reden IS NULL THEN COALESCE(r.available, 0) ELSE 0 END,
37257
+ 'tradeItemKey', r.tkey,
37258
+ 'bron', 'trade_item_stock (physical_stock − reserved_stock), canoniek artikel',
37259
+ 'reden', r.reden);
37260
+ END IF;
37261
+ END LOOP;
37262
+ END IF;
37263
+
37264
+ RETURN jsonb_build_object(
37265
+ 'allowed', v_allow OR jsonb_array_length(v_shortages) = 0,
37266
+ 'allowBackorder', v_allow,
37267
+ 'shortages', v_shortages,
37268
+ 'bron', CASE WHEN v_canon THEN 'voorraad_canoniek_artikel' ELSE 'products.quantity (Voorraad-module ontbreekt)' END,
37269
+ 'reason', CASE WHEN NOT v_allow AND jsonb_array_length(v_shortages) > 0 THEN 'insufficient_stock' ELSE NULL END);
37270
+ END;
37271
+ $function$;
37272
+
37273
+ -- 2. Reserveringssleutel: zelfde regel als Voorraad — voorraad_canoniek_artikel(ref::text).trade_item_key.
37274
+ CREATE OR REPLACE FUNCTION public.logistics_reserved_key(p_key text)
37275
+ RETURNS text
37276
+ LANGUAGE plpgsql
37277
+ SECURITY DEFINER
37278
+ SET search_path TO 'public'
37279
+ AS $function$
37280
+ DECLARE
37281
+ v_raw text;
37282
+ v_key text;
37283
+ v_pid uuid;
37284
+ BEGIN
37285
+ PERFORM public.logistics_acc_guard('server', 'logistics_reserved_key');
37286
+ IF p_key IS NULL OR btrim(p_key) = '' THEN RETURN NULL; END IF;
37287
+ v_raw := btrim(p_key);
37288
+
37289
+ IF to_regprocedure('public.voorraad_canoniek_artikel(text)') IS NOT NULL THEN
37290
+ BEGIN
37291
+ SELECT c.trade_item_key INTO v_key FROM public.voorraad_canoniek_artikel(v_raw::text) c LIMIT 1;
37292
+ EXCEPTION
37293
+ WHEN SQLSTATE 'VR010' THEN RETURN NULL; -- meerdere artikelen: niets kiezen (fail-closed)
37294
+ WHEN others THEN v_key := NULL;
37295
+ END;
37296
+ IF v_key IS NOT NULL AND EXISTS (SELECT 1 FROM public.trade_item_stock s
37297
+ WHERE s.trade_item_key = v_key AND s.archived_at IS NULL) THEN
37298
+ RETURN v_key;
37299
+ END IF;
37300
+ v_key := NULL;
37301
+ END IF;
37302
+
37303
+ -- Terugval: regel van vóór 1.0.217.
37304
+ v_key := public.ensure_trade_item_stock_row_key(v_raw);
37305
+ IF v_key IS NOT NULL THEN RETURN v_key; END IF;
37306
+
37307
+ BEGIN
37308
+ v_pid := CASE WHEN v_raw LIKE 'product:%' THEN substring(v_raw from 9)::uuid ELSE v_raw::uuid END;
37309
+ EXCEPTION WHEN others THEN
37310
+ v_pid := NULL;
37311
+ END;
37312
+
37313
+ IF v_pid IS NOT NULL THEN
37314
+ SELECT COALESCE(NULLIF(btrim(COALESCE(p.floriday_trade_item_id, '')), ''),
37315
+ NULLIF(btrim(COALESCE(p.trade_item_key, '')), ''), p.id::text)
37316
+ INTO v_key FROM public.products p WHERE p.id = v_pid LIMIT 1;
37317
+ ELSE
37318
+ SELECT COALESCE(NULLIF(btrim(COALESCE(p.floriday_trade_item_id, '')), ''),
37319
+ NULLIF(btrim(COALESCE(p.trade_item_key, '')), ''), p.id::text)
37320
+ INTO v_key FROM public.products p
37321
+ WHERE p.floriday_trade_item_id = v_raw OR p.trade_item_key = v_raw LIMIT 1;
37322
+ END IF;
37323
+ v_key := COALESCE(v_key, v_raw);
37324
+
37325
+ IF EXISTS (SELECT 1 FROM public.trade_item_stock s WHERE s.trade_item_key = v_key) THEN
37326
+ RETURN v_key;
37327
+ END IF;
37328
+ RETURN NULL;
37329
+ END
37330
+ $function$;
37331
+
37332
+ -- 3. Dagtelling: unieke fysieke karren per dag; orders zonder kar apart.
37333
+ CREATE OR REPLACE FUNCTION public.logistics_capacity_day_load(p_from date, p_to date, p_user uuid, p_settings jsonb)
37334
+ RETURNS TABLE(d date, carts integer, required numeric, zonder_kar integer)
37335
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
37336
+ WITH t AS (
37337
+ SELECT DISTINCT ON (t.order_ref) t.order_ref, (t.planned_start AT TIME ZONE public.logistics_tz())::date AS d
37338
+ FROM public.logistics_tasks t
37339
+ WHERE t.task_type = 'pick.execute' AND t.status NOT IN ('done','cancelled') AND NOT t.shadow
37340
+ AND t.planned_start IS NOT NULL AND (p_user IS NULL OR t.user_id = p_user)
37341
+ AND (t.planned_start AT TIME ZONE public.logistics_tz())::date BETWEEN p_from AND p_to
37342
+ ORDER BY t.order_ref, t.planned_start
37343
+ ),
37344
+ o AS (SELECT t.d, t.order_ref, o.id FROM t JOIN public.orders o ON o.order_number = t.order_ref AND o.deleted_at IS NULL),
37345
+ k AS (
37346
+ SELECT DISTINCT ON (u.cart_id) u.cart_id, o.d
37347
+ FROM o, LATERAL public.logistics_order_unit_carts_core(ARRAY[o.id]) u
37348
+ ORDER BY u.cart_id, o.d
37349
+ ),
37350
+ kc AS (
37351
+ SELECT k.d, count(*)::int AS carts,
37352
+ SUM(public.logistics_carrier_minutes(COALESCE(c.carrier_type, 'trolley'), p_settings)) AS mins
37353
+ FROM k JOIN public.order_carts c ON c.id = k.cart_id GROUP BY k.d
37354
+ ),
37355
+ zk AS (
37356
+ SELECT t.d, count(*)::int AS n FROM t
37357
+ WHERE NOT EXISTS (SELECT 1 FROM o JOIN LATERAL public.logistics_order_unit_carts_core(ARRAY[o.id]) u ON true
37358
+ WHERE o.order_ref = t.order_ref)
37359
+ GROUP BY t.d
37360
+ ),
37361
+ dd AS (SELECT DISTINCT t.d FROM t)
37362
+ SELECT dd.d, COALESCE(kc.carts, 0), COALESCE(NULLIF(kc.mins, 0), COALESCE(kc.carts, 0) * 15)::numeric, COALESCE(zk.n, 0)
37363
+ FROM dd LEFT JOIN kc ON kc.d = dd.d LEFT JOIN zk ON zk.d = dd.d
37364
+ ORDER BY dd.d;
37365
+ $$;
37366
+ GRANT EXECUTE ON FUNCTION public.logistics_capacity_day_load(date, date, uuid, jsonb) TO authenticated, service_role;
37367
+
37368
+ DO $do$
37369
+ DECLARE d text; n text;
37370
+ BEGIN
37371
+ d := pg_get_functiondef('public.logistics_capacity_day_check(date,date)'::regprocedure);
37372
+ IF position('logistics_capacity_day_load' IN d) = 0 THEN
37373
+ n := regexp_replace(d, 'FOR v_day IN\s+SELECT d,.*?ORDER BY d\s+LOOP',
37374
+ 'FOR v_day IN SELECT * FROM public.logistics_capacity_day_load(p_from, v_to, v_user, v_settings) LOOP', 's');
37375
+ IF n = d THEN RAISE EXCEPTION 'patroon capacity_day_check (lus) niet gevonden'; END IF;
37376
+ d := n;
37377
+ n := replace(d, $a$'carts', v_day.carts,$a$, $a$'carts', v_day.carts,
37378
+ 'ordersZonderKar', v_day.zonder_kar,$a$);
37379
+ IF n = d THEN RAISE EXCEPTION 'patroon capacity_day_check (carts) niet gevonden'; END IF;
37380
+ EXECUTE n;
37381
+ END IF;
37382
+ END $do$;
37383
+
37384
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_217()
37385
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
37386
+ SELECT 'Voorraad canoniek artikel aanwezig', to_regprocedure('public.voorraad_canoniek_artikel(text)') IS NOT NULL,
37387
+ 'Zonder deze functie valt de Engine terug op de oude regel (bron zichtbaar in het antwoord)'
37388
+ UNION ALL SELECT 'voorraadcontrole canoniek', position('voorraad_canoniek_artikel' IN pg_get_functiondef('public.logistics_check_order_allowed(jsonb,uuid)'::regprocedure)) > 0,
37389
+ 'Beschikbaar = physical_stock − reserved_stock (archived_at IS NULL) van de canonieke sleutel'
37390
+ UNION ALL SELECT 'reserveringssleutel canoniek', position('voorraad_canoniek_artikel' IN pg_get_functiondef('public.logistics_reserved_key(text)'::regprocedure)) > 0,
37391
+ 'voorraad_canoniek_artikel(ref::text).trade_item_key, anders oude regel'
37392
+ UNION ALL SELECT 'dagtelling unieke karren', position('logistics_capacity_day_load' IN pg_get_functiondef('public.logistics_capacity_day_check(date,date)'::regprocedure)) > 0,
37393
+ 'Gedeelde kar telt één keer; orders zonder kar apart (ordersZonderKar)'
37394
+ UNION ALL SELECT 'guards', position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_check_order_allowed(jsonb,uuid)'::regprocedure)) > 0
37395
+ AND position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_reserved_key(text)'::regprocedure)) > 0,
37396
+ 'Aanroeper wordt intern gecontroleerd'
37397
+ $$;
37398
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_217() TO authenticated, service_role;
37399
+
37400
+
37401
+ -- ===== 1.0.218 =====
37402
+ -- ===== 1.0.218 eigen vervoer / klant haalt op =====
37403
+ DO $fs$ BEGIN
37404
+ IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_carrier_source_check')
37405
+ AND position('eigen_vervoer' IN (SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'orders_carrier_source_check' LIMIT 1)) = 0 THEN
37406
+ ALTER TABLE public.orders DROP CONSTRAINT orders_carrier_source_check;
37407
+ END IF;
37408
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'orders_carrier_source_check') THEN
37409
+ ALTER TABLE public.orders ADD CONSTRAINT orders_carrier_source_check CHECK (carrier_source IS NULL OR carrier_source IN
37410
+ ('handmatig','afleverlocatie','afleverlocatie_gln','klant','werkgebied','favoriet','geen','oud','eigen_vervoer')) NOT VALID;
37411
+ END IF;
37412
+ END $fs$;
37413
+
37414
+ CREATE OR REPLACE FUNCTION public.logistics_order_own_transport(p_custom_fields jsonb)
37415
+ RETURNS boolean LANGUAGE sql IMMUTABLE SET search_path TO 'public'
37416
+ AS $function$
37417
+ SELECT CASE jsonb_typeof(p_custom_fields->'own_transport')
37418
+ WHEN 'object' THEN p_custom_fields->'own_transport' <> '{}'::jsonb
37419
+ WHEN 'boolean' THEN (p_custom_fields->>'own_transport')::boolean
37420
+ WHEN 'string' THEN coalesce(nullif(btrim(lower(p_custom_fields->>'own_transport')), ''), 'false') NOT IN ('false','0','nee','no')
37421
+ WHEN 'number' THEN (p_custom_fields->>'own_transport')::numeric <> 0
37422
+ WHEN 'array' THEN jsonb_array_length(p_custom_fields->'own_transport') > 0
37423
+ ELSE false END
37424
+ $function$;
37425
+ GRANT EXECUTE ON FUNCTION public.logistics_order_own_transport(jsonb) TO authenticated, service_role;
37426
+
37427
+ -- Soort vertrek van een order (orders-rij als jsonb): extern | eigen_vervoer | klant_haalt_op.
37428
+ CREATE OR REPLACE FUNCTION public.logistics_order_transport_group(p_order jsonb)
37429
+ RETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
37430
+ AS $function$
37431
+ DECLARE cf jsonb := CASE WHEN jsonb_typeof(p_order->'custom_fields') = 'object' THEN p_order->'custom_fields' ELSE '{}'::jsonb END;
37432
+ v_id uuid; v_kind text;
37433
+ BEGIN
37434
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_transport_group');
37435
+ IF public.logistics_order_own_transport(cf) THEN RETURN 'eigen_vervoer'; END IF;
37436
+ v_id := public.logistics_try_uuid(p_order->>'carrier_id');
37437
+ IF v_id IS NOT NULL THEN
37438
+ SELECT to_jsonb(c)->>'carrier_kind' INTO v_kind FROM public.carriers c WHERE c.id = v_id;
37439
+ IF v_kind IN ('eigen_vervoer', 'klant_haalt_op') THEN RETURN v_kind; END IF;
37440
+ RETURN 'extern';
37441
+ END IF;
37442
+ -- Zonder vervoerder maar met een bron (afleverlocatie self_pickup) = klant haalt op.
37443
+ IF coalesce(p_order->>'carrier_source', 'geen') NOT IN ('geen', 'eigen_vervoer') THEN RETURN 'klant_haalt_op'; END IF;
37444
+ RETURN 'extern';
37445
+ END $function$;
37446
+ GRANT EXECUTE ON FUNCTION public.logistics_order_transport_group(jsonb) TO authenticated, service_role;
37447
+
37448
+ -- Resolverstap eigen vervoer: NULL = niet van toepassing.
37449
+ CREATE OR REPLACE FUNCTION public.logistics_own_transport_carrier(p_order jsonb, p_skip_manual boolean)
37450
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
37451
+ AS $function$
37452
+ DECLARE cf jsonb := CASE WHEN jsonb_typeof(p_order->'custom_fields') = 'object' THEN p_order->'custom_fields' ELSE '{}'::jsonb END;
37453
+ v_id uuid; v_kind text; v_skips text[] := '{}'::text[]; v_ids uuid[]; v_reden text;
37454
+ BEGIN
37455
+ PERFORM public.logistics_acc_guard('server', 'logistics_own_transport_carrier');
37456
+ IF NOT public.logistics_order_own_transport(cf) THEN RETURN NULL; END IF;
37457
+ v_id := public.logistics_try_uuid(p_order->>'carrier_id');
37458
+ IF v_id IS NOT NULL AND NOT p_skip_manual AND p_order->>'carrier_source' = 'handmatig' THEN
37459
+ SELECT to_jsonb(c)->>'carrier_kind' INTO v_kind FROM public.carriers c WHERE c.id = v_id;
37460
+ IF v_kind = 'eigen_vervoer' AND public.logistics_carrier_skip_reason(v_id) IS NULL THEN
37461
+ RETURN public.logistics_carrier_result(v_id, 'handmatig', 'Handmatig gekozen: eigen vervoer.', v_skips);
37462
+ END IF;
37463
+ v_skips := v_skips || 'handmatige vervoerder is geen eigen vervoer'::text;
37464
+ END IF;
37465
+ SELECT array_agg(c.id ORDER BY c.id) INTO v_ids FROM public.carriers c
37466
+ WHERE to_jsonb(c)->>'carrier_kind' = 'eigen_vervoer' AND nullif(to_jsonb(c)->>'archived_at', '') IS NULL;
37467
+ IF coalesce(cardinality(v_ids), 0) = 1 THEN
37468
+ RETURN public.logistics_carrier_result(v_ids[1], 'eigen_vervoer', 'Eigen vervoer op de order; vervoerder met soort eigen vervoer.', v_skips);
37469
+ END IF;
37470
+ v_reden := CASE WHEN coalesce(cardinality(v_ids), 0) = 0
37471
+ THEN 'Eigen vervoer op de order, maar er is geen vervoerder met soort eigen vervoer. Er is geen andere vervoerder gekozen.'
37472
+ ELSE 'Eigen vervoer op de order, maar er zijn ' || cardinality(v_ids) || ' vervoerders met soort eigen vervoer. Er is geen vervoerder gekozen.' END;
37473
+ RETURN jsonb_build_object('carrier_id', NULL, 'carrier_name', 'Eigen vervoer', 'carrier_kind', 'eigen_vervoer',
37474
+ 'bron', 'geen', 'reden', v_reden, 'waarschuwing', v_reden, 'overgeslagen', to_jsonb(v_skips));
37475
+ END $function$;
37476
+ REVOKE ALL ON FUNCTION public.logistics_own_transport_carrier(jsonb, boolean) FROM PUBLIC, anon;
37477
+ GRANT EXECUTE ON FUNCTION public.logistics_own_transport_carrier(jsonb, boolean) TO authenticated, service_role;
37478
+
37479
+ -- Bestaande functies gericht aanvullen (luid falen als het patroon ontbreekt).
37480
+ DO $do$
37481
+ DECLARE d text; n text; f record; v_hit int := 0;
37482
+ BEGIN
37483
+ -- resolver
37484
+ d := pg_get_functiondef('public.logistics_resolve_carrier_core(jsonb,boolean)'::regprocedure);
37485
+ IF position('logistics_own_transport_carrier' IN d) = 0 THEN
37486
+ n := replace(d, $p$PERFORM public.logistics_acc_guard('server', 'logistics_resolve_carrier_core');$p$,
37487
+ $p$PERFORM public.logistics_acc_guard('server', 'logistics_resolve_carrier_core');
37488
+ IF public.logistics_own_transport_carrier(p_order, p_skip_manual) IS NOT NULL THEN
37489
+ RETURN public.logistics_own_transport_carrier(p_order, p_skip_manual);
37490
+ END IF;$p$);
37491
+ IF n = d THEN RAISE EXCEPTION '1.0.218: patroon resolver niet gevonden'; END IF;
37492
+ EXECUTE n;
37493
+ END IF;
37494
+ -- set_order_carrier: externe vervoerder bij eigen vervoer weigeren
37495
+ d := pg_get_functiondef('public.logistics_set_order_carrier(uuid,uuid,uuid)'::regprocedure);
37496
+ IF position('logistics_order_own_transport' IN d) = 0 THEN
37497
+ n := replace(d, $p$ IF v_skip IS NOT NULL THEN RAISE EXCEPTION 'Deze vervoerder kan niet gekozen worden: %.', v_skip; END IF;$p$,
37498
+ $p$ IF v_skip IS NOT NULL THEN RAISE EXCEPTION 'Deze vervoerder kan niet gekozen worden: %.', v_skip; END IF;
37499
+ IF public.logistics_order_own_transport(o->'custom_fields')
37500
+ AND coalesce((SELECT to_jsonb(ca)->>'carrier_kind' FROM public.carriers ca WHERE ca.id = p_carrier_id), '') <> 'eigen_vervoer' THEN
37501
+ RAISE EXCEPTION 'Deze order heeft eigen vervoer; kies een vervoerder met soort eigen vervoer.' USING ERRCODE = '22023';
37502
+ END IF;$p$);
37503
+ IF n = d THEN RAISE EXCEPTION '1.0.218: patroon set_order_carrier niet gevonden'; END IF;
37504
+ EXECUTE n;
37505
+ END IF;
37506
+ -- trigger: own_transport-wijziging herberekent, ook na handmatige externe keuze
37507
+ d := pg_get_functiondef('public.logistics_orders_carrier_fill()'::regprocedure);
37508
+ IF position('own_transport' IN d) = 0 THEN
37509
+ n := replace(d, $p$ IF OLD.carrier_source = 'handmatig' THEN RETURN NEW; END IF;$p$,
37510
+ $p$ IF OLD.carrier_source = 'handmatig'
37511
+ AND (NEW.custom_fields->'own_transport') IS NOT DISTINCT FROM (OLD.custom_fields->'own_transport') THEN RETURN NEW; END IF;$p$);
37512
+ n := replace(n, $p$ AND (NEW.custom_fields->>'carrier') IS NOT DISTINCT FROM (OLD.custom_fields->>'carrier') THEN$p$,
37513
+ $p$ AND (NEW.custom_fields->>'carrier') IS NOT DISTINCT FROM (OLD.custom_fields->>'carrier')
37514
+ AND (NEW.custom_fields->'own_transport') IS NOT DISTINCT FROM (OLD.custom_fields->'own_transport') THEN$p$);
37515
+ IF position('own_transport' IN n) = 0 THEN RAISE EXCEPTION '1.0.218: patroon carrier_fill niet gevonden'; END IF;
37516
+ EXECUTE n;
37517
+ END IF;
37518
+ -- bufferpayload: soort vertrek meegeven
37519
+ d := pg_get_functiondef('public.logistics_buffer_payload(uuid)'::regprocedure);
37520
+ IF position('transportGroup' IN d) = 0 THEN
37521
+ n := replace(d, $p$'isReservation', COALESCE(o.is_reservation, false)$p$,
37522
+ $p$'isReservation', COALESCE(o.is_reservation, false),
37523
+ 'transportGroup', public.logistics_order_transport_group(to_jsonb(o))$p$);
37524
+ IF n = d THEN RAISE EXCEPTION '1.0.218: patroon buffer_payload niet gevonden'; END IF;
37525
+ EXECUTE n;
37526
+ END IF;
37527
+ -- buffersleutel: niet-extern altijd apart (extern ongewijzigd = bestaande sleutels blijven gelijk)
37528
+ FOR f IN SELECT p.oid FROM pg_proc p JOIN pg_namespace s ON s.oid = p.pronamespace
37529
+ WHERE s.nspname = 'public' AND p.proname LIKE 'logistics_buffer_decision%' LOOP
37530
+ d := pg_get_functiondef(f.oid);
37531
+ IF position('v_parts := ARRAY(SELECT DISTINCT ON (p) p FROM unnest(v_parts)' IN d) > 0 THEN
37532
+ v_hit := v_hit + 1;
37533
+ IF position('transport_group' IN d) = 0 THEN
37534
+ n := replace(d, $p$ v_parts := ARRAY(SELECT DISTINCT ON (p) p FROM unnest(v_parts)$p$,
37535
+ $p$ IF coalesce(p_order ->> 'transportGroup', 'extern') IN ('eigen_vervoer', 'klant_haalt_op') THEN
37536
+ v_parts := v_parts || ('vervoer=' || (p_order ->> 'transportGroup'));
37537
+ v_trace := v_trace || jsonb_build_array(jsonb_build_object('rule', 'transport_group', 'value', p_order ->> 'transportGroup', 'result', true,
37538
+ 'message', CASE p_order ->> 'transportGroup' WHEN 'eigen_vervoer' THEN 'Eigen vervoer wordt alleen samen gebufferd met andere orders met eigen vervoer.'
37539
+ ELSE 'Klant haalt op wordt alleen samen gebufferd met andere orders die de klant ophaalt.' END));
37540
+ END IF;
37541
+ v_parts := ARRAY(SELECT DISTINCT ON (p) p FROM unnest(v_parts)$p$);
37542
+ EXECUTE n;
37543
+ END IF;
37544
+ END IF;
37545
+ END LOOP;
37546
+ IF v_hit = 0 THEN RAISE EXCEPTION '1.0.218: buffersleutel niet gevonden'; END IF;
37547
+ -- verouderd: gemengde soorten op één kar (alleen melding)
37548
+ d := pg_get_functiondef('public.logistics_cart_verouderd_core(uuid)'::regprocedure);
37549
+ IF position('logistics_cart_vervoer_gemengd' IN d) = 0 THEN
37550
+ n := replace(d, $p$ v_r := public.logistics_cart_regels_verouderd_core(p_cart_id);$p$,
37551
+ $p$ v_r := public.logistics_cart_vervoer_gemengd(p_cart_id);
37552
+ IF v_r IS NOT NULL THEN
37553
+ RETURN jsonb_build_object('verouderd', true, 'reden', 'vervoer_gemengd', 'tekst', v_r->>'tekst', 'blokkade', NULL);
37554
+ END IF;
37555
+ v_r := public.logistics_cart_regels_verouderd_core(p_cart_id);$p$);
37556
+ IF n = d THEN RAISE EXCEPTION '1.0.218: patroon cart_verouderd_core niet gevonden'; END IF;
37557
+ -- hulpfunctie eerst
37558
+ EXECUTE $h$CREATE OR REPLACE FUNCTION public.logistics_cart_vervoer_gemengd(p_cart_id uuid)
37559
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path TO 'public'
37560
+ AS $f$
37561
+ WITH k AS (SELECT oc.order_id, oc.buffer_id FROM public.order_carts oc WHERE oc.id = p_cart_id),
37562
+ o AS (SELECT DISTINCT x.id, x.order_number, public.logistics_order_transport_group(to_jsonb(x)) g
37563
+ FROM public.orders x, k
37564
+ WHERE x.deleted_at IS NULL AND (x.id = k.order_id OR (k.buffer_id IS NOT NULL AND x.buffer_id = k.buffer_id)))
37565
+ SELECT CASE WHEN count(DISTINCT g) > 1 THEN jsonb_build_object('soorten', jsonb_agg(DISTINCT g),
37566
+ 'tekst', 'Op deze kar staan verschillende soorten vertrek (' ||
37567
+ string_agg(order_number || ': ' || CASE g WHEN 'eigen_vervoer' THEN 'eigen vervoer' WHEN 'klant_haalt_op' THEN 'klant haalt op' ELSE 'vervoerder' END, ', ' ORDER BY order_number)
37568
+ || '); die worden niet samen gebufferd.') END
37569
+ FROM o
37570
+ $f$$h$;
37571
+ EXECUTE n;
37572
+ END IF;
37573
+ END $do$;
37574
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_vervoer_gemengd(uuid) TO service_role;
37575
+
37576
+ -- Voorbeeldberekening: reden bij soort vertrek.
37577
+ CREATE OR REPLACE FUNCTION public.logistics_rebuffer_simulate_open()
37578
+ RETURNS TABLE(id uuid, order_number text, delivery_date text, buffer_id uuid, cur_label text, new_label text, changed boolean, reason text)
37579
+ LANGUAGE plpgsql
37580
+ STABLE SECURITY DEFINER
37581
+ SET search_path TO 'public'
37582
+ AS $function$
37583
+ BEGIN
37584
+ PERFORM public.logistics_acc_guard('server', 'logistics_rebuffer_simulate_open');
37585
+ RETURN QUERY
37586
+ WITH c AS MATERIALIZED (
37587
+ SELECT o.id, o.order_number, o.delivery_date, o.buffer_id, public.logistics_buffer_target(o.id) AS t,
37588
+ public.logistics_order_afgemeld(o.id) AS afgemeld,
37589
+ (o.buffer_id IS NOT NULL AND coalesce(public.logistics_buffer_frozen(o.buffer_id), false)) AS buf_vast,
37590
+ public.logistics_order_transport_group(to_jsonb(o)) AS grp
37591
+ FROM public.orders o JOIN public.logistics_rebuffer_candidates() k ON k.order_id = o.id
37592
+ ), m AS (
37593
+ SELECT c.*,
37594
+ (c.afgemeld OR c.buf_vast) AS beschermd,
37595
+ (SELECT array_agg(x.id ORDER BY x.id) FROM public.orders x WHERE c.buffer_id IS NOT NULL AND x.buffer_id = c.buffer_id AND x.deleted_at IS NULL AND x.id <> c.id) AS cur_mates,
37596
+ (SELECT array_agg(x.id ORDER BY x.id) FROM public.orders x WHERE c.t->>'soort' = 'buffer' AND x.buffer_id = (c.t->>'bufferId')::uuid AND x.deleted_at IS NULL AND x.id <> c.id) AS new_mates,
37597
+ EXISTS (SELECT 1 FROM public.orders x WHERE c.buffer_id IS NOT NULL AND x.buffer_id = c.buffer_id AND x.deleted_at IS NULL AND x.id <> c.id
37598
+ AND public.logistics_order_transport_group(to_jsonb(x)) <> c.grp) AS gemengd
37599
+ FROM c
37600
+ ), l AS (
37601
+ SELECT m.*,
37602
+ CASE WHEN m.buffer_id IS NULL THEN 'Geen buffer (eigen kar)'
37603
+ WHEN m.cur_mates IS NULL THEN 'Eigen buffer'
37604
+ ELSE 'Buffer met ' || (SELECT string_agg(x.order_number, ', ' ORDER BY x.order_number) FROM public.orders x WHERE x.id = ANY(m.cur_mates)) END AS cur_l,
37605
+ CASE m.grp WHEN 'eigen_vervoer' THEN 'eigen vervoer' WHEN 'klant_haalt_op' THEN 'klant haalt op' ELSE 'vervoerder' END AS grp_t
37606
+ FROM m
37607
+ )
37608
+ SELECT l.id, l.order_number, l.delivery_date, l.buffer_id, l.cur_l,
37609
+ CASE WHEN l.beschermd THEN l.cur_l
37610
+ WHEN l.t->>'soort' IN ('solo','uitgesloten') THEN 'Geen buffer (eigen kar)'
37611
+ WHEN l.new_mates IS NULL THEN 'Eigen buffer'
37612
+ ELSE 'Buffer met ' || (SELECT string_agg(x.order_number, ', ' ORDER BY x.order_number) FROM public.orders x WHERE x.id = ANY(l.new_mates)) END,
37613
+ CASE WHEN l.beschermd THEN false
37614
+ ELSE coalesce(l.cur_mates, '{}') IS DISTINCT FROM coalesce(l.new_mates, '{}')
37615
+ OR ((l.t->>'soort') IN ('solo','uitgesloten') AND l.buffer_id IS NOT NULL AND l.cur_mates IS NOT NULL) END,
37616
+ CASE WHEN l.afgemeld THEN 'Ongewijzigd: de order is al verzonden of afgemeld; een bestaande buffer wordt niet gesplitst.'
37617
+ WHEN l.buf_vast THEN 'Ongewijzigd: de buffer ligt al vast (geprint, gescand of verstuurd) en wordt niet gesplitst.'
37618
+ || CASE WHEN l.gemengd THEN ' Let op: in deze buffer staan verschillende soorten vertrek (deze order: ' || l.grp_t || ').' ELSE '' END
37619
+ WHEN jsonb_array_length(coalesce(l.t->'overgeslagen','[]'::jsonb)) > 0
37620
+ THEN 'Niet samengevoegd: buffer van ' || (SELECT string_agg(v, '; ') FROM jsonb_array_elements_text(l.t->'overgeslagen') v)
37621
+ || ' ligt vast (geprint, gescand of verstuurd).'
37622
+ WHEN l.t->>'soort' = 'uitgesloten' THEN 'Deze order is uitgesloten van combineren (handmatig uitgesloten van indeling).'
37623
+ WHEN l.gemengd THEN 'Aparte kar: ' || l.grp_t || ' wordt niet samen gebufferd met orders met een ander soort vertrek (extern, eigen vervoer, klant haalt op).'
37624
+ WHEN l.t->>'soort' = 'solo' THEN 'Volgens de huidige combineerregels krijgt deze order een eigen kar (' || coalesce(l.t->>'reason','onbekend') || ').'
37625
+ WHEN l.grp <> 'extern' THEN 'Volgens de huidige combineerregels, alleen met orders met ' || l.grp_t || ' (' || coalesce(l.t->>'reason','onbekend') || ').'
37626
+ ELSE 'Volgens de huidige combineerregels (' || coalesce(l.t->>'reason','onbekend') || ').' END
37627
+ FROM l;
37628
+ END $function$;
37629
+
37630
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_218()
37631
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
37632
+ SELECT 'resolver eigen vervoer', position('logistics_own_transport_carrier' IN pg_get_functiondef('public.logistics_resolve_carrier_core(jsonb,boolean)'::regprocedure)) > 0,
37633
+ 'own_transport: vervoerder met soort eigen vervoer of geen; nooit favoriet/standaard'
37634
+ UNION ALL SELECT 'bron eigen_vervoer toegestaan', position('eigen_vervoer' IN coalesce((SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'orders_carrier_source_check' LIMIT 1), 'eigen_vervoer')) > 0,
37635
+ 'carrier_source mag eigen_vervoer zijn'
37636
+ UNION ALL SELECT 'externe keuze geweigerd', position('logistics_order_own_transport' IN pg_get_functiondef('public.logistics_set_order_carrier(uuid,uuid,uuid)'::regprocedure)) > 0,
37637
+ 'logistics_set_order_carrier weigert externe vervoerder bij eigen vervoer'
37638
+ UNION ALL SELECT 'trigger own_transport', position('own_transport' IN pg_get_functiondef('public.logistics_orders_carrier_fill()'::regprocedure)) > 0,
37639
+ 'Wijziging van own_transport herberekent de vervoerder'
37640
+ UNION ALL SELECT 'buffersleutel soort vertrek', position('transportGroup' IN pg_get_functiondef('public.logistics_buffer_payload(uuid)'::regprocedure)) > 0
37641
+ AND EXISTS (SELECT 1 FROM pg_proc p JOIN pg_namespace s ON s.oid = p.pronamespace WHERE s.nspname = 'public'
37642
+ AND p.proname LIKE 'logistics_buffer_decision%' AND position('transport_group' IN pg_get_functiondef(p.oid)) > 0),
37643
+ 'Extern, eigen vervoer en klant haalt op nooit samen gebufferd'
37644
+ UNION ALL SELECT 'verouderd bij gemengd vervoer', position('logistics_cart_vervoer_gemengd' IN pg_get_functiondef('public.logistics_cart_verouderd_core(uuid)'::regprocedure)) > 0,
37645
+ 'Kar met verschillende soorten vertrek: verouderd: opnieuw indelen'
37646
+ UNION ALL SELECT 'guards', position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_own_transport_carrier(jsonb,boolean)'::regprocedure)) > 0
37647
+ AND position('logistics_acc_guard' IN pg_get_functiondef('public.logistics_order_transport_group(jsonb)'::regprocedure)) > 0,
37648
+ 'Aanroeper wordt intern gecontroleerd'
37649
+ $$;
37650
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_218() TO authenticated, service_role;