mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 11:23:18 +00:00
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Resumen Implementa las observaciones de facturas, nómina e IMSS sobre el catálogo de proveedores y la nómina semanal que ya existían. ## Qué queda disponible - Proveedores: régimen fiscal, código postal, estatus activo/inactivo y filtro por nombre o RFC. - Facturas recibidas y emitidas: Excel, UUID, revisión si el RFC no está en el catálogo, clasificación a obra/concepto y gasto `source = invoice` solo para la recibida clasificada. - Nómina: bono, descuento con observación, horas y día festivo dentro del armado semanal. La semana pagada no acepta cambios. - Préstamos: edición (libre sin pagos; con pagos solo nota y cuota) y estado de cuenta con saldo = entregado + comisión − pagos. - Finiquitos y liquidaciones: conceptos, neto descontando descuentos vigentes y saldo de préstamo, y gasto al pagar. - Dispersión: corte semanal, corrección antes de definitiva y comprobante. - Historial de alta, baja y reingreso en la ficha, más movimientos IMSS. - Reporte por periodo en Excel y PDF. - Jornal IMSS distinto del jornal real y consulta de cuotas exportable a Excel. ## Verificación `deno check` de la API pasó. En este entorno no hay Postgres, así que `api/pendientes_test.ts` queda listo para correr cuando la base tenga las migraciones 042–050 y el permiso IAM 007. <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-5e23dc3b-921e-4091-9650-7091d3a52bbf?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-5e23dc3b-921e-4091-9650-7091d3a52bbf&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>
104 lines
5 KiB
PL/PgSQL
104 lines
5 KiB
PL/PgSQL
--liquibase formatted sql
|
|
-- PANELS · core · historial de alta, baja y reingreso
|
|
|
|
--changeset panel:core-048a-table endDelimiter:; splitStatements:true
|
|
ALTER TABLE core.workers ADD COLUMN IF NOT EXISTS imss_daily_wage NUMERIC(12,2);
|
|
CREATE TABLE IF NOT EXISTS core.worker_movements (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
tenant_id INTEGER,
|
|
worker_id BIGINT NOT NULL REFERENCES core.workers(id) ON DELETE CASCADE,
|
|
kind TEXT NOT NULL CHECK (kind IN ('alta', 'baja', 'reingreso', 'alta_imss', 'baja_imss', 'modificacion_salario')),
|
|
movement_date DATE NOT NULL,
|
|
note TEXT NOT NULL DEFAULT '',
|
|
actor_name TEXT NOT NULL DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_core_worker_movements_worker ON core.worker_movements (worker_id, movement_date, id);
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON core.worker_movements TO panels_core_app;
|
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA core TO panels_core_app;
|
|
|
|
--changeset panel:core-048b-backfill endDelimiter:; splitStatements:true
|
|
INSERT INTO core.worker_movements (tenant_id, worker_id, kind, movement_date, note)
|
|
SELECT w.tenant_id, w.id, 'alta', COALESCE(w.created_at::date, CURRENT_DATE), 'Alta de padrón'
|
|
FROM core.workers w
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM core.worker_movements m WHERE m.worker_id = w.id AND m.kind = 'alta'
|
|
);
|
|
INSERT INTO core.worker_movements (tenant_id, worker_id, kind, movement_date, note)
|
|
SELECT w.tenant_id, w.id, 'reingreso', w.last_rehire_at, 'Reingreso'
|
|
FROM core.workers w
|
|
WHERE w.last_rehire_at IS NOT NULL
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM core.worker_movements m WHERE m.worker_id = w.id AND m.kind = 'reingreso' AND m.movement_date = w.last_rehire_at
|
|
);
|
|
INSERT INTO core.worker_movements (tenant_id, worker_id, kind, movement_date, note)
|
|
SELECT w.tenant_id, w.id, 'baja', COALESCE(w.updated_at::date, CURRENT_DATE), 'Baja de padrón'
|
|
FROM core.workers w
|
|
WHERE w.status = 'baja'
|
|
AND NOT EXISTS (SELECT 1 FROM core.worker_movements m WHERE m.worker_id = w.id AND m.kind = 'baja');
|
|
|
|
--changeset panel:core-048c-trigger splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.trg_worker_movement()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SET search_path = core
|
|
AS $$
|
|
BEGIN
|
|
IF TG_OP = 'INSERT' THEN
|
|
INSERT INTO worker_movements (tenant_id, worker_id, kind, movement_date, note)
|
|
VALUES (NEW.tenant_id, NEW.id, 'alta', COALESCE(NEW.created_at::date, CURRENT_DATE), 'Alta de padrón');
|
|
RETURN NEW;
|
|
END IF;
|
|
IF OLD.status IS DISTINCT FROM 'baja' AND NEW.status = 'baja' THEN
|
|
INSERT INTO worker_movements (tenant_id, worker_id, kind, movement_date, note)
|
|
VALUES (NEW.tenant_id, NEW.id, 'baja', CURRENT_DATE, 'Baja de padrón');
|
|
ELSIF OLD.status = 'baja' AND NEW.status = 'activo' THEN
|
|
INSERT INTO worker_movements (tenant_id, worker_id, kind, movement_date, note)
|
|
VALUES (NEW.tenant_id, NEW.id, 'reingreso', COALESCE(NEW.last_rehire_at, CURRENT_DATE), 'Reingreso');
|
|
END IF;
|
|
IF OLD.imss_status IS DISTINCT FROM 'alta' AND NEW.imss_status = 'alta' THEN
|
|
INSERT INTO worker_movements (tenant_id, worker_id, kind, movement_date, note)
|
|
VALUES (NEW.tenant_id, NEW.id, 'alta_imss', COALESCE(NEW.imss_alta_at, CURRENT_DATE), 'Alta IMSS');
|
|
ELSIF OLD.imss_status IS DISTINCT FROM 'baja_imss' AND NEW.imss_status = 'baja_imss' THEN
|
|
INSERT INTO worker_movements (tenant_id, worker_id, kind, movement_date, note)
|
|
VALUES (NEW.tenant_id, NEW.id, 'baja_imss', COALESCE(NEW.imss_baja_at, CURRENT_DATE), 'Baja IMSS');
|
|
END IF;
|
|
IF NEW.imss_daily_wage IS NOT NULL AND NEW.imss_daily_wage IS DISTINCT FROM OLD.imss_daily_wage THEN
|
|
INSERT INTO worker_movements (tenant_id, worker_id, kind, movement_date, note)
|
|
VALUES (NEW.tenant_id, NEW.id, 'modificacion_salario', CURRENT_DATE,
|
|
format('Jornal IMSS %s', NEW.imss_daily_wage));
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
DROP TRIGGER IF EXISTS trg_worker_movement ON core.workers;
|
|
CREATE TRIGGER trg_worker_movement
|
|
AFTER INSERT OR UPDATE OF status, imss_status, imss_daily_wage ON core.workers
|
|
FOR EACH ROW EXECUTE FUNCTION core.trg_worker_movement();
|
|
|
|
--changeset panel:core-048d-fn-list splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_worker_movement_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_rows jsonb;
|
|
BEGIN
|
|
SELECT COALESCE(jsonb_agg(to_jsonb(m) ORDER BY m.movement_date, m.id), '[]'::jsonb)
|
|
INTO v_rows
|
|
FROM worker_movements m
|
|
WHERE m.worker_id = v_worker_id;
|
|
RETURN core.rpc_ok(jsonb_build_object('movements', v_rows), 'Historial de movimientos',
|
|
jsonb_build_object('fn', 'fn_worker_movement_list', 'worker_id', v_worker_id));
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_worker_movement_list', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-048e-grants endDelimiter:; splitStatements:true
|
|
GRANT EXECUTE ON FUNCTION core.trg_worker_movement() TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_worker_movement_list(jsonb) TO panels_core_app;
|