diff --git a/db/core/changelog-master.xml b/db/core/changelog-master.xml
index ecb60c8..fa34d98 100644
--- a/db/core/changelog-master.xml
+++ b/db/core/changelog-master.xml
@@ -38,5 +38,6 @@
+
diff --git a/db/core/changesets/032-fix-work-program-jsonb-object-length.sql b/db/core/changesets/032-fix-work-program-jsonb-object-length.sql
new file mode 100644
index 0000000..b2cb279
--- /dev/null
+++ b/db/core/changesets/032-fix-work-program-jsonb-object-length.sql
@@ -0,0 +1,123 @@
+--liquibase formatted sql
+-- PANELS · core · fn_work_program_replace: jsonb_object_length no existe en PostgreSQL
+
+--changeset panel:core-032a-fn-work-program-replace-period-count splitStatements:false
+CREATE OR REPLACE FUNCTION core.fn_work_program_replace(payload jsonb)
+RETURNS jsonb
+LANGUAGE plpgsql
+SECURITY INVOKER
+SET search_path = core
+AS $$
+DECLARE
+ v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
+ v_source text := COALESCE(nullif(btrim(payload->>'source'), ''), 'import');
+ v_status text := COALESCE(nullif(btrim(payload->>'status'), ''), 'active');
+ v_start date;
+ v_end date;
+ v_program_id bigint;
+ v_period jsonb;
+ v_period_id bigint;
+ v_period_map jsonb := '{}'::jsonb;
+ v_period_count integer := 0;
+ v_slot jsonb;
+ v_partida jsonb;
+ v_chapter jsonb;
+ v_ch jsonb;
+ v_slot_count integer := 0;
+ v_partida_count integer := 0;
+ v_err jsonb;
+BEGIN
+ IF v_project_id IS NULL THEN
+ RETURN core.rpc_err('VALIDATION', 'fn_work_program_replace: project_id es obligatorio',
+ jsonb_build_object('fn', 'fn_work_program_replace'));
+ END IF;
+
+ v_err := core._wp_assert_project_ready(v_project_id);
+ IF v_err IS NOT NULL THEN RETURN v_err; END IF;
+
+ SELECT start_date, end_date INTO v_start, v_end FROM projects WHERE id = v_project_id;
+
+ DELETE FROM work_programs WHERE project_id = v_project_id;
+
+ INSERT INTO work_programs (project_id, start_date, end_date, duration_days, source, status)
+ VALUES (v_project_id, v_start, v_end, (v_end - v_start + 1), v_source, v_status)
+ RETURNING id INTO v_program_id;
+
+ FOR v_period IN SELECT * FROM jsonb_array_elements(core._wp_generate_periods(v_start, v_end)) LOOP
+ INSERT INTO work_program_periods (program_id, period_index, label, start_date, end_date)
+ VALUES (
+ v_program_id,
+ (v_period->>'period_index')::integer,
+ v_period->>'label',
+ (v_period->>'start_date')::date,
+ (v_period->>'end_date')::date
+ )
+ RETURNING id INTO v_period_id;
+ v_period_map := v_period_map || jsonb_build_object(v_period->>'period_index', v_period_id);
+ v_period_count := v_period_count + 1;
+ END LOOP;
+
+ FOR v_slot IN SELECT * FROM jsonb_array_elements(COALESCE(payload->'concept_slots', '[]'::jsonb)) LOOP
+ v_period_id := (v_period_map->>(v_slot->>'period_index'))::bigint;
+ IF v_period_id IS NULL THEN CONTINUE; END IF;
+ IF NOT EXISTS (
+ SELECT 1 FROM budget_items bi
+ WHERE bi.id = (v_slot->>'budget_item_id')::bigint AND bi.project_id = v_project_id
+ ) THEN CONTINUE; END IF;
+ IF core._wp_slot_weight((v_slot->>'start_pct')::numeric, (v_slot->>'end_pct')::numeric) <= 0 THEN CONTINUE; END IF;
+ INSERT INTO work_program_concept_slots (program_id, budget_item_id, period_id, start_pct, end_pct)
+ VALUES (
+ v_program_id,
+ (v_slot->>'budget_item_id')::bigint,
+ v_period_id,
+ COALESCE((v_slot->>'start_pct')::numeric, 0),
+ COALESCE((v_slot->>'end_pct')::numeric, 0)
+ );
+ v_slot_count := v_slot_count + 1;
+ END LOOP;
+
+ FOR v_ch IN SELECT * FROM jsonb_array_elements(COALESCE(payload->'chapters_to_create', '[]'::jsonb)) LOOP
+ INSERT INTO budget_chapters (project_id, parent_id, code, name, wbs, sort_order)
+ VALUES (
+ v_project_id,
+ NULLIF(v_ch->>'parent_id', '')::bigint,
+ upper(COALESCE(nullif(btrim(v_ch->>'code'), ''), '')),
+ COALESCE(nullif(btrim(v_ch->>'name'), ''), 'Partida'),
+ COALESCE(nullif(btrim(v_ch->>'wbs'), ''), ''),
+ COALESCE((v_ch->>'sort_order')::integer, 0)
+ );
+ END LOOP;
+
+ FOR v_partida IN SELECT * FROM jsonb_array_elements(COALESCE(payload->'partida_amounts', '[]'::jsonb)) LOOP
+ v_period_id := (v_period_map->>(v_partida->>'period_index'))::bigint;
+ IF v_period_id IS NULL THEN CONTINUE; END IF;
+ IF NOT EXISTS (
+ SELECT 1 FROM budget_chapters bc
+ WHERE bc.id = (v_partida->>'budget_chapter_id')::bigint AND bc.project_id = v_project_id
+ ) THEN CONTINUE; END IF;
+ IF COALESCE((v_partida->>'amount')::numeric, 0) = 0 THEN CONTINUE; END IF;
+ INSERT INTO work_program_partida_amounts (program_id, budget_chapter_id, period_id, amount)
+ VALUES (
+ v_program_id,
+ (v_partida->>'budget_chapter_id')::bigint,
+ v_period_id,
+ round(COALESCE((v_partida->>'amount')::numeric, 0), 2)
+ );
+ v_partida_count := v_partida_count + 1;
+ END LOOP;
+
+ RETURN core.rpc_ok(
+ jsonb_build_object(
+ 'program_id', v_program_id,
+ 'concept_slots', v_slot_count,
+ 'partida_amounts', v_partida_count,
+ 'period_count', v_period_count
+ ),
+ format('Programa de obra del proyecto %s reemplazado (%s slots, %s erogaciones)',
+ v_project_id, v_slot_count, v_partida_count),
+ jsonb_build_object('fn', 'fn_work_program_replace', 'project_id', v_project_id)
+ );
+EXCEPTION WHEN OTHERS THEN
+ RETURN core.rpc_from_exception('fn_work_program_replace', SQLSTATE, SQLERRM);
+END;
+$$;