mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 16:13:17 +00:00
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Validación de endpoints y SPs Se probaron todos los endpoints nuevos (gastos, almacén, control presupuestal, IAM) y sus RPCs asociados. ### Bugs corregidos en migraciones/SQL 1. **IAM grants en schema core** — `iam-004e` intentaba `GRANT` sobre `core` con rol `iam_owner` (sin permiso). Los grants cruzados se movieron a `core-027-iam-rpc-cross-grants.sql`. 2. **`_cost_settings` ambiguo** — columnas `budget_warn_pct` etc. colisionaban con `RETURNS TABLE` en PL/pgSQL, rompiendo `fn_expense_create` y control presupuestal. Corregido en `core-028-fix-cost-settings-ambiguous.sql`. ### Tests añadidos - `api/cost_modules_test.ts` — CRUD RPC gastos, flujo almacén completo, control presupuestal, IAM permisos - `scripts/crud-smoke-test.sh` — smoke HTTP de todos los endpoints nuevos ### Resultados - `deno test cost_modules_test.ts` — 5/5 OK - `./scripts/crud-smoke-test.sh` — todos los checks OK (gastos CRUD, IVA, cost-settings, almacén, transferencias, cost-control, IAM) <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-eeec3b5c-f789-43e8-a353-0755e4706377?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-eeec3b5c-f789-43e8-a353-0755e4706377&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
197 lines
7.5 KiB
PL/PgSQL
197 lines
7.5 KiB
PL/PgSQL
--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;
|