mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 15:13:17 +00:00
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary
Migrates the `core` schema business logic from inline `db.prepare()` calls in the API to PostgreSQL RPC functions (`core.fn_*`) with a unified JSON envelope for errors and HTTP status mapping.
### Database (Liquibase changesets 006–017)
- **006** — RPC infra: `rpc_ok`, `rpc_err`, `rpc_created`, `rpc_from_exception`
- **007** — Catalogs and tenant settings
- **008** — Companies CRUD
- **009** — Projects, checklists, document lists
- **010** — Workers CRUD, pipeline, checklist, assign
- **011** — Budget CRUD + `fn_budget_replace`
- **012** — Badge jobs
- **013** — Payroll (settings, attendance, loans, weeks, destajo)
- **014** — Worker import batch + document store
- **015** — Project/company/worker document metadata RPCs
- **016–017** — Fixes: `needs_badge` default on worker create; Liquibase `splitStatements:false` on function changesets
### API
- `api/rpc.ts` — `callCoreFn()`, `RpcCallError` (jsonb payload fix: pass JS object, not `JSON.stringify`)
- `api/http_errors.ts` — `mapRpcToStatus()`, `respondRpc()`, `respondApiError()`, `onAppError()`
- Refactored: `main.ts`, `companies.ts`, `db.ts`, `budget.ts`, `excel.ts`, `payroll.ts`, `payroll_http.ts`
- Front helpers: `web-panel/composables/api-response.ts`, `web-saas/composables/api-response.ts`
### Envelope contract
DB functions return `{ ok, code, layer: "db", message, context, data, errors }`. The API adds `status` (HTTP code) via `respondRpc()` / `respondApiError()`.
### Out of scope
`iam`, `platform`, `saas.ts`, auth/sessions, S3, PDF generation, Excel parsing, and bootstrap scripts still use direct SQL where appropriate.
## Test plan
- [x] `deno check main.ts` — compila sin errores de tipos
- [x] `npm run build` — web-panel y web-saas compilan
- [x] `deno test` — 25 tests unitarios (http_errors, companies, budget, mx, document_validity)
- [x] Liquibase migrations `006`–`017` aplicadas en Postgres local (`--context-filter=dev`)
- [x] API levantada localmente; `/v1/health` OK
- [x] Smoke CRUD vía `scripts/crud-smoke-test.sh`: empresas, proyectos, trabajadores, catálogos (create/get/list/patch)
- [ ] Import Excel de trabajadores (flujo multipart + S3/local storage)
- [ ] Import presupuesto desde Excel
- [ ] Flujo nómina: asistencia → cerrar semana
- [ ] CI en el remoto (sin checks reportados aún)
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-06667c14-38e8-42a8-9ed9-6b1322f12ae7?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-06667c14-38e8-42a8-9ed9-6b1322f12ae7&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>
2032 lines
68 KiB
PL/PgSQL
2032 lines
68 KiB
PL/PgSQL
--liquibase formatted sql
|
|
-- PANELS · core · RPC nómina (port de api/payroll.ts)
|
|
|
|
--changeset panel:core-013a-payroll-helpers splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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;
|