--liquibase formatted sql -- PANELS · core · sincronización nómina/destajo → gastos --changeset panel:core-025a-fn-payroll-sync-cost-entries splitStatements:false CREATE OR REPLACE FUNCTION core.fn_payroll_sync_cost_entries(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_week_id bigint := NULLIF(payload->>'week_id', '')::bigint; v_tid integer := core._cost_tenant_id(payload); v_line record; v_company_id bigint; v_expense_date date; v_desc text; v_budget_item_id bigint; v_inserted integer := 0; BEGIN IF v_week_id IS NULL THEN RETURN core.rpc_err('VALIDATION', 'fn_payroll_sync_cost_entries: week_id es obligatorio', jsonb_build_object('fn', 'fn_payroll_sync_cost_entries')); END IF; SELECT week_end INTO v_expense_date FROM payroll_weeks WHERE id = v_week_id; FOR v_line IN SELECT l.id AS line_id, l.payable_net, l.concepto, l.project_id, l.worker_id, s.kind AS sheet_kind, s.project_id AS sheet_project_id, w.company_id AS worker_company_id, p.company_id AS project_company_id, dj.budget_item_id AS destajo_budget_item_id 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 = COALESCE(l.project_id, s.project_id) LEFT JOIN destajo_cut_lines dcl ON dcl.id = l.destajo_cut_line_id LEFT JOIN destajo_jobs dj ON dj.id = dcl.job_id WHERE s.week_id = v_week_id AND l.payable_net > 0 LOOP v_company_id := COALESCE(v_line.project_company_id, v_line.worker_company_id); IF v_company_id IS NULL THEN SELECT id INTO v_company_id FROM companies WHERE tenant_id = v_tid AND kind = 'principal' LIMIT 1; END IF; v_budget_item_id := v_line.destajo_budget_item_id; v_desc := coalesce(nullif(btrim(v_line.concepto), ''), CASE v_line.sheet_kind WHEN 'destajo' THEN 'Destajo semana ' || v_week_id WHEN 'obra' THEN 'Nómina obra semana ' || v_week_id ELSE 'Nómina admin semana ' || v_week_id END); IF EXISTS ( SELECT 1 FROM expense_entries WHERE source = 'payroll' AND source_ref_id = v_line.line_id AND status <> 'void' ) THEN UPDATE expense_entries SET subtotal = v_line.payable_net, total = v_line.payable_net, project_id = COALESCE(v_line.project_id, v_line.sheet_project_id), budget_item_id = v_budget_item_id, description = v_desc, expense_date = v_expense_date WHERE source = 'payroll' AND source_ref_id = v_line.line_id AND status <> 'void'; ELSE INSERT INTO expense_entries ( tenant_id, company_id, project_id, expense_date, source, source_ref_id, description, subtotal, iva_rate, iva_amount, total, tax_included, is_deductible, counts_toward_budget, is_locked, status, budget_item_id, created_by_name ) VALUES ( v_tid, v_company_id, COALESCE(v_line.project_id, v_line.sheet_project_id), v_expense_date, 'payroll', v_line.line_id, v_desc, v_line.payable_net, 0, 0, v_line.payable_net, false, false, true, true, 'confirmed', v_budget_item_id, 'nómina' ); END IF; v_inserted := v_inserted + 1; END LOOP; RETURN core.rpc_ok( jsonb_build_object('week_id', v_week_id, 'lines_synced', v_inserted), format('Sincronizados %s gastos de nómina de la semana %s', v_inserted, v_week_id), jsonb_build_object('fn', 'fn_payroll_sync_cost_entries', 'week_id', v_week_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_payroll_sync_cost_entries', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-025b-fn-payroll-week-pay-with-costs splitStatements:false 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; v_sync 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; v_sync := core.fn_payroll_sync_cost_entries(jsonb_build_object('week_id', v_week_id, 'tenant_id', v_tid)); RETURN core.rpc_ok( jsonb_build_object('week_id', v_week_id, 'status', 'paid', 'cost_sync', v_sync->'data'), format('Semana de nómina %s marcada como pagada y costos sincronizados', 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; $$; --changeset panel:core-025c-payroll-cost-grants endDelimiter:; splitStatements:true GRANT EXECUTE ON FUNCTION core.fn_payroll_sync_cost_entries(jsonb) TO panels_core_app;