--liquibase formatted sql -- PANELS · core · RPC nómina (port de api/payroll.ts) --changeset panel:core-013a-payroll-helpers endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.round_money(p_value numeric) RETURNS numeric LANGUAGE sql IMMUTABLE AS $$ SELECT round(COALESCE(p_value, 0)::numeric, 2); $$; CREATE OR REPLACE FUNCTION core._payroll_tenant_id(payload jsonb) RETURNS integer LANGUAGE sql STABLE AS $$ SELECT COALESCE( NULLIF(payload->>'tenant_id', '')::integer, NULLIF(current_setting('app.tenant_id', true), '')::integer, 0 ); $$; CREATE OR REPLACE FUNCTION core._payroll_add_days(p_iso date, p_days integer) RETURNS date LANGUAGE sql IMMUTABLE AS $$ SELECT (p_iso + make_interval(days => p_days))::date; $$; CREATE OR REPLACE FUNCTION core._payroll_weekday_mon0(p_iso date) RETURNS integer LANGUAGE sql IMMUTABLE AS $$ SELECT (extract(isodow FROM p_iso)::integer - 1); $$; CREATE OR REPLACE FUNCTION core._payroll_week_containing(p_iso date) RETURNS TABLE(week_start date, week_end date) LANGUAGE sql IMMUTABLE AS $$ WITH ws AS ( SELECT core._payroll_add_days(p_iso, -core._payroll_weekday_mon0(p_iso)) AS week_start ) SELECT ws.week_start, core._payroll_add_days(ws.week_start, 5) AS week_end FROM ws; $$; CREATE OR REPLACE FUNCTION core._payroll_destajo_period_bounds(p_week_start date) RETURNS TABLE(period_start date, period_end date) LANGUAGE sql IMMUTABLE AS $$ WITH pe AS ( SELECT core._payroll_add_days(p_week_start, 3) AS period_end ) SELECT core._payroll_add_days(pe.period_end, -7), pe.period_end FROM pe; $$; CREATE OR REPLACE FUNCTION core._payroll_this_saturday(p_iso date) RETURNS date LANGUAGE sql IMMUTABLE AS $$ SELECT week_end FROM core._payroll_week_containing(p_iso); $$; CREATE OR REPLACE FUNCTION core._payroll_next_saturday_after_week(p_iso date) RETURNS date LANGUAGE sql IMMUTABLE AS $$ SELECT core._payroll_add_days(core._payroll_this_saturday(p_iso), 7); $$; CREATE OR REPLACE FUNCTION core._payroll_week_assemble_date(p_week_start date) RETURNS date LANGUAGE sql IMMUTABLE AS $$ SELECT core._payroll_add_days(p_week_start, 3); $$; CREATE OR REPLACE FUNCTION core._payroll_can_assemble_week(p_week_start date, p_today date) RETURNS boolean LANGUAGE sql IMMUTABLE AS $$ SELECT p_today >= core._payroll_week_assemble_date(p_week_start); $$; CREATE OR REPLACE FUNCTION core._payroll_tenant_today(p_tenant_id integer) RETURNS date LANGUAGE sql STABLE SET search_path = core AS $$ SELECT (now() AT TIME ZONE COALESCE( (SELECT timezone FROM tenant_settings WHERE tenant_id = p_tenant_id), 'America/Mexico_City' ))::date; $$; CREATE OR REPLACE FUNCTION core._payroll_money_label(p_amount numeric) RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT '$' || trim(to_char(core.round_money(p_amount), 'FM999,999,999,990.00')); $$; CREATE OR REPLACE FUNCTION core._payroll_commission_amount(p_delivered numeric, p_pct numeric) RETURNS numeric LANGUAGE sql IMMUTABLE AS $$ SELECT core.round_money(COALESCE(p_delivered, 0) * COALESCE(p_pct, 0) / 100.0); $$; CREATE OR REPLACE FUNCTION core._payroll_loan_condition_label( p_delivered numeric, p_commission_pct numeric, p_pay_amount numeric, p_installment_n integer, p_installments_n integer ) RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT format( '%s · %s/%s · %s', core._payroll_money_label(p_pay_amount), p_installment_n, GREATEST(COALESCE(p_installments_n, 1), 1), CASE WHEN COALESCE(p_commission_pct, 0) > 0 THEN format('préstamo %s + comisión %s%%', core._payroll_money_label(p_delivered), trim(to_char(p_commission_pct, 'FM999990.##'))) ELSE format('préstamo %s', core._payroll_money_label(p_delivered)) END ); $$; CREATE OR REPLACE FUNCTION core._payroll_seed_defaults(p_tenant_id integer) RETURNS void LANGUAGE plpgsql SET search_path = core AS $$ BEGIN INSERT INTO payroll_settings (tenant_id, loan_commission_enabled, loan_commission_pct, loan_small_max) VALUES (p_tenant_id, true, 10, 500) ON CONFLICT (tenant_id) DO NOTHING; IF NOT EXISTS (SELECT 1 FROM destajo_units WHERE tenant_id = p_tenant_id) THEN INSERT INTO destajo_units (tenant_id, code, label) VALUES (p_tenant_id, 'm', 'Metro (m)'), (p_tenant_id, 'm2', 'Metro cuadrado (m²)'), (p_tenant_id, 'm3', 'Metro cúbico (m³)'), (p_tenant_id, 'ml', 'Metro lineal (ml)'), (p_tenant_id, 'pza', 'Pieza'); END IF; END; $$; CREATE OR REPLACE FUNCTION core._payroll_sheet_line( p_sheet_id bigint, p_worker_id bigint, p_destajo_cut_line_id bigint ) RETURNS bigint LANGUAGE plpgsql SET search_path = core AS $$ DECLARE v_id bigint; BEGIN IF p_destajo_cut_line_id IS NOT NULL THEN SELECT id INTO v_id FROM payroll_week_lines WHERE sheet_id = p_sheet_id AND destajo_cut_line_id = p_destajo_cut_line_id; ELSE SELECT id INTO v_id FROM payroll_week_lines WHERE sheet_id = p_sheet_id AND worker_id = p_worker_id AND destajo_cut_line_id IS NULL; END IF; IF v_id IS NOT NULL THEN RETURN v_id; END IF; INSERT INTO payroll_week_lines (sheet_id, worker_id, destajo_cut_line_id) VALUES (p_sheet_id, p_worker_id, p_destajo_cut_line_id) RETURNING id INTO v_id; RETURN v_id; END; $$; --changeset panel:core-013b-payroll-recalc endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core._payroll_recalc_week(p_week_id bigint) RETURNS void LANGUAGE plpgsql SET search_path = core AS $$ DECLARE v_week record; v_sheet record; v_worker record; v_cut record; v_line record; v_loan record; v_line_id bigint; v_att_n integer; v_wage numeric; v_gross numeric; v_payable_qty numeric; v_required_gross numeric; v_keep bigint[]; v_stale record; v_tot numeric; v_worker_id bigint; v_lines record; v_gross_sum numeric; v_remaining numeric; v_placed boolean; v_n integer; v_is_last boolean; v_want numeric; v_take numeric; v_label text; v_host_id bigint; v_host_gross numeric; v_host_discounts numeric; BEGIN SELECT * INTO v_week FROM payroll_weeks WHERE id = p_week_id; IF NOT FOUND OR v_week.status = 'paid' THEN RETURN; END IF; UPDATE payroll_week_lines SET loan_id = NULL, loan_discount = 0, loan_label = NULL WHERE sheet_id IN (SELECT id FROM payroll_sheets WHERE week_id = p_week_id); FOR v_sheet IN SELECT * FROM payroll_sheets WHERE week_id = p_week_id LOOP IF v_sheet.kind = 'obra' AND v_sheet.project_id IS NOT NULL THEN FOR v_worker IN SELECT w.id, w.daily_wage FROM workers w WHERE w.status = 'activo' AND w.pipeline_status != 'baja' AND ( EXISTS ( SELECT 1 FROM assignments a WHERE a.worker_id = w.id AND a.project_id = v_sheet.project_id AND a.active = true AND w.work_type = 'N' ) OR EXISTS ( SELECT 1 FROM attendance att WHERE att.worker_id = w.id AND att.project_id = v_sheet.project_id AND att.work_date BETWEEN v_week.week_start AND v_week.week_end ) OR EXISTS ( SELECT 1 FROM payroll_week_lines l JOIN payroll_sheets s ON s.id = l.sheet_id WHERE l.worker_id = w.id AND s.project_id = v_sheet.project_id AND s.kind = 'obra' AND s.week_id = p_week_id ) ) ORDER BY w.last_name_p, w.first_name LOOP v_line_id := core._payroll_sheet_line(v_sheet.id, v_worker.id, NULL); SELECT COUNT(*)::integer INTO v_att_n FROM attendance WHERE worker_id = v_worker.id AND project_id = v_sheet.project_id AND work_date BETWEEN v_week.week_start AND v_week.week_end AND present = true; SELECT daily_wage INTO v_wage FROM payroll_week_lines WHERE id = v_line_id; IF v_week.status = 'assembled' AND COALESCE(v_wage, 0) > 0 THEN NULL; ELSE v_wage := v_worker.daily_wage; END IF; v_gross := core.round_money(v_att_n * v_wage); UPDATE payroll_week_lines SET days = v_att_n, daily_wage = v_wage, amount = v_gross, gross = v_gross, payable_net = v_gross WHERE id = v_line_id; END LOOP; END IF; IF v_sheet.kind = 'destajo' AND v_sheet.project_id IS NOT NULL THEN SELECT id INTO v_line_id FROM destajo_periods WHERE week_id = p_week_id; IF v_line_id IS NULL THEN CONTINUE; END IF; v_keep := ARRAY[]::bigint[]; FOR v_cut IN SELECT c.id, c.qty_planned, c.qty_actual, c.qty_extra, j.worker_id, j.unit_price, j.unit_code, j.concepto FROM destajo_cut_lines c JOIN destajo_jobs j ON j.id = c.job_id WHERE c.period_id = v_line_id AND j.project_id = v_sheet.project_id LOOP v_line_id := core._payroll_sheet_line(v_sheet.id, v_cut.worker_id, v_cut.id); v_keep := array_append(v_keep, v_line_id); IF v_week.status = 'draft' AND v_cut.qty_actual = 0 AND v_cut.qty_extra = 0 THEN v_payable_qty := core.round_money(v_cut.qty_planned); ELSE v_payable_qty := core.round_money(v_cut.qty_actual + v_cut.qty_extra); END IF; v_gross := core.round_money(v_payable_qty * v_cut.unit_price); v_required_gross := core.round_money(v_cut.qty_planned * v_cut.unit_price); UPDATE payroll_week_lines SET destajo_cut_line_id = v_cut.id, qty_planned = v_cut.qty_planned, qty_actual = v_cut.qty_actual, qty_extra = v_cut.qty_extra, unit_price = v_cut.unit_price, unit_code = v_cut.unit_code, concepto = v_cut.concepto, amount = v_required_gross, gross = v_gross, payable_net = v_gross WHERE id = v_line_id; END LOOP; FOR v_stale IN SELECT id FROM payroll_week_lines WHERE sheet_id = v_sheet.id LOOP IF NOT (v_stale.id = ANY (v_keep)) THEN DELETE FROM payroll_week_lines WHERE id = v_stale.id; END IF; END LOOP; END IF; IF v_sheet.kind = 'admin' THEN FOR v_line IN SELECT id, amount, discounts FROM payroll_week_lines WHERE sheet_id = v_sheet.id LOOP v_gross := core.round_money(v_line.amount); UPDATE payroll_week_lines SET gross = v_gross, payable_net = core.round_money(v_gross - v_line.discounts) WHERE id = v_line.id; END LOOP; END IF; END LOOP; FOR v_worker_id IN SELECT DISTINCT l.worker_id FROM payroll_week_lines l JOIN payroll_sheets s ON s.id = l.sheet_id WHERE s.week_id = p_week_id LOOP SELECT core.round_money(COALESCE(SUM(l.gross - l.discounts), 0)) INTO v_gross_sum FROM payroll_week_lines l JOIN payroll_sheets s ON s.id = l.sheet_id WHERE s.week_id = p_week_id AND l.worker_id = v_worker_id; v_remaining := GREATEST(0, v_gross_sum); v_placed := false; FOR v_loan IN SELECT * FROM loans WHERE worker_id = v_worker_id AND balance > 0 AND first_due IS NOT NULL AND first_due <= v_week.week_end ORDER BY first_due, id LOOP EXIT WHEN v_remaining <= 0; SELECT COUNT(*)::integer + 1 INTO v_n FROM loan_payments WHERE loan_id = v_loan.id; v_is_last := v_n >= GREATEST(COALESCE(v_loan.installments_n, 1), 1); IF v_is_last THEN v_want := v_loan.balance; ELSE v_want := LEAST(COALESCE(NULLIF(v_loan.weekly_payment, 0), v_loan.balance), v_loan.balance); END IF; v_take := core.round_money(LEAST(v_want, v_remaining)); IF v_take <= 0 THEN CONTINUE; END IF; v_remaining := core.round_money(v_remaining - v_take); IF NOT v_placed THEN SELECT l.id, l.gross, l.discounts INTO v_host_id, v_host_gross, v_host_discounts FROM payroll_week_lines l JOIN payroll_sheets s ON s.id = l.sheet_id WHERE s.week_id = p_week_id AND l.worker_id = v_worker_id ORDER BY l.gross DESC, l.id LIMIT 1; v_label := core._payroll_loan_condition_label( v_loan.delivered, v_loan.commission_pct, v_take, v_n, v_loan.installments_n ); UPDATE payroll_week_lines SET loan_id = v_loan.id, loan_discount = v_take, loan_label = v_label, payable_net = core.round_money(v_host_gross - v_host_discounts - v_take) WHERE id = v_host_id; v_placed := true; END IF; EXIT; END LOOP; IF NOT v_placed THEN UPDATE payroll_week_lines l SET payable_net = core.round_money(l.gross - l.discounts) FROM payroll_sheets s WHERE s.id = l.sheet_id AND s.week_id = p_week_id AND l.worker_id = v_worker_id; ELSE UPDATE payroll_week_lines l SET payable_net = core.round_money(l.gross - l.discounts) FROM payroll_sheets s WHERE s.id = l.sheet_id AND s.week_id = p_week_id AND l.worker_id = v_worker_id AND l.id <> v_host_id; END IF; END LOOP; SELECT COALESCE(SUM(l.payable_net), 0) INTO v_tot FROM payroll_week_lines l JOIN payroll_sheets s ON s.id = l.sheet_id WHERE s.week_id = p_week_id; UPDATE payroll_weeks SET payable_net = core.round_money(v_tot) WHERE id = p_week_id; IF v_week.status = 'draft' THEN UPDATE payroll_weeks SET required_net = core.round_money(v_tot) WHERE id = p_week_id; UPDATE payroll_week_lines SET required_net = payable_net WHERE sheet_id IN (SELECT id FROM payroll_sheets WHERE week_id = p_week_id); END IF; END; $$; CREATE OR REPLACE FUNCTION core._payroll_ensure_cut_for_open_jobs(p_week_id bigint) RETURNS void LANGUAGE plpgsql SET search_path = core AS $$ DECLARE v_period record; v_job record; BEGIN SELECT id, tenant_id INTO v_period FROM destajo_periods WHERE week_id = p_week_id; IF NOT FOUND THEN RETURN; END IF; FOR v_job IN SELECT id FROM destajo_jobs WHERE tenant_id = v_period.tenant_id AND status = 'open' LOOP INSERT INTO destajo_cut_lines (period_id, job_id, qty_planned, qty_actual, qty_extra) VALUES (v_period.id, v_job.id, 0, 0, 0) ON CONFLICT (period_id, job_id) DO NOTHING; END LOOP; END; $$; CREATE OR REPLACE FUNCTION core._payroll_ensure_week(p_tenant_id integer, p_week_start date) RETURNS bigint LANGUAGE plpgsql SET search_path = core AS $$ DECLARE v_week_start date; v_week_end date; v_week_id bigint; v_bounds record; v_period_id bigint; v_proj record; BEGIN PERFORM core._payroll_seed_defaults(p_tenant_id); SELECT wc.week_start, wc.week_end INTO v_week_start, v_week_end FROM core._payroll_week_containing(p_week_start) wc; SELECT id INTO v_week_id FROM payroll_weeks WHERE tenant_id = p_tenant_id AND week_start = v_week_start; IF v_week_id IS NULL THEN INSERT INTO payroll_weeks (week_start, week_end, status, tenant_id) VALUES (v_week_start, v_week_end, 'draft', p_tenant_id) RETURNING id INTO v_week_id; END IF; SELECT * INTO v_bounds FROM core._payroll_destajo_period_bounds(v_week_start); SELECT id INTO v_period_id FROM destajo_periods WHERE tenant_id = p_tenant_id AND period_end = v_bounds.period_end; IF v_period_id IS NULL THEN INSERT INTO destajo_periods (period_start, period_end, week_id, tenant_id) VALUES (v_bounds.period_start, v_bounds.period_end, v_week_id, p_tenant_id); END IF; FOR v_proj IN SELECT id FROM projects WHERE status IN ('activo', 'pausado') AND (p_tenant_id = 0 OR tenant_id = p_tenant_id OR tenant_id IS NULL) ORDER BY name LOOP IF NOT EXISTS ( SELECT 1 FROM payroll_sheets WHERE week_id = v_week_id AND kind = 'obra' AND COALESCE(project_id, 0) = v_proj.id ) THEN INSERT INTO payroll_sheets (week_id, kind, project_id) VALUES (v_week_id, 'obra', v_proj.id); END IF; IF NOT EXISTS ( SELECT 1 FROM payroll_sheets WHERE week_id = v_week_id AND kind = 'destajo' AND COALESCE(project_id, 0) = v_proj.id ) THEN INSERT INTO payroll_sheets (week_id, kind, project_id) VALUES (v_week_id, 'destajo', v_proj.id); END IF; END LOOP; IF NOT EXISTS ( SELECT 1 FROM payroll_sheets WHERE week_id = v_week_id AND kind = 'admin' AND project_id IS NULL ) THEN INSERT INTO payroll_sheets (week_id, kind, project_id) VALUES (v_week_id, 'admin', NULL); END IF; RETURN v_week_id; END; $$; CREATE OR REPLACE FUNCTION core._payroll_first_due_for_loan( p_delivered numeric, p_small_max numeric, p_plan text, p_grant_iso date, p_week_assembled boolean, p_weekday_mon0 integer ) RETURNS date LANGUAGE plpgsql IMMUTABLE AS $$ BEGIN IF p_delivered <= p_small_max AND p_plan = 'single' THEN RETURN core._payroll_next_saturday_after_week(p_grant_iso); END IF; IF p_week_assembled AND p_weekday_mon0 >= 4 THEN RETURN core._payroll_next_saturday_after_week(p_grant_iso); END IF; RETURN core._payroll_this_saturday(p_grant_iso); END; $$; CREATE OR REPLACE FUNCTION core._payroll_prefill_thu_sat( p_project_id bigint, p_week_start date, p_worker_id bigint ) RETURNS void LANGUAGE plpgsql SET search_path = core AS $$ DECLARE v_offset integer; v_day date; v_other record; v_existing record; BEGIN FOR v_offset IN SELECT unnest(ARRAY[3, 4, 5]) LOOP v_day := core._payroll_add_days(p_week_start, v_offset); SELECT a.project_id, p.name AS project_name INTO v_other FROM attendance a JOIN projects p ON p.id = a.project_id WHERE a.worker_id = p_worker_id AND a.work_date = v_day AND a.present = true AND a.project_id <> p_project_id LIMIT 1; IF FOUND THEN CONTINUE; END IF; SELECT id, present INTO v_existing FROM attendance WHERE worker_id = p_worker_id AND project_id = p_project_id AND work_date = v_day; IF FOUND THEN CONTINUE; END IF; INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (p_worker_id, p_project_id, v_day, true); END LOOP; END; $$; --changeset panel:core-013c-fn-payroll-settings endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_payroll_settings_get(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := core._payroll_tenant_id(payload); v_settings jsonb; v_units jsonb; BEGIN PERFORM core._payroll_seed_defaults(v_tid); SELECT to_jsonb(s) INTO v_settings FROM payroll_settings s WHERE tenant_id = v_tid; SELECT COALESCE(jsonb_agg(to_jsonb(u) ORDER BY u.id), '[]'::jsonb) INTO v_units FROM destajo_units u WHERE u.tenant_id = v_tid; RETURN core.rpc_ok( jsonb_build_object('settings', v_settings, 'units', v_units), format('Configuración de nómina del tenant %s cargada', v_tid), jsonb_build_object('fn', 'fn_payroll_settings_get', 'tenant_id', v_tid) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_settings_get', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_payroll_settings_save(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := core._payroll_tenant_id(payload); v_cur payroll_settings%ROWTYPE; v_enabled boolean; v_pct numeric; v_small_max numeric; v_settings jsonb; BEGIN PERFORM core._payroll_seed_defaults(v_tid); SELECT * INTO v_cur FROM payroll_settings WHERE tenant_id = v_tid; v_enabled := COALESCE((payload->>'loan_commission_enabled')::boolean, v_cur.loan_commission_enabled); v_pct := COALESCE((payload->>'loan_commission_pct')::numeric, v_cur.loan_commission_pct, 0); v_small_max := COALESCE((payload->>'loan_small_max')::numeric, v_cur.loan_small_max, 0); UPDATE payroll_settings SET loan_commission_enabled = v_enabled, loan_commission_pct = COALESCE(v_pct, 0), loan_small_max = COALESCE(v_small_max, 0) WHERE tenant_id = v_tid; SELECT to_jsonb(s) INTO v_settings FROM payroll_settings s WHERE tenant_id = v_tid; RETURN core.rpc_ok( jsonb_build_object('settings', v_settings), format('Configuración de nómina del tenant %s guardada', v_tid), jsonb_build_object('fn', 'fn_payroll_settings_save', 'tenant_id', v_tid) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_settings_save', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-013d-fn-payroll-units endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_payroll_unit_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := core._payroll_tenant_id(payload); v_units jsonb; BEGIN PERFORM core._payroll_seed_defaults(v_tid); SELECT COALESCE(jsonb_agg(to_jsonb(u) ORDER BY u.id), '[]'::jsonb) INTO v_units FROM destajo_units u WHERE u.tenant_id = v_tid; RETURN core.rpc_ok( jsonb_build_object('units', v_units), format('Unidades de destajo del tenant %s: %s registro(s)', v_tid, jsonb_array_length(v_units)), jsonb_build_object('fn', 'fn_payroll_unit_list', 'tenant_id', v_tid, 'count', jsonb_array_length(v_units)) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_unit_list', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_payroll_unit_add(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := core._payroll_tenant_id(payload); v_code text := lower(regexp_replace(btrim(COALESCE(payload->>'code', '')), '\s+', '', 'g')); v_label text := COALESCE(NULLIF(btrim(payload->>'label'), ''), v_code); v_id bigint; BEGIN IF v_code = '' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_unit_add: unidad requerida', jsonb_build_object('fn', 'fn_payroll_unit_add', 'field', 'code') ); END IF; PERFORM core._payroll_seed_defaults(v_tid); INSERT INTO destajo_units (tenant_id, code, label) VALUES (v_tid, v_code, v_label) RETURNING id INTO v_id; RETURN core.rpc_created( jsonb_build_object('id', v_id, 'code', v_code, 'label', v_label), format('Unidad de destajo %s (%s) agregada', v_label, v_code), jsonb_build_object('fn', 'fn_payroll_unit_add', 'tenant_id', v_tid, 'id', v_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_unit_add', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-013e-fn-attendance endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_attendance_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint; v_from date := NULLIF(btrim(payload->>'from'), '')::date; v_to date := NULLIF(btrim(payload->>'to'), '')::date; v_rows jsonb; BEGIN IF v_project_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_attendance_list: project_id es obligatorio', jsonb_build_object('fn', 'fn_attendance_list', 'field', 'project_id') ); END IF; IF v_from IS NULL OR v_to IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_attendance_list: from y to son obligatorios (fechas ISO)', jsonb_build_object('fn', 'fn_attendance_list', 'fields', jsonb_build_array('from', 'to')) ); END IF; SELECT COALESCE(jsonb_agg(row_to_json(x)::jsonb ORDER BY x.work_date, x.last_name_p), '[]'::jsonb) INTO v_rows FROM ( SELECT a.*, w.first_name, w.last_name_p FROM attendance a JOIN workers w ON w.id = a.worker_id WHERE a.project_id = v_project_id AND a.work_date BETWEEN v_from AND v_to ) x; RETURN core.rpc_ok( jsonb_build_object('attendance', v_rows), format('Asistencia del proyecto %s: %s registro(s) entre %s y %s', v_project_id, jsonb_array_length(v_rows), v_from, v_to), jsonb_build_object('fn', 'fn_attendance_list', 'project_id', v_project_id, 'count', jsonb_array_length(v_rows)) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_attendance_list', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_attendance_set(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := core._payroll_tenant_id(payload); v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint; v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint; v_work_date date := NULLIF(btrim(payload->>'work_date'), '')::date; v_present boolean := COALESCE((payload->>'present')::boolean, false); v_other record; v_week_start date; v_week record; BEGIN IF v_project_id IS NULL OR v_worker_id IS NULL OR v_work_date IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_attendance_set: project_id, worker_id y work_date son obligatorios', jsonb_build_object('fn', 'fn_attendance_set') ); END IF; IF v_present THEN SELECT a.project_id, p.name AS project_name INTO v_other FROM attendance a JOIN projects p ON p.id = a.project_id WHERE a.worker_id = v_worker_id AND a.work_date = v_work_date AND a.present = true AND a.project_id <> v_project_id LIMIT 1; IF FOUND THEN RETURN core.rpc_err( 'CONFLICT', format('Ese día ya está marcado en %s', v_other.project_name), jsonb_build_object( 'fn', 'fn_attendance_set', 'worker_id', v_worker_id, 'work_date', v_work_date, 'other_project_id', v_other.project_id ), jsonb_build_object( 'other', jsonb_build_object( 'project_id', v_other.project_id, 'project_name', v_other.project_name ) ) ); END IF; END IF; INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (v_worker_id, v_project_id, v_work_date, v_present) ON CONFLICT (worker_id, project_id, work_date) DO UPDATE SET present = EXCLUDED.present; SELECT week_start INTO v_week_start FROM core._payroll_week_containing(v_work_date); SELECT id, status INTO v_week FROM payroll_weeks WHERE tenant_id = v_tid AND week_start = v_week_start; IF FOUND AND v_week.status <> 'paid' THEN PERFORM core._payroll_recalc_week(v_week.id); END IF; RETURN core.rpc_ok( jsonb_build_object('ok', true), format('Asistencia registrada para trabajador %s el %s', v_worker_id, v_work_date), jsonb_build_object('fn', 'fn_attendance_set', 'worker_id', v_worker_id, 'work_date', v_work_date) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_attendance_set', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-013f-fn-loan-create endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_loan_create(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := core._payroll_tenant_id(payload); v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint; v_delivered numeric; v_settings payroll_settings%ROWTYPE; v_enabled boolean; v_pct numeric; v_comm numeric; v_balance numeric; v_small boolean; v_plan text; v_installments integer; v_weekly numeric; v_grant_iso date; v_week record; v_assembled boolean; v_first_due date; v_loan_id bigint; BEGIN IF v_worker_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_loan_create: worker_id es obligatorio', jsonb_build_object('fn', 'fn_loan_create', 'field', 'worker_id') ); END IF; IF NOT EXISTS (SELECT 1 FROM workers WHERE id = v_worker_id) THEN RETURN core.rpc_err( 'NOT_FOUND', format('fn_loan_create: persona id=%s no encontrada', v_worker_id), jsonb_build_object('fn', 'fn_loan_create', 'worker_id', v_worker_id) ); END IF; PERFORM core._payroll_seed_defaults(v_tid); SELECT * INTO v_settings FROM payroll_settings WHERE tenant_id = v_tid; v_delivered := core.round_money(COALESCE( (payload->>'delivered')::numeric, (payload->>'amount')::numeric, 0 )); IF v_delivered <= 0 THEN RETURN core.rpc_err( 'VALIDATION', 'fn_loan_create: monto entregado requerido y mayor a cero', jsonb_build_object('fn', 'fn_loan_create', 'field', 'delivered') ); END IF; v_enabled := COALESCE( (payload->>'commission_enabled')::boolean, v_settings.loan_commission_enabled ); IF v_enabled THEN v_pct := COALESCE((payload->>'commission_pct')::numeric, v_settings.loan_commission_pct, 0); ELSE v_pct := 0; END IF; v_comm := core._payroll_commission_amount(v_delivered, v_pct); v_balance := core.round_money(v_delivered + v_comm); v_small := v_delivered <= v_settings.loan_small_max; v_plan := COALESCE(NULLIF(btrim(payload->>'plan'), ''), 'single'); v_installments := GREATEST(1, floor(COALESCE((payload->>'installments_n')::numeric, 1))::integer); IF v_small THEN v_plan := 'single'; v_installments := 1; END IF; IF v_plan = 'installments' AND v_installments < 2 THEN v_installments := 2; END IF; IF v_plan = 'single' THEN v_installments := 1; END IF; v_weekly := core.round_money(v_balance / v_installments); v_grant_iso := COALESCE( NULLIF(btrim(payload->>'grant_iso'), '')::date, core._payroll_tenant_today(v_tid) ); SELECT id, status INTO v_week FROM payroll_weeks WHERE tenant_id = v_tid AND week_start = (SELECT week_start FROM core._payroll_week_containing(v_grant_iso)); v_assembled := FOUND AND v_week.status IN ('assembled', 'paid'); v_first_due := core._payroll_first_due_for_loan( v_delivered, v_settings.loan_small_max, v_plan, v_grant_iso, v_assembled, core._payroll_weekday_mon0(v_grant_iso) ); INSERT INTO loans ( worker_id, amount, delivered, balance, weekly_payment, note, commission_pct, commission_amount, plan, installments_n, first_due ) VALUES ( v_worker_id, v_delivered, v_delivered, v_balance, v_weekly, NULLIF(btrim(payload->>'note'), ''), v_pct, v_comm, v_plan, v_installments, v_first_due ) RETURNING id INTO v_loan_id; IF FOUND AND v_week.id IS NOT NULL AND v_week.status <> 'paid' THEN PERFORM core._payroll_recalc_week(v_week.id); END IF; RETURN core.rpc_created( jsonb_build_object('id', v_loan_id), format('Préstamo de %s registrado para trabajador %s', core._payroll_money_label(v_delivered), v_worker_id), jsonb_build_object('fn', 'fn_loan_create', 'id', v_loan_id, 'worker_id', v_worker_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_loan_create', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-013g-fn-payroll-week endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_payroll_week_ensure(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := core._payroll_tenant_id(payload); v_week_start date := NULLIF(btrim(payload->>'week_start'), '')::date; v_week_id bigint; BEGIN IF v_week_start IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_week_ensure: week_start es obligatorio (fecha ISO)', jsonb_build_object('fn', 'fn_payroll_week_ensure', 'field', 'week_start') ); END IF; v_week_id := core._payroll_ensure_week(v_tid, v_week_start); RETURN core.rpc_ok( jsonb_build_object('week_id', v_week_id), format('Semana de nómina asegurada (id=%s) para inicio %s', v_week_id, v_week_start), jsonb_build_object('fn', 'fn_payroll_week_ensure', 'week_id', v_week_id, 'tenant_id', v_tid) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_week_ensure', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_payroll_week_recalc(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_week_id bigint := NULLIF(payload->>'week_id', '')::bigint; BEGIN IF v_week_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_week_recalc: week_id es obligatorio', jsonb_build_object('fn', 'fn_payroll_week_recalc', 'field', 'week_id') ); END IF; IF NOT EXISTS (SELECT 1 FROM payroll_weeks WHERE id = v_week_id) THEN RETURN core.rpc_err( 'NOT_FOUND', format('fn_payroll_week_recalc: semana id=%s no encontrada', v_week_id), jsonb_build_object('fn', 'fn_payroll_week_recalc', 'week_id', v_week_id) ); END IF; PERFORM core._payroll_recalc_week(v_week_id); RETURN core.rpc_ok( jsonb_build_object('week_id', v_week_id), format('Semana de nómina %s recalculada', v_week_id), jsonb_build_object('fn', 'fn_payroll_week_recalc', 'week_id', v_week_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_week_recalc', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_payroll_week_assemble(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := core._payroll_tenant_id(payload); v_week_id bigint := NULLIF(payload->>'week_id', '')::bigint; v_now_iso date := COALESCE( NULLIF(btrim(payload->>'now_iso'), '')::date, core._payroll_tenant_today(v_tid) ); v_week record; v_sheet record; v_worker record; v_assigned boolean; v_has_line boolean; v_period_id bigint; v_payable numeric; BEGIN IF v_week_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_week_assemble: week_id es obligatorio', jsonb_build_object('fn', 'fn_payroll_week_assemble', 'field', 'week_id') ); END IF; SELECT * INTO v_week FROM payroll_weeks WHERE id = v_week_id; IF NOT FOUND THEN RETURN core.rpc_err( 'NOT_FOUND', format('fn_payroll_week_assemble: semana id=%s no encontrada', v_week_id), jsonb_build_object('fn', 'fn_payroll_week_assemble', 'week_id', v_week_id) ); END IF; IF v_week.status = 'paid' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_week_assemble: la semana ya está pagada', jsonb_build_object('fn', 'fn_payroll_week_assemble', 'week_id', v_week_id, 'status', v_week.status) ); END IF; IF NOT core._payroll_can_assemble_week(v_week.week_start, v_now_iso) THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_week_assemble: la nómina se arma el jueves o después; aún no es jueves de esta semana', jsonb_build_object('fn', 'fn_payroll_week_assemble', 'week_id', v_week_id, 'today', v_now_iso) ); END IF; FOR v_sheet IN SELECT * FROM payroll_sheets WHERE week_id = v_week_id AND kind = 'obra' LOOP IF v_sheet.project_id IS NULL THEN CONTINUE; END IF; FOR v_worker IN SELECT w.id, w.work_type FROM workers w WHERE w.status = 'activo' AND w.pipeline_status != 'baja' AND ( EXISTS ( SELECT 1 FROM assignments a WHERE a.worker_id = w.id AND a.project_id = v_sheet.project_id AND a.active = true AND w.work_type = 'N' ) OR EXISTS ( SELECT 1 FROM attendance att WHERE att.worker_id = w.id AND att.project_id = v_sheet.project_id AND att.work_date BETWEEN v_week.week_start AND core._payroll_add_days(v_week.week_start, 5) ) OR EXISTS ( SELECT 1 FROM payroll_week_lines l JOIN payroll_sheets s ON s.id = l.sheet_id WHERE l.worker_id = w.id AND s.project_id = v_sheet.project_id AND s.kind = 'obra' AND s.week_id = v_week_id ) ) ORDER BY w.last_name_p, w.first_name LOOP SELECT EXISTS ( SELECT 1 FROM assignments WHERE worker_id = v_worker.id AND project_id = v_sheet.project_id AND active = true ) INTO v_assigned; IF NOT v_assigned THEN CONTINUE; END IF; IF v_worker.work_type <> 'N' THEN SELECT EXISTS ( SELECT 1 FROM payroll_week_lines WHERE sheet_id = v_sheet.id AND worker_id = v_worker.id ) INTO v_has_line; IF NOT v_has_line THEN CONTINUE; END IF; END IF; PERFORM core._payroll_prefill_thu_sat(v_sheet.project_id, v_week.week_start, v_worker.id); END LOOP; END LOOP; SELECT id INTO v_period_id FROM destajo_periods WHERE week_id = v_week_id; IF v_period_id IS NOT NULL THEN UPDATE destajo_cut_lines SET qty_actual = qty_planned WHERE period_id = v_period_id AND qty_actual = 0 AND qty_planned > 0; END IF; UPDATE payroll_weeks SET status = 'assembled', assembled_at = now() WHERE id = v_week_id; PERFORM core._payroll_recalc_week(v_week_id); SELECT payable_net INTO v_payable FROM payroll_weeks WHERE id = v_week_id; UPDATE payroll_weeks SET required_net = v_payable WHERE id = v_week_id; UPDATE payroll_week_lines SET required_net = payable_net WHERE sheet_id IN (SELECT id FROM payroll_sheets WHERE week_id = v_week_id); RETURN core.rpc_ok( jsonb_build_object('week_id', v_week_id, 'status', 'assembled'), format('Semana de nómina %s armada; neto a pagar %s', v_week_id, core._payroll_money_label(v_payable)), jsonb_build_object('fn', 'fn_payroll_week_assemble', 'week_id', v_week_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_week_assemble', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_payroll_week_pay(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := core._payroll_tenant_id(payload); v_week_id bigint := NULLIF(payload->>'week_id', '')::bigint; v_now_iso date := COALESCE( NULLIF(btrim(payload->>'now_iso'), '')::date, core._payroll_tenant_today(v_tid) ); v_week record; v_line record; v_loan record; v_take numeric; v_n integer; v_period_id bigint; v_job record; v_paid numeric; v_env jsonb; BEGIN IF v_week_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_week_pay: week_id es obligatorio', jsonb_build_object('fn', 'fn_payroll_week_pay', 'field', 'week_id') ); END IF; SELECT * INTO v_week FROM payroll_weeks WHERE id = v_week_id; IF NOT FOUND THEN RETURN core.rpc_err( 'NOT_FOUND', format('fn_payroll_week_pay: semana id=%s no encontrada', v_week_id), jsonb_build_object('fn', 'fn_payroll_week_pay', 'week_id', v_week_id) ); END IF; IF v_week.status = 'paid' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_week_pay: la semana ya está pagada', jsonb_build_object('fn', 'fn_payroll_week_pay', 'week_id', v_week_id) ); END IF; IF v_week.status <> 'assembled' THEN v_env := core.fn_payroll_week_assemble(jsonb_build_object( 'week_id', v_week_id, 'tenant_id', v_tid, 'now_iso', v_now_iso::text )); IF NOT COALESCE((v_env->>'ok')::boolean, false) THEN RETURN v_env; END IF; END IF; PERFORM core._payroll_recalc_week(v_week_id); FOR v_line IN SELECT l.loan_id, l.loan_discount, l.loan_label, l.worker_id FROM payroll_week_lines l JOIN payroll_sheets s ON s.id = l.sheet_id WHERE s.week_id = v_week_id AND l.loan_id IS NOT NULL AND l.loan_discount > 0 LOOP SELECT * INTO v_loan FROM loans WHERE id = v_line.loan_id; IF NOT FOUND OR v_loan.balance <= 0 THEN CONTINUE; END IF; v_take := core.round_money(LEAST(v_line.loan_discount, v_loan.balance)); IF v_take <= 0 THEN CONTINUE; END IF; SELECT COUNT(*)::integer + 1 INTO v_n FROM loan_payments WHERE loan_id = v_loan.id; INSERT INTO loan_payments (loan_id, week_id, amount, installment_n, label) VALUES (v_loan.id, v_week_id, v_take, v_n, v_line.loan_label); UPDATE loans SET balance = core.round_money(balance - v_take) WHERE id = v_loan.id; END LOOP; SELECT id INTO v_period_id FROM destajo_periods WHERE week_id = v_week_id; IF v_period_id IS NOT NULL THEN FOR v_job IN SELECT id, qty_total_estimated FROM destajo_jobs LOOP SELECT COALESCE(SUM(c.qty_actual + c.qty_extra), 0) INTO v_paid FROM destajo_cut_lines c JOIN destajo_periods p ON p.id = c.period_id JOIN payroll_weeks w ON w.id = p.week_id WHERE c.job_id = v_job.id AND (w.status = 'paid' OR w.id = v_week_id); IF v_paid >= v_job.qty_total_estimated THEN UPDATE destajo_jobs SET status = 'done' WHERE id = v_job.id; END IF; END LOOP; END IF; UPDATE payroll_weeks SET status = 'paid', paid_at = now() WHERE id = v_week_id; RETURN core.rpc_ok( jsonb_build_object('week_id', v_week_id, 'status', 'paid'), format('Semana de nómina %s marcada como pagada', v_week_id), jsonb_build_object('fn', 'fn_payroll_week_pay', 'week_id', v_week_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_week_pay', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_payroll_week_bundle(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := core._payroll_tenant_id(payload); v_week_start date := COALESCE( NULLIF(btrim(payload->>'week_start'), '')::date, (SELECT week_start FROM core._payroll_week_containing(core._payroll_tenant_today(v_tid))) ); v_week_id bigint; v_week jsonb; v_sheets jsonb; BEGIN v_week_id := core._payroll_ensure_week(v_tid, v_week_start); PERFORM core._payroll_ensure_cut_for_open_jobs(v_week_id); PERFORM core._payroll_recalc_week(v_week_id); SELECT to_jsonb(w) INTO v_week FROM payroll_weeks w WHERE id = v_week_id; SELECT COALESCE(jsonb_agg( to_jsonb(s)::jsonb || jsonb_build_object( 'project_name', p.name, 'project_code', p.code, 'lines', COALESCE(( SELECT jsonb_agg(row_to_json(lx)::jsonb ORDER BY lx.last_name_p, lx.first_name, lx.id) FROM ( SELECT l.*, w.first_name, w.middle_name, w.last_name_p, w.last_name_m, w.position, w.work_type FROM payroll_week_lines l JOIN workers w ON w.id = l.worker_id WHERE l.sheet_id = s.id ) lx ), '[]'::jsonb) ) ORDER BY CASE s.kind WHEN 'obra' THEN 0 WHEN 'destajo' THEN 1 ELSE 2 END, p.name ), '[]'::jsonb) INTO v_sheets FROM payroll_sheets s LEFT JOIN projects p ON p.id = s.project_id WHERE s.week_id = v_week_id; RETURN core.rpc_ok( jsonb_build_object('week', v_week, 'sheets', v_sheets), format('Paquete de semana de nómina %s cargado (inicio %s)', v_week_id, v_week->>'week_start'), jsonb_build_object('fn', 'fn_payroll_week_bundle', 'week_id', v_week_id, 'tenant_id', v_tid) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_week_bundle', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-013h-fn-destajo endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_destajo_job_create(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := core._payroll_tenant_id(payload); v_week_id bigint := NULLIF(payload->>'week_id', '')::bigint; v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint; v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint; v_concepto text := btrim(COALESCE(payload->>'concepto', '')); v_unit_code text := btrim(COALESCE(payload->>'unit_code', '')); v_qty_total numeric := COALESCE((payload->>'qty_total_estimated')::numeric, 0); v_unit_price numeric := COALESCE((payload->>'unit_price')::numeric, 0); v_qty_planned numeric := COALESCE((payload->>'qty_planned')::numeric, 0); v_qty_extra numeric := COALESCE((payload->>'qty_extra')::numeric, 0); v_week_status text; v_period_id bigint; v_job_id bigint; BEGIN IF v_week_id IS NULL OR v_project_id IS NULL OR v_worker_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_destajo_job_create: week_id, project_id y worker_id son obligatorios', jsonb_build_object('fn', 'fn_destajo_job_create') ); END IF; SELECT status INTO v_week_status FROM payroll_weeks WHERE id = v_week_id; IF NOT FOUND OR v_week_status = 'paid' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_destajo_job_create: no se puede editar una semana pagada o inexistente', jsonb_build_object('fn', 'fn_destajo_job_create', 'week_id', v_week_id) ); END IF; IF v_concepto = '' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_destajo_job_create: concepto requerido', jsonb_build_object('fn', 'fn_destajo_job_create', 'field', 'concepto') ); END IF; INSERT INTO destajo_jobs ( project_id, worker_id, concepto, unit_code, qty_total_estimated, unit_price, tenant_id ) VALUES ( v_project_id, v_worker_id, v_concepto, v_unit_code, v_qty_total, v_unit_price, v_tid ) RETURNING id INTO v_job_id; SELECT id INTO v_period_id FROM destajo_periods WHERE week_id = v_week_id; IF v_period_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', format('fn_destajo_job_create: periodo destajo no encontrado para semana %s', v_week_id), jsonb_build_object('fn', 'fn_destajo_job_create', 'week_id', v_week_id) ); END IF; INSERT INTO destajo_cut_lines (period_id, job_id, qty_planned, qty_actual, qty_extra) VALUES (v_period_id, v_job_id, v_qty_planned, 0, v_qty_extra); PERFORM core._payroll_recalc_week(v_week_id); RETURN core.rpc_created( jsonb_build_object('id', v_job_id), format('Trabajo de destajo %s creado para semana %s', v_job_id, v_week_id), jsonb_build_object('fn', 'fn_destajo_job_create', 'id', v_job_id, 'week_id', v_week_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_destajo_job_create', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_destajo_cut_patch(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_week_id bigint := NULLIF(payload->>'week_id', '')::bigint; v_cut_id bigint := NULLIF(payload->>'cut_id', '')::bigint; v_week_status text; v_cut record; v_planned numeric; v_actual numeric; v_extra numeric; BEGIN IF v_week_id IS NULL OR v_cut_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_destajo_cut_patch: week_id y cut_id son obligatorios', jsonb_build_object('fn', 'fn_destajo_cut_patch') ); END IF; SELECT status INTO v_week_status FROM payroll_weeks WHERE id = v_week_id; IF NOT FOUND OR v_week_status = 'paid' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_destajo_cut_patch: no se puede editar una semana pagada o inexistente', jsonb_build_object('fn', 'fn_destajo_cut_patch', 'week_id', v_week_id) ); END IF; SELECT * INTO v_cut FROM destajo_cut_lines WHERE id = v_cut_id; IF NOT FOUND THEN RETURN core.rpc_err( 'NOT_FOUND', format('fn_destajo_cut_patch: partida id=%s no encontrada', v_cut_id), jsonb_build_object('fn', 'fn_destajo_cut_patch', 'cut_id', v_cut_id) ); END IF; v_planned := COALESCE((payload->>'qty_planned')::numeric, v_cut.qty_planned); v_actual := COALESCE((payload->>'qty_actual')::numeric, v_cut.qty_actual); v_extra := COALESCE((payload->>'qty_extra')::numeric, v_cut.qty_extra); UPDATE destajo_cut_lines SET qty_planned = v_planned, qty_actual = v_actual, qty_extra = v_extra WHERE id = v_cut_id; PERFORM core._payroll_recalc_week(v_week_id); RETURN core.rpc_ok( jsonb_build_object('cut_id', v_cut_id, 'week_id', v_week_id), format('Partida de destajo %s actualizada', v_cut_id), jsonb_build_object('fn', 'fn_destajo_cut_patch', 'cut_id', v_cut_id, 'week_id', v_week_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_destajo_cut_patch', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-013i-fn-payroll-admin-jornal endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_payroll_admin_line_add(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_week_id bigint := NULLIF(payload->>'week_id', '')::bigint; v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint; v_amount numeric := core.round_money(COALESCE((payload->>'amount')::numeric, 0)); v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint; v_week_status text; v_sheet_id bigint; v_line_id bigint; BEGIN IF v_week_id IS NULL OR v_worker_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_admin_line_add: week_id y worker_id son obligatorios', jsonb_build_object('fn', 'fn_payroll_admin_line_add') ); END IF; SELECT status INTO v_week_status FROM payroll_weeks WHERE id = v_week_id; IF NOT FOUND OR v_week_status = 'paid' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_admin_line_add: no se puede editar una semana pagada o inexistente', jsonb_build_object('fn', 'fn_payroll_admin_line_add', 'week_id', v_week_id) ); END IF; SELECT id INTO v_sheet_id FROM payroll_sheets WHERE week_id = v_week_id AND kind = 'admin' AND project_id IS NULL; IF v_sheet_id IS NULL THEN RETURN core.rpc_err( 'NOT_FOUND', format('fn_payroll_admin_line_add: hoja administrativa no encontrada para semana %s', v_week_id), jsonb_build_object('fn', 'fn_payroll_admin_line_add', 'week_id', v_week_id) ); END IF; v_line_id := core._payroll_sheet_line(v_sheet_id, v_worker_id, NULL); UPDATE payroll_week_lines SET amount = v_amount, project_id = v_project_id, gross = v_amount, payable_net = v_amount WHERE id = v_line_id; PERFORM core._payroll_recalc_week(v_week_id); RETURN core.rpc_ok( jsonb_build_object('line_id', v_line_id, 'week_id', v_week_id), format('Línea administrativa %s agregada por %s', v_line_id, core._payroll_money_label(v_amount)), jsonb_build_object('fn', 'fn_payroll_admin_line_add', 'line_id', v_line_id, 'week_id', v_week_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_admin_line_add', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_payroll_admin_line_update(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_week_id bigint := NULLIF(payload->>'week_id', '')::bigint; v_line_id bigint := NULLIF(payload->>'line_id', '')::bigint; v_amount numeric := core.round_money(COALESCE((payload->>'amount')::numeric, 0)); v_week_status text; BEGIN IF v_week_id IS NULL OR v_line_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_admin_line_update: week_id y line_id son obligatorios', jsonb_build_object('fn', 'fn_payroll_admin_line_update') ); END IF; SELECT status INTO v_week_status FROM payroll_weeks WHERE id = v_week_id; IF NOT FOUND OR v_week_status = 'paid' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_admin_line_update: no se puede editar una semana pagada o inexistente', jsonb_build_object('fn', 'fn_payroll_admin_line_update', 'week_id', v_week_id) ); END IF; UPDATE payroll_week_lines SET amount = v_amount WHERE id = v_line_id; PERFORM core._payroll_recalc_week(v_week_id); RETURN core.rpc_ok( jsonb_build_object('line_id', v_line_id, 'amount', v_amount), format('Línea administrativa %s actualizada a %s', v_line_id, core._payroll_money_label(v_amount)), jsonb_build_object('fn', 'fn_payroll_admin_line_update', 'line_id', v_line_id, 'week_id', v_week_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_admin_line_update', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_payroll_admin_line_remove(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_week_id bigint := NULLIF(payload->>'week_id', '')::bigint; v_line_id bigint := NULLIF(payload->>'line_id', '')::bigint; v_week_status text; BEGIN IF v_week_id IS NULL OR v_line_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_admin_line_remove: week_id y line_id son obligatorios', jsonb_build_object('fn', 'fn_payroll_admin_line_remove') ); END IF; SELECT status INTO v_week_status FROM payroll_weeks WHERE id = v_week_id; IF NOT FOUND OR v_week_status = 'paid' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_admin_line_remove: no se puede editar una semana pagada o inexistente', jsonb_build_object('fn', 'fn_payroll_admin_line_remove', 'week_id', v_week_id) ); END IF; DELETE FROM payroll_week_lines WHERE id = v_line_id; PERFORM core._payroll_recalc_week(v_week_id); RETURN core.rpc_ok( jsonb_build_object('line_id', v_line_id, 'week_id', v_week_id), format('Línea administrativa %s eliminada de la semana %s', v_line_id, v_week_id), jsonb_build_object('fn', 'fn_payroll_admin_line_remove', 'line_id', v_line_id, 'week_id', v_week_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_admin_line_remove', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_payroll_jornal_worker_add(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_week_id bigint := NULLIF(payload->>'week_id', '')::bigint; v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint; v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint; v_week_status text; v_sheet_id bigint; v_line_id bigint; BEGIN IF v_week_id IS NULL OR v_project_id IS NULL OR v_worker_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_jornal_worker_add: week_id, project_id y worker_id son obligatorios', jsonb_build_object('fn', 'fn_payroll_jornal_worker_add') ); END IF; SELECT status INTO v_week_status FROM payroll_weeks WHERE id = v_week_id; IF NOT FOUND OR v_week_status = 'paid' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_jornal_worker_add: no se puede editar una semana pagada o inexistente', jsonb_build_object('fn', 'fn_payroll_jornal_worker_add', 'week_id', v_week_id) ); END IF; SELECT id INTO v_sheet_id FROM payroll_sheets WHERE week_id = v_week_id AND kind = 'obra' AND project_id = v_project_id; IF v_sheet_id IS NULL THEN RETURN core.rpc_err( 'NOT_FOUND', format('fn_payroll_jornal_worker_add: hoja de obra no encontrada (semana %s, proyecto %s)', v_week_id, v_project_id), jsonb_build_object('fn', 'fn_payroll_jornal_worker_add', 'week_id', v_week_id, 'project_id', v_project_id) ); END IF; v_line_id := core._payroll_sheet_line(v_sheet_id, v_worker_id, NULL); PERFORM core._payroll_recalc_week(v_week_id); RETURN core.rpc_ok( jsonb_build_object('line_id', v_line_id, 'week_id', v_week_id), format('Trabajador %s agregado a jornal (proyecto %s, semana %s)', v_worker_id, v_project_id, v_week_id), jsonb_build_object('fn', 'fn_payroll_jornal_worker_add', 'line_id', v_line_id, 'week_id', v_week_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_jornal_worker_add', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-013i2-fn-payroll-week-list-open endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_payroll_week_list_open(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := (payload->>'tenant_id')::integer; v_weeks jsonb; BEGIN SELECT COALESCE(jsonb_agg(to_jsonb(w) ORDER BY w.week_start DESC), '[]'::jsonb) INTO v_weeks FROM ( SELECT pw.*, (SELECT COALESCE(SUM(l.loan_discount), 0) FROM payroll_week_lines l JOIN payroll_sheets s ON s.id = l.sheet_id WHERE s.week_id = pw.id) AS loan_recovery FROM payroll_weeks pw WHERE pw.tenant_id = v_tid AND pw.status != 'paid' ) w; RETURN core.rpc_ok( jsonb_build_object('weeks', v_weeks), format('Semanas abiertas listadas para tenant %s: %s semana(s)', v_tid, jsonb_array_length(v_weeks)), jsonb_build_object('fn', 'fn_payroll_week_list_open', 'tenant_id', v_tid) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_week_list_open', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-013i3-fn-payroll-week-csv endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_payroll_week_csv(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_week_id bigint := (payload->>'week_id')::bigint; v_csv text; BEGIN IF v_week_id IS NULL THEN RETURN core.rpc_err('VALIDATION', 'fn_payroll_week_csv: week_id es obligatorio', jsonb_build_object('fn', 'fn_payroll_week_csv', 'field', 'week_id')); END IF; SELECT string_agg(row_text, E'\n' ORDER BY ord) INTO v_csv FROM ( SELECT 0 AS ord, 'Nombre,Apellido,Hoja,Proyecto,Dias,Jornal,Concepto,Cantidad,Unidad,Bruto,Descuentos,Prestamo,EtiquetaPrestamo,Requerido,APagar' AS row_text UNION ALL SELECT 1 AS ord, concat_ws(',', w.first_name, w.last_name_p, s.kind, COALESCE(p.name, ''), l.days, l.daily_wage, COALESCE(l.concepto, ''), COALESCE(l.qty_actual, 0) + COALESCE(l.qty_extra, 0), COALESCE(l.unit_code, ''), l.gross, l.discounts, l.loan_discount, replace(COALESCE(l.loan_label, ''), ',', ' '), l.required_net, l.payable_net ) FROM payroll_week_lines l JOIN payroll_sheets s ON s.id = l.sheet_id JOIN workers w ON w.id = l.worker_id LEFT JOIN projects p ON p.id = s.project_id WHERE s.week_id = v_week_id ORDER BY s.kind, p.name, w.last_name_p ) q; RETURN core.rpc_ok( jsonb_build_object('csv', v_csv), format('CSV de nómina generado para semana id=%s', v_week_id), jsonb_build_object('fn', 'fn_payroll_week_csv', 'week_id', v_week_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_week_csv', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-013i4-fn-loan-read endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_loan_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint; v_loans jsonb; BEGIN SELECT COALESCE(jsonb_agg(row_to_json(x)::jsonb ORDER BY x.id DESC), '[]'::jsonb) INTO v_loans FROM ( SELECT l.*, w.first_name, w.last_name_p FROM loans l JOIN workers w ON w.id = l.worker_id WHERE v_worker_id IS NULL OR l.worker_id = v_worker_id ) x; RETURN core.rpc_ok( jsonb_build_object('loans', v_loans), format('Préstamos listados: %s registro(s)', jsonb_array_length(v_loans)), jsonb_build_object('fn', 'fn_loan_list', 'worker_id', v_worker_id, 'count', jsonb_array_length(v_loans)) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_loan_list', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_loan_get(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_id bigint := NULLIF(payload->>'id', '')::bigint; v_loan jsonb; BEGIN IF v_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_loan_get: id es obligatorio', jsonb_build_object('fn', 'fn_loan_get', 'field', 'id') ); END IF; SELECT row_to_json(x)::jsonb INTO v_loan FROM ( SELECT l.*, w.first_name, w.middle_name, w.last_name_p, w.last_name_m FROM loans l JOIN workers w ON w.id = l.worker_id WHERE l.id = v_id ) x; IF v_loan IS NULL THEN RETURN core.rpc_err( 'NOT_FOUND', 'Préstamo no encontrado', jsonb_build_object('fn', 'fn_loan_get', 'id', v_id) ); END IF; RETURN core.rpc_ok( jsonb_build_object('loan', v_loan), format('Préstamo id=%s cargado', v_id), jsonb_build_object('fn', 'fn_loan_get', 'id', v_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_loan_get', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_loan_payment_get(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_id bigint := NULLIF(payload->>'id', '')::bigint; v_payment jsonb; BEGIN IF v_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_loan_payment_get: id es obligatorio', jsonb_build_object('fn', 'fn_loan_payment_get', 'field', 'id') ); END IF; SELECT row_to_json(x)::jsonb INTO v_payment FROM ( SELECT p.*, l.delivered, l.commission_pct, w.first_name, w.middle_name, w.last_name_p, w.last_name_m, wk.week_end FROM loan_payments p JOIN loans l ON l.id = p.loan_id JOIN workers w ON w.id = l.worker_id JOIN payroll_weeks wk ON wk.id = p.week_id WHERE p.id = v_id ) x; IF v_payment IS NULL THEN RETURN core.rpc_err( 'NOT_FOUND', 'Recibo no encontrado', jsonb_build_object('fn', 'fn_loan_payment_get', 'id', v_id) ); END IF; RETURN core.rpc_ok( jsonb_build_object('payment', v_payment), format('Pago de préstamo id=%s cargado', v_id), jsonb_build_object('fn', 'fn_loan_payment_get', 'id', v_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_loan_payment_get', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-013i5-fn-payroll-period endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_payroll_period_create(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint; v_week_start date := NULLIF(btrim(payload->>'week_start'), '')::date; v_week_end date := NULLIF(btrim(payload->>'week_end'), '')::date; v_extra jsonb := COALESCE(payload->'extra_discounts', '{}'::jsonb); v_period_id bigint; v_worker record; v_att_n integer; v_days integer; v_gross numeric; v_extra_disc numeric; v_net numeric; BEGIN IF v_project_id IS NULL OR v_week_start IS NULL OR v_week_end IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_period_create: project_id, week_start y week_end son obligatorios', jsonb_build_object('fn', 'fn_payroll_period_create') ); END IF; INSERT INTO payroll_periods (project_id, week_start, week_end, status) VALUES (v_project_id, v_week_start, v_week_end, 'draft') RETURNING id INTO v_period_id; FOR v_worker IN SELECT w.id, w.daily_wage FROM workers w JOIN assignments a ON a.worker_id = w.id AND a.project_id = v_project_id AND a.active = true WHERE w.status = 'activo' LOOP SELECT COUNT(*)::integer INTO v_att_n FROM attendance WHERE worker_id = v_worker.id AND project_id = v_project_id AND work_date BETWEEN v_week_start AND v_week_end AND present = true; v_days := v_att_n; v_gross := v_days * v_worker.daily_wage; v_extra_disc := COALESCE((v_extra->>v_worker.id::text)::numeric, 0); v_net := v_gross - v_extra_disc; INSERT INTO payroll_lines (period_id, worker_id, days, daily_wage, gross, discounts, loan_payment, net) VALUES (v_period_id, v_worker.id, v_days, v_worker.daily_wage, v_gross, v_extra_disc, 0, v_net); END LOOP; RETURN core.rpc_created( jsonb_build_object('id', v_period_id), format('Periodo de nómina %s creado para proyecto %s', v_period_id, v_project_id), jsonb_build_object('fn', 'fn_payroll_period_create', 'id', v_period_id, 'project_id', v_project_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_period_create', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_payroll_period_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint; v_periods jsonb; BEGIN SELECT COALESCE(jsonb_agg(row_to_json(x)::jsonb ORDER BY x.id DESC), '[]'::jsonb) INTO v_periods FROM ( SELECT pe.*, p.name AS project_name FROM payroll_periods pe JOIN projects p ON p.id = pe.project_id WHERE v_project_id IS NULL OR pe.project_id = v_project_id ) x; RETURN core.rpc_ok( jsonb_build_object('periods', v_periods), format('Periodos de nómina listados: %s registro(s)', jsonb_array_length(v_periods)), jsonb_build_object('fn', 'fn_payroll_period_list', 'project_id', v_project_id, 'count', jsonb_array_length(v_periods)) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_period_list', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_payroll_period_get(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_id bigint := NULLIF(payload->>'id', '')::bigint; v_period jsonb; v_lines jsonb; BEGIN IF v_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_period_get: id es obligatorio', jsonb_build_object('fn', 'fn_payroll_period_get', 'field', 'id') ); END IF; SELECT row_to_json(x)::jsonb INTO v_period FROM ( SELECT pe.*, p.name AS project_name FROM payroll_periods pe JOIN projects p ON p.id = pe.project_id WHERE pe.id = v_id ) x; IF v_period IS NULL THEN RETURN core.rpc_err( 'NOT_FOUND', 'Periodo no encontrado', jsonb_build_object('fn', 'fn_payroll_period_get', 'id', v_id) ); END IF; SELECT COALESCE(jsonb_agg(row_to_json(lx)::jsonb ORDER BY lx.last_name_p), '[]'::jsonb) INTO v_lines FROM ( SELECT l.*, w.first_name, w.last_name_p, w.position FROM payroll_lines l JOIN workers w ON w.id = l.worker_id WHERE l.period_id = v_id ) lx; RETURN core.rpc_ok( jsonb_build_object('period', v_period, 'lines', v_lines), format('Periodo de nómina %s cargado con %s línea(s)', v_id, jsonb_array_length(v_lines)), jsonb_build_object('fn', 'fn_payroll_period_get', 'id', v_id, 'line_count', jsonb_array_length(v_lines)) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_period_get', SQLSTATE, SQLERRM); END; $$; CREATE OR REPLACE FUNCTION core.fn_payroll_period_patch(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_id bigint := NULLIF(payload->>'id', '')::bigint; v_status text := btrim(COALESCE(payload->>'status', '')); BEGIN IF v_id IS NULL OR v_status = '' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_payroll_period_patch: id y status son obligatorios', jsonb_build_object('fn', 'fn_payroll_period_patch') ); END IF; IF NOT EXISTS (SELECT 1 FROM payroll_periods WHERE id = v_id) THEN RETURN core.rpc_err( 'NOT_FOUND', 'Periodo no encontrado', jsonb_build_object('fn', 'fn_payroll_period_patch', 'id', v_id) ); END IF; UPDATE payroll_periods SET status = v_status WHERE id = v_id; RETURN core.rpc_ok( jsonb_build_object('ok', true, 'id', v_id, 'status', v_status), format('Periodo de nómina %s actualizado a estado %s', v_id, v_status), jsonb_build_object('fn', 'fn_payroll_period_patch', 'id', v_id, 'status', v_status) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_period_patch', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-013j-fn-payroll-grants endDelimiter:; splitStatements:true GRANT EXECUTE ON FUNCTION core.round_money(numeric) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_settings_get(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_settings_save(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_unit_list(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_unit_add(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_attendance_list(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_attendance_set(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_loan_create(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_week_ensure(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_week_recalc(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_week_assemble(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_week_pay(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_week_bundle(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_destajo_job_create(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_destajo_cut_patch(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_admin_line_add(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_admin_line_update(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_admin_line_remove(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_jornal_worker_add(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_week_list_open(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_week_csv(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_loan_list(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_loan_get(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_loan_payment_get(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_period_create(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_period_list(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_period_get(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_payroll_period_patch(jsonb) TO panels_core_app;