mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 18:03:18 +00:00
Reutiliza el catálogo de proveedores y agrega carga de facturas, conceptos de la semana, préstamos editables, finiquitos, dispersión, historial de personal, reportes y jornal de cotización. Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
715 lines
25 KiB
PL/PgSQL
715 lines
25 KiB
PL/PgSQL
--liquibase formatted sql
|
|
-- PANELS · core · bono, descuentos, horas y días festivos
|
|
|
|
--changeset panel:core-044a-payroll-adjustments endDelimiter:; splitStatements:true
|
|
CREATE TABLE IF NOT EXISTS core.holidays (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
holiday_date DATE NOT NULL,
|
|
name TEXT NOT NULL,
|
|
UNIQUE (tenant_id, holiday_date)
|
|
);
|
|
ALTER TABLE core.holidays ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation ON core.holidays;
|
|
CREATE POLICY tenant_isolation ON core.holidays
|
|
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
|
|
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON core.holidays TO panels_core_app;
|
|
|
|
CREATE TABLE IF NOT EXISTS core.payroll_adjustments (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
week_id BIGINT NOT NULL REFERENCES core.payroll_weeks(id) ON DELETE CASCADE,
|
|
worker_id BIGINT NOT NULL REFERENCES core.workers(id),
|
|
project_id BIGINT REFERENCES core.projects(id) ON DELETE SET NULL,
|
|
kind TEXT NOT NULL CHECK (kind IN ('descuento', 'bono', 'horas')),
|
|
concept TEXT NOT NULL,
|
|
amount NUMERIC(14,2) NOT NULL DEFAULT 0,
|
|
hours NUMERIC(8,2) NOT NULL DEFAULT 0,
|
|
note TEXT NOT NULL DEFAULT '',
|
|
adjustment_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_core_payroll_adjustments_week ON core.payroll_adjustments (week_id, worker_id);
|
|
ALTER TABLE core.payroll_adjustments ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation ON core.payroll_adjustments;
|
|
CREATE POLICY tenant_isolation ON core.payroll_adjustments
|
|
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
|
|
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON core.payroll_adjustments TO panels_core_app;
|
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA core TO panels_core_app;
|
|
|
|
--changeset panel:core-044b-fn-apply-adjustments splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core._payroll_apply_adjustments(p_week_id bigint)
|
|
RETURNS void
|
|
LANGUAGE plpgsql
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_adj record;
|
|
v_line_id bigint;
|
|
v_wage numeric;
|
|
v_extra numeric;
|
|
BEGIN
|
|
FOR v_adj IN
|
|
SELECT * FROM payroll_adjustments WHERE week_id = p_week_id ORDER BY id
|
|
LOOP
|
|
SELECT l.id INTO v_line_id
|
|
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_adj.worker_id
|
|
AND (v_adj.project_id IS NULL OR s.project_id = v_adj.project_id OR l.project_id = v_adj.project_id)
|
|
ORDER BY CASE WHEN s.project_id IS NOT DISTINCT FROM v_adj.project_id THEN 0 ELSE 1 END, l.gross DESC, l.id
|
|
LIMIT 1;
|
|
IF v_line_id IS NULL THEN
|
|
CONTINUE;
|
|
END IF;
|
|
IF v_adj.kind = 'bono' THEN
|
|
UPDATE payroll_week_lines
|
|
SET gross = core.round_money(gross + v_adj.amount),
|
|
payable_net = core.round_money(payable_net + v_adj.amount)
|
|
WHERE id = v_line_id;
|
|
ELSIF v_adj.kind = 'descuento' THEN
|
|
UPDATE payroll_week_lines
|
|
SET discounts = core.round_money(discounts + v_adj.amount),
|
|
payable_net = core.round_money(payable_net - v_adj.amount)
|
|
WHERE id = v_line_id;
|
|
ELSIF v_adj.kind = 'horas' THEN
|
|
SELECT w.daily_wage INTO v_wage FROM workers w WHERE w.id = v_adj.worker_id;
|
|
v_extra := core.round_money(v_adj.hours * COALESCE(v_wage, 0) / 8);
|
|
UPDATE payroll_week_lines
|
|
SET gross = core.round_money(gross + v_extra),
|
|
payable_net = core.round_money(payable_net + v_extra)
|
|
WHERE id = v_line_id;
|
|
END IF;
|
|
END LOOP;
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-044c-recalc-week 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_holiday_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, discounts = 0
|
|
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 COUNT(*)::integer INTO v_holiday_n
|
|
FROM holidays h
|
|
WHERE h.tenant_id = v_week.tenant_id
|
|
AND h.holiday_date BETWEEN v_week.week_start AND v_week.week_end
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM attendance att
|
|
WHERE att.worker_id = v_worker.id
|
|
AND att.project_id = v_sheet.project_id
|
|
AND att.work_date = h.holiday_date
|
|
);
|
|
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_holiday_n) * v_wage);
|
|
UPDATE payroll_week_lines
|
|
SET days = v_att_n + v_holiday_n,
|
|
daily_wage = v_wage,
|
|
amount = v_gross,
|
|
gross = v_gross,
|
|
discounts = 0,
|
|
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;
|
|
|
|
PERFORM core._payroll_apply_adjustments(p_week_id);
|
|
|
|
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-044d-fn-adjustments splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_payroll_adjustment_save(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_tid integer := core._payroll_tenant_id(payload);
|
|
v_id bigint := NULLIF(payload->>'id', '')::bigint;
|
|
v_week_id bigint := NULLIF(payload->>'week_id', '')::bigint;
|
|
v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint;
|
|
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
|
v_kind text := nullif(btrim(payload->>'kind'), '');
|
|
v_concept text := coalesce(nullif(btrim(payload->>'concept'), ''), '');
|
|
v_note text := coalesce(nullif(btrim(payload->>'note'), ''), '');
|
|
v_amount numeric := COALESCE((payload->>'amount')::numeric, 0);
|
|
v_hours numeric := COALESCE((payload->>'hours')::numeric, 0);
|
|
v_date date := COALESCE(NULLIF(payload->>'adjustment_date', '')::date, CURRENT_DATE);
|
|
v_status text;
|
|
BEGIN
|
|
IF v_week_id IS NULL OR v_worker_id IS NULL OR v_kind IS NULL OR v_concept = '' THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_payroll_adjustment_save: semana, persona, tipo y concepto son obligatorios',
|
|
jsonb_build_object('fn', 'fn_payroll_adjustment_save'));
|
|
END IF;
|
|
IF v_kind NOT IN ('descuento', 'bono', 'horas') THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_payroll_adjustment_save: tipo inválido',
|
|
jsonb_build_object('fn', 'fn_payroll_adjustment_save', 'kind', v_kind));
|
|
END IF;
|
|
IF v_kind = 'descuento' AND v_note = '' THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_payroll_adjustment_save: el descuento requiere observación',
|
|
jsonb_build_object('fn', 'fn_payroll_adjustment_save', 'field', 'note'));
|
|
END IF;
|
|
SELECT status INTO v_status FROM payroll_weeks WHERE id = v_week_id AND tenant_id = v_tid;
|
|
IF NOT FOUND THEN
|
|
RETURN core.rpc_err('NOT_FOUND', format('fn_payroll_adjustment_save: semana id=%s no encontrada', v_week_id),
|
|
jsonb_build_object('fn', 'fn_payroll_adjustment_save', 'week_id', v_week_id));
|
|
END IF;
|
|
IF v_status = 'paid' THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_payroll_adjustment_save: la semana ya está pagada',
|
|
jsonb_build_object('fn', 'fn_payroll_adjustment_save', 'week_id', v_week_id));
|
|
END IF;
|
|
IF v_id IS NULL THEN
|
|
INSERT INTO payroll_adjustments (
|
|
tenant_id, week_id, worker_id, project_id, kind, concept, amount, hours, note, adjustment_date
|
|
) VALUES (
|
|
v_tid, v_week_id, v_worker_id, v_project_id, v_kind, v_concept, v_amount, v_hours, v_note, v_date
|
|
) RETURNING id INTO v_id;
|
|
ELSE
|
|
UPDATE payroll_adjustments SET
|
|
project_id = v_project_id, kind = v_kind, concept = v_concept,
|
|
amount = v_amount, hours = v_hours, note = v_note, adjustment_date = v_date
|
|
WHERE id = v_id AND tenant_id = v_tid AND week_id = v_week_id;
|
|
END IF;
|
|
PERFORM core._payroll_recalc_week(v_week_id);
|
|
RETURN core.rpc_ok(
|
|
jsonb_build_object('id', v_id),
|
|
format('Ajuste de nómina %s guardado', v_kind),
|
|
jsonb_build_object('fn', 'fn_payroll_adjustment_save', 'id', v_id)
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_payroll_adjustment_save', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core.fn_payroll_adjustment_list(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_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint;
|
|
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
|
v_kind text := nullif(btrim(payload->>'kind'), '');
|
|
v_rows jsonb;
|
|
BEGIN
|
|
SELECT COALESCE(jsonb_agg(to_jsonb(x) ORDER BY x.adjustment_date DESC, x.id DESC), '[]'::jsonb)
|
|
INTO v_rows
|
|
FROM (
|
|
SELECT a.*, w.first_name, w.last_name_p, p.name AS project_name
|
|
FROM payroll_adjustments a
|
|
JOIN workers w ON w.id = a.worker_id
|
|
LEFT JOIN projects p ON p.id = a.project_id
|
|
WHERE a.tenant_id = v_tid
|
|
AND (v_week_id IS NULL OR a.week_id = v_week_id)
|
|
AND (v_worker_id IS NULL OR a.worker_id = v_worker_id)
|
|
AND (v_project_id IS NULL OR a.project_id = v_project_id)
|
|
AND (v_kind IS NULL OR a.kind = v_kind)
|
|
) x;
|
|
RETURN core.rpc_ok(
|
|
jsonb_build_object('adjustments', v_rows, 'count', jsonb_array_length(v_rows)),
|
|
format('Ajustes de nómina: %s', jsonb_array_length(v_rows)),
|
|
jsonb_build_object('fn', 'fn_payroll_adjustment_list')
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_payroll_adjustment_list', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core.fn_payroll_adjustment_delete(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_tid integer := core._payroll_tenant_id(payload);
|
|
v_id bigint := NULLIF(payload->>'id', '')::bigint;
|
|
v_week_id bigint;
|
|
v_status text;
|
|
BEGIN
|
|
SELECT a.week_id, wk.status INTO v_week_id, v_status
|
|
FROM payroll_adjustments a
|
|
JOIN payroll_weeks wk ON wk.id = a.week_id
|
|
WHERE a.id = v_id AND a.tenant_id = v_tid;
|
|
IF NOT FOUND THEN
|
|
RETURN core.rpc_err('NOT_FOUND', 'Ajuste no encontrado',
|
|
jsonb_build_object('fn', 'fn_payroll_adjustment_delete', 'id', v_id));
|
|
END IF;
|
|
IF v_status = 'paid' THEN
|
|
RETURN core.rpc_err('VALIDATION', 'La semana ya está pagada',
|
|
jsonb_build_object('fn', 'fn_payroll_adjustment_delete', 'week_id', v_week_id));
|
|
END IF;
|
|
DELETE FROM payroll_adjustments WHERE id = v_id;
|
|
PERFORM core._payroll_recalc_week(v_week_id);
|
|
RETURN core.rpc_ok(jsonb_build_object('id', v_id), 'Ajuste eliminado',
|
|
jsonb_build_object('fn', 'fn_payroll_adjustment_delete', 'id', v_id));
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_payroll_adjustment_delete', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core.fn_holiday_save(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_tid integer := core._payroll_tenant_id(payload);
|
|
v_date date := NULLIF(payload->>'holiday_date', '')::date;
|
|
v_name text := coalesce(nullif(btrim(payload->>'name'), ''), '');
|
|
v_id bigint;
|
|
v_week record;
|
|
BEGIN
|
|
IF v_date IS NULL OR v_name = '' THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_holiday_save: fecha y nombre son obligatorios',
|
|
jsonb_build_object('fn', 'fn_holiday_save'));
|
|
END IF;
|
|
INSERT INTO holidays (tenant_id, holiday_date, name)
|
|
VALUES (v_tid, v_date, v_name)
|
|
ON CONFLICT (tenant_id, holiday_date) DO UPDATE SET name = EXCLUDED.name
|
|
RETURNING id INTO v_id;
|
|
FOR v_week IN
|
|
SELECT id FROM payroll_weeks
|
|
WHERE tenant_id = v_tid AND status <> 'paid'
|
|
AND v_date BETWEEN week_start AND week_end
|
|
LOOP
|
|
PERFORM core._payroll_recalc_week(v_week.id);
|
|
END LOOP;
|
|
RETURN core.rpc_ok(jsonb_build_object('id', v_id), format('Festivo %s registrado', v_name),
|
|
jsonb_build_object('fn', 'fn_holiday_save', 'id', v_id));
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_holiday_save', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core.fn_holiday_list(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_tid integer := core._payroll_tenant_id(payload);
|
|
v_from date := NULLIF(payload->>'date_from', '')::date;
|
|
v_to date := NULLIF(payload->>'date_to', '')::date;
|
|
v_rows jsonb;
|
|
BEGIN
|
|
SELECT COALESCE(jsonb_agg(to_jsonb(h) ORDER BY h.holiday_date), '[]'::jsonb)
|
|
INTO v_rows
|
|
FROM holidays h
|
|
WHERE h.tenant_id = v_tid
|
|
AND (v_from IS NULL OR h.holiday_date >= v_from)
|
|
AND (v_to IS NULL OR h.holiday_date <= v_to);
|
|
RETURN core.rpc_ok(jsonb_build_object('holidays', v_rows), 'Festivos cargados',
|
|
jsonb_build_object('fn', 'fn_holiday_list'));
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_holiday_list', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-044e-grants endDelimiter:; splitStatements:true
|
|
GRANT EXECUTE ON FUNCTION core._payroll_apply_adjustments(bigint) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_payroll_adjustment_save(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_payroll_adjustment_list(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_payroll_adjustment_delete(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_holiday_save(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_holiday_list(jsonb) TO panels_core_app;
|
|
|