- 30-settings.sql: system_settings, plan_settings, tenant_settings, user_preferences (4 tables) - 31-reports.sql: report_definitions, executions, schedules, dashboards, widgets (12 tables, 7 enums) - 45-hr.sql: employees, departments, job_positions, contracts, leave_types, leaves (7 tables, 6 enums) Includes RLS policies, triggers, and utility functions for each module. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
667 lines
22 KiB
PL/PgSQL
667 lines
22 KiB
PL/PgSQL
-- =============================================================
|
|
-- ARCHIVO: 45-hr.sql
|
|
-- DESCRIPCION: Sistema de Recursos Humanos (RRHH)
|
|
-- VERSION: 1.0.0
|
|
-- PROYECTO: ERP-Core
|
|
-- FECHA: 2026-01-26
|
|
-- MODULO: MGN-010 (RRHH Basico)
|
|
-- REFERENCIA: odoo-hr-analysis.md, hr-domain.md
|
|
-- =============================================================
|
|
|
|
-- =====================
|
|
-- SCHEMA: hr
|
|
-- =====================
|
|
CREATE SCHEMA IF NOT EXISTS hr;
|
|
|
|
-- =====================
|
|
-- ENUMS
|
|
-- =====================
|
|
|
|
-- Estado del empleado
|
|
DO $$ BEGIN
|
|
CREATE TYPE hr.employee_status AS ENUM (
|
|
'active',
|
|
'inactive',
|
|
'on_leave',
|
|
'terminated'
|
|
);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
-- Tipo de contrato
|
|
DO $$ BEGIN
|
|
CREATE TYPE hr.contract_type AS ENUM (
|
|
'permanent',
|
|
'temporary',
|
|
'contractor',
|
|
'internship',
|
|
'part_time'
|
|
);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
-- Estado del contrato
|
|
DO $$ BEGIN
|
|
CREATE TYPE hr.contract_status AS ENUM (
|
|
'draft',
|
|
'active',
|
|
'expired',
|
|
'terminated',
|
|
'cancelled'
|
|
);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
-- Tipo de ausencia
|
|
DO $$ BEGIN
|
|
CREATE TYPE hr.leave_type_category AS ENUM (
|
|
'vacation',
|
|
'sick',
|
|
'personal',
|
|
'maternity',
|
|
'paternity',
|
|
'bereavement',
|
|
'unpaid',
|
|
'other'
|
|
);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
-- Estado de ausencia
|
|
DO $$ BEGIN
|
|
CREATE TYPE hr.leave_status AS ENUM (
|
|
'draft',
|
|
'submitted',
|
|
'approved',
|
|
'rejected',
|
|
'cancelled'
|
|
);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
-- Tipo de asignacion
|
|
DO $$ BEGIN
|
|
CREATE TYPE hr.allocation_type AS ENUM (
|
|
'fixed',
|
|
'accrual',
|
|
'unlimited'
|
|
);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
-- =====================
|
|
-- TABLA: hr.departments
|
|
-- Departamentos organizacionales
|
|
-- =====================
|
|
CREATE TABLE IF NOT EXISTS hr.departments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
company_id UUID NOT NULL REFERENCES core.companies(id) ON DELETE CASCADE,
|
|
|
|
-- Identificacion
|
|
name VARCHAR(255) NOT NULL,
|
|
code VARCHAR(50),
|
|
|
|
-- Jerarquia
|
|
parent_id UUID REFERENCES hr.departments(id) ON DELETE SET NULL,
|
|
manager_id UUID, -- FK a employees (se agrega despues)
|
|
|
|
-- Estado
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
|
|
-- Metadata
|
|
description TEXT,
|
|
color VARCHAR(7),
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
UNIQUE(tenant_id, company_id, code)
|
|
);
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_departments_tenant ON hr.departments(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_departments_company ON hr.departments(company_id);
|
|
CREATE INDEX IF NOT EXISTS idx_departments_parent ON hr.departments(parent_id);
|
|
CREATE INDEX IF NOT EXISTS idx_departments_active ON hr.departments(is_active) WHERE is_active = true;
|
|
|
|
-- =====================
|
|
-- TABLA: hr.job_positions
|
|
-- Puestos de trabajo
|
|
-- =====================
|
|
CREATE TABLE IF NOT EXISTS hr.job_positions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
company_id UUID NOT NULL REFERENCES core.companies(id) ON DELETE CASCADE,
|
|
|
|
-- Identificacion
|
|
name VARCHAR(255) NOT NULL,
|
|
code VARCHAR(50),
|
|
description TEXT,
|
|
|
|
-- Relaciones
|
|
department_id UUID REFERENCES hr.departments(id) ON DELETE SET NULL,
|
|
|
|
-- Estado
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
UNIQUE(tenant_id, company_id, code)
|
|
);
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_job_positions_tenant ON hr.job_positions(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_job_positions_department ON hr.job_positions(department_id);
|
|
CREATE INDEX IF NOT EXISTS idx_job_positions_active ON hr.job_positions(is_active) WHERE is_active = true;
|
|
|
|
-- =====================
|
|
-- TABLA: hr.employees
|
|
-- Empleados
|
|
-- =====================
|
|
CREATE TABLE IF NOT EXISTS hr.employees (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
company_id UUID NOT NULL REFERENCES core.companies(id) ON DELETE CASCADE,
|
|
|
|
-- Identificacion
|
|
employee_number VARCHAR(50) NOT NULL,
|
|
first_name VARCHAR(100) NOT NULL,
|
|
last_name VARCHAR(100) NOT NULL,
|
|
full_name VARCHAR(255) GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED,
|
|
|
|
-- Relaciones con usuario y partner
|
|
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
partner_id UUID, -- REFERENCES partners.partners(id)
|
|
|
|
-- Organizacion
|
|
department_id UUID REFERENCES hr.departments(id) ON DELETE SET NULL,
|
|
job_position_id UUID REFERENCES hr.job_positions(id) ON DELETE SET NULL,
|
|
manager_id UUID REFERENCES hr.employees(id) ON DELETE SET NULL,
|
|
|
|
-- Contacto laboral
|
|
work_email VARCHAR(255),
|
|
work_phone VARCHAR(50),
|
|
work_mobile VARCHAR(50),
|
|
work_location VARCHAR(255),
|
|
|
|
-- Datos personales
|
|
personal_email VARCHAR(255),
|
|
personal_phone VARCHAR(50),
|
|
birth_date DATE,
|
|
gender VARCHAR(20),
|
|
marital_status VARCHAR(20),
|
|
|
|
-- Identificacion oficial
|
|
identification_type VARCHAR(50),
|
|
identification_number VARCHAR(100),
|
|
tax_id VARCHAR(50), -- RFC en Mexico
|
|
social_security_number VARCHAR(50), -- NSS/IMSS
|
|
|
|
-- Direccion
|
|
street VARCHAR(255),
|
|
city VARCHAR(100),
|
|
state VARCHAR(100),
|
|
postal_code VARCHAR(20),
|
|
country VARCHAR(100),
|
|
|
|
-- Empleo
|
|
hire_date DATE NOT NULL,
|
|
termination_date DATE,
|
|
status hr.employee_status NOT NULL DEFAULT 'active',
|
|
|
|
-- Emergencia
|
|
emergency_contact_name VARCHAR(255),
|
|
emergency_contact_phone VARCHAR(50),
|
|
emergency_contact_relationship VARCHAR(50),
|
|
|
|
-- Metadata
|
|
notes TEXT,
|
|
avatar_url TEXT,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
UNIQUE(tenant_id, employee_number)
|
|
);
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_employees_tenant ON hr.employees(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_employees_company ON hr.employees(company_id);
|
|
CREATE INDEX IF NOT EXISTS idx_employees_department ON hr.employees(department_id);
|
|
CREATE INDEX IF NOT EXISTS idx_employees_job ON hr.employees(job_position_id);
|
|
CREATE INDEX IF NOT EXISTS idx_employees_manager ON hr.employees(manager_id);
|
|
CREATE INDEX IF NOT EXISTS idx_employees_user ON hr.employees(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_employees_status ON hr.employees(status);
|
|
CREATE INDEX IF NOT EXISTS idx_employees_active ON hr.employees(status) WHERE status = 'active';
|
|
CREATE INDEX IF NOT EXISTS idx_employees_name ON hr.employees(full_name);
|
|
|
|
-- Agregar FK de manager_id en departments
|
|
ALTER TABLE hr.departments
|
|
ADD CONSTRAINT fk_departments_manager
|
|
FOREIGN KEY (manager_id) REFERENCES hr.employees(id) ON DELETE SET NULL;
|
|
|
|
-- =====================
|
|
-- TABLA: hr.contracts
|
|
-- Contratos laborales
|
|
-- =====================
|
|
CREATE TABLE IF NOT EXISTS hr.contracts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
company_id UUID NOT NULL REFERENCES core.companies(id) ON DELETE CASCADE,
|
|
|
|
-- Relaciones
|
|
employee_id UUID NOT NULL REFERENCES hr.employees(id) ON DELETE CASCADE,
|
|
|
|
-- Identificacion
|
|
name VARCHAR(255) NOT NULL,
|
|
reference VARCHAR(50),
|
|
|
|
-- Tipo y estado
|
|
contract_type hr.contract_type NOT NULL DEFAULT 'permanent',
|
|
status hr.contract_status NOT NULL DEFAULT 'draft',
|
|
|
|
-- Vigencia
|
|
date_start DATE NOT NULL,
|
|
date_end DATE,
|
|
|
|
-- Puesto y departamento al momento del contrato
|
|
job_position_id UUID REFERENCES hr.job_positions(id) ON DELETE SET NULL,
|
|
department_id UUID REFERENCES hr.departments(id) ON DELETE SET NULL,
|
|
|
|
-- Compensacion
|
|
wage DECIMAL(15,2) NOT NULL,
|
|
wage_type VARCHAR(20) NOT NULL DEFAULT 'monthly' CHECK (wage_type IN ('hourly', 'daily', 'weekly', 'biweekly', 'monthly', 'annual')),
|
|
currency VARCHAR(3) NOT NULL DEFAULT 'MXN',
|
|
|
|
-- Jornada
|
|
hours_per_week DECIMAL(5,2) DEFAULT 48,
|
|
schedule_type VARCHAR(50),
|
|
|
|
-- Probatorio
|
|
trial_period_months INTEGER DEFAULT 0,
|
|
trial_date_end DATE,
|
|
|
|
-- Metadata
|
|
notes TEXT,
|
|
document_url TEXT,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
activated_at TIMESTAMPTZ,
|
|
terminated_at TIMESTAMPTZ,
|
|
terminated_by UUID REFERENCES auth.users(id),
|
|
termination_reason TEXT
|
|
);
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_contracts_tenant ON hr.contracts(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_contracts_employee ON hr.contracts(employee_id);
|
|
CREATE INDEX IF NOT EXISTS idx_contracts_status ON hr.contracts(status);
|
|
CREATE INDEX IF NOT EXISTS idx_contracts_active ON hr.contracts(employee_id, status) WHERE status = 'active';
|
|
CREATE INDEX IF NOT EXISTS idx_contracts_dates ON hr.contracts(date_start, date_end);
|
|
|
|
-- =====================
|
|
-- TABLA: hr.leave_types
|
|
-- Tipos de ausencia
|
|
-- =====================
|
|
CREATE TABLE IF NOT EXISTS hr.leave_types (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
company_id UUID NOT NULL REFERENCES core.companies(id) ON DELETE CASCADE,
|
|
|
|
-- Identificacion
|
|
name VARCHAR(255) NOT NULL,
|
|
code VARCHAR(50) NOT NULL,
|
|
description TEXT,
|
|
color VARCHAR(7) DEFAULT '#3B82F6',
|
|
|
|
-- Categoria
|
|
leave_category hr.leave_type_category NOT NULL DEFAULT 'other',
|
|
|
|
-- Configuracion
|
|
allocation_type hr.allocation_type NOT NULL DEFAULT 'fixed',
|
|
requires_approval BOOLEAN NOT NULL DEFAULT true,
|
|
requires_document BOOLEAN NOT NULL DEFAULT false,
|
|
|
|
-- Limites
|
|
max_days_per_request INTEGER,
|
|
max_days_per_year INTEGER,
|
|
min_days_notice INTEGER DEFAULT 0,
|
|
|
|
-- Pago
|
|
is_paid BOOLEAN NOT NULL DEFAULT true,
|
|
pay_percentage DECIMAL(5,2) DEFAULT 100,
|
|
|
|
-- Estado
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
UNIQUE(tenant_id, company_id, code)
|
|
);
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_leave_types_tenant ON hr.leave_types(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_leave_types_company ON hr.leave_types(company_id);
|
|
CREATE INDEX IF NOT EXISTS idx_leave_types_category ON hr.leave_types(leave_category);
|
|
CREATE INDEX IF NOT EXISTS idx_leave_types_active ON hr.leave_types(is_active) WHERE is_active = true;
|
|
|
|
-- =====================
|
|
-- TABLA: hr.leave_allocations
|
|
-- Asignacion de dias por empleado
|
|
-- =====================
|
|
CREATE TABLE IF NOT EXISTS hr.leave_allocations (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Relaciones
|
|
employee_id UUID NOT NULL REFERENCES hr.employees(id) ON DELETE CASCADE,
|
|
leave_type_id UUID NOT NULL REFERENCES hr.leave_types(id) ON DELETE CASCADE,
|
|
|
|
-- Asignacion
|
|
days_allocated DECIMAL(5,2) NOT NULL,
|
|
days_used DECIMAL(5,2) NOT NULL DEFAULT 0,
|
|
days_remaining DECIMAL(5,2) GENERATED ALWAYS AS (days_allocated - days_used) STORED,
|
|
|
|
-- Periodo
|
|
date_from DATE NOT NULL,
|
|
date_to DATE NOT NULL,
|
|
|
|
-- Metadata
|
|
notes TEXT,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CHECK (date_to > date_from),
|
|
CHECK (days_allocated >= 0),
|
|
CHECK (days_used >= 0)
|
|
);
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_leave_allocations_tenant ON hr.leave_allocations(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_leave_allocations_employee ON hr.leave_allocations(employee_id);
|
|
CREATE INDEX IF NOT EXISTS idx_leave_allocations_type ON hr.leave_allocations(leave_type_id);
|
|
CREATE INDEX IF NOT EXISTS idx_leave_allocations_period ON hr.leave_allocations(date_from, date_to);
|
|
|
|
-- =====================
|
|
-- TABLA: hr.leaves
|
|
-- Solicitudes de ausencia
|
|
-- =====================
|
|
CREATE TABLE IF NOT EXISTS hr.leaves (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
company_id UUID NOT NULL REFERENCES core.companies(id) ON DELETE CASCADE,
|
|
|
|
-- Relaciones
|
|
employee_id UUID NOT NULL REFERENCES hr.employees(id) ON DELETE CASCADE,
|
|
leave_type_id UUID NOT NULL REFERENCES hr.leave_types(id) ON DELETE RESTRICT,
|
|
allocation_id UUID REFERENCES hr.leave_allocations(id) ON DELETE SET NULL,
|
|
|
|
-- Periodo solicitado
|
|
date_from DATE NOT NULL,
|
|
date_to DATE NOT NULL,
|
|
days_requested DECIMAL(5,2) NOT NULL,
|
|
|
|
-- Si es parcial
|
|
is_half_day BOOLEAN NOT NULL DEFAULT false,
|
|
half_day_type VARCHAR(20) CHECK (half_day_type IN ('morning', 'afternoon')),
|
|
|
|
-- Estado
|
|
status hr.leave_status NOT NULL DEFAULT 'draft',
|
|
|
|
-- Aprobacion
|
|
approver_id UUID REFERENCES auth.users(id),
|
|
approved_at TIMESTAMPTZ,
|
|
rejection_reason TEXT,
|
|
|
|
-- Metadata
|
|
request_reason TEXT,
|
|
document_url TEXT,
|
|
notes TEXT,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
submitted_at TIMESTAMPTZ,
|
|
cancelled_at TIMESTAMPTZ,
|
|
cancelled_by UUID REFERENCES auth.users(id),
|
|
|
|
CHECK (date_to >= date_from),
|
|
CHECK (days_requested > 0)
|
|
);
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_leaves_tenant ON hr.leaves(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_leaves_company ON hr.leaves(company_id);
|
|
CREATE INDEX IF NOT EXISTS idx_leaves_employee ON hr.leaves(employee_id);
|
|
CREATE INDEX IF NOT EXISTS idx_leaves_type ON hr.leaves(leave_type_id);
|
|
CREATE INDEX IF NOT EXISTS idx_leaves_status ON hr.leaves(status);
|
|
CREATE INDEX IF NOT EXISTS idx_leaves_dates ON hr.leaves(date_from, date_to);
|
|
CREATE INDEX IF NOT EXISTS idx_leaves_pending ON hr.leaves(status) WHERE status IN ('submitted');
|
|
CREATE INDEX IF NOT EXISTS idx_leaves_approver ON hr.leaves(approver_id) WHERE status = 'submitted';
|
|
|
|
-- =====================
|
|
-- RLS POLICIES
|
|
-- =====================
|
|
|
|
-- Departments
|
|
ALTER TABLE hr.departments ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_departments ON hr.departments;
|
|
CREATE POLICY tenant_isolation_departments ON hr.departments
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- Job Positions
|
|
ALTER TABLE hr.job_positions ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_job_positions ON hr.job_positions;
|
|
CREATE POLICY tenant_isolation_job_positions ON hr.job_positions
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- Employees
|
|
ALTER TABLE hr.employees ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_employees ON hr.employees;
|
|
CREATE POLICY tenant_isolation_employees ON hr.employees
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- Contracts
|
|
ALTER TABLE hr.contracts ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_contracts ON hr.contracts;
|
|
CREATE POLICY tenant_isolation_contracts ON hr.contracts
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- Leave Types
|
|
ALTER TABLE hr.leave_types ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_leave_types ON hr.leave_types;
|
|
CREATE POLICY tenant_isolation_leave_types ON hr.leave_types
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- Leave Allocations
|
|
ALTER TABLE hr.leave_allocations ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_leave_allocations ON hr.leave_allocations;
|
|
CREATE POLICY tenant_isolation_leave_allocations ON hr.leave_allocations
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- Leaves
|
|
ALTER TABLE hr.leaves ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_leaves ON hr.leaves;
|
|
CREATE POLICY tenant_isolation_leaves ON hr.leaves
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- =====================
|
|
-- TRIGGERS
|
|
-- =====================
|
|
|
|
CREATE OR REPLACE FUNCTION hr.update_timestamp()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_departments_updated_at
|
|
BEFORE UPDATE ON hr.departments
|
|
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
|
|
|
CREATE TRIGGER trg_job_positions_updated_at
|
|
BEFORE UPDATE ON hr.job_positions
|
|
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
|
|
|
CREATE TRIGGER trg_employees_updated_at
|
|
BEFORE UPDATE ON hr.employees
|
|
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
|
|
|
CREATE TRIGGER trg_contracts_updated_at
|
|
BEFORE UPDATE ON hr.contracts
|
|
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
|
|
|
CREATE TRIGGER trg_leave_types_updated_at
|
|
BEFORE UPDATE ON hr.leave_types
|
|
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
|
|
|
CREATE TRIGGER trg_leave_allocations_updated_at
|
|
BEFORE UPDATE ON hr.leave_allocations
|
|
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
|
|
|
CREATE TRIGGER trg_leaves_updated_at
|
|
BEFORE UPDATE ON hr.leaves
|
|
FOR EACH ROW EXECUTE FUNCTION hr.update_timestamp();
|
|
|
|
-- =====================
|
|
-- FUNCIONES DE UTILIDAD
|
|
-- =====================
|
|
|
|
-- Obtener subordinados directos de un manager
|
|
CREATE OR REPLACE FUNCTION hr.get_direct_subordinates(p_manager_id UUID)
|
|
RETURNS TABLE (
|
|
id UUID,
|
|
employee_number VARCHAR(50),
|
|
full_name VARCHAR(255),
|
|
department_name VARCHAR(255),
|
|
job_title VARCHAR(255)
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT e.id, e.employee_number, e.full_name, d.name, j.name
|
|
FROM hr.employees e
|
|
LEFT JOIN hr.departments d ON d.id = e.department_id
|
|
LEFT JOIN hr.job_positions j ON j.id = e.job_position_id
|
|
WHERE e.manager_id = p_manager_id
|
|
AND e.status = 'active'
|
|
ORDER BY e.full_name;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
-- Obtener saldo de dias de un tipo de ausencia
|
|
CREATE OR REPLACE FUNCTION hr.get_leave_balance(
|
|
p_employee_id UUID,
|
|
p_leave_type_id UUID,
|
|
p_date DATE DEFAULT CURRENT_DATE
|
|
)
|
|
RETURNS TABLE (
|
|
days_allocated DECIMAL(5,2),
|
|
days_used DECIMAL(5,2),
|
|
days_remaining DECIMAL(5,2)
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT
|
|
COALESCE(SUM(la.days_allocated), 0),
|
|
COALESCE(SUM(la.days_used), 0),
|
|
COALESCE(SUM(la.days_remaining), 0)
|
|
FROM hr.leave_allocations la
|
|
WHERE la.employee_id = p_employee_id
|
|
AND la.leave_type_id = p_leave_type_id
|
|
AND p_date BETWEEN la.date_from AND la.date_to;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
-- Obtener contrato activo de un empleado
|
|
CREATE OR REPLACE FUNCTION hr.get_active_contract(p_employee_id UUID)
|
|
RETURNS hr.contracts AS $$
|
|
DECLARE
|
|
v_contract hr.contracts;
|
|
BEGIN
|
|
SELECT * INTO v_contract
|
|
FROM hr.contracts
|
|
WHERE employee_id = p_employee_id
|
|
AND status = 'active'
|
|
LIMIT 1;
|
|
|
|
RETURN v_contract;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
-- Verificar si hay solapamiento de ausencias
|
|
CREATE OR REPLACE FUNCTION hr.check_leave_overlap(
|
|
p_employee_id UUID,
|
|
p_date_from DATE,
|
|
p_date_to DATE,
|
|
p_exclude_leave_id UUID DEFAULT NULL
|
|
)
|
|
RETURNS BOOLEAN AS $$
|
|
DECLARE
|
|
v_overlap_count INTEGER;
|
|
BEGIN
|
|
SELECT COUNT(*) INTO v_overlap_count
|
|
FROM hr.leaves l
|
|
WHERE l.employee_id = p_employee_id
|
|
AND l.status IN ('submitted', 'approved')
|
|
AND l.date_from <= p_date_to
|
|
AND l.date_to >= p_date_from
|
|
AND (p_exclude_leave_id IS NULL OR l.id != p_exclude_leave_id);
|
|
|
|
RETURN v_overlap_count > 0;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
-- =====================
|
|
-- SEED DATA: Tipos de Ausencia Base
|
|
-- =====================
|
|
|
|
-- Nota: Este seed se ejecuta por tenant cuando se crea la empresa
|
|
-- Aqui solo documentamos los tipos base
|
|
|
|
/*
|
|
INSERT INTO hr.leave_types (tenant_id, company_id, name, code, leave_category, allocation_type, requires_approval, is_paid, max_days_per_year) VALUES
|
|
-- Vacaciones (Mexico: 12 dias primer ano, incrementa)
|
|
(tenant_id, company_id, 'Vacaciones', 'VAC', 'vacation', 'fixed', true, true, 30),
|
|
-- Enfermedad
|
|
(tenant_id, company_id, 'Incapacidad por Enfermedad', 'SICK', 'sick', 'unlimited', false, true, NULL),
|
|
-- Maternidad (Mexico: 84 dias)
|
|
(tenant_id, company_id, 'Licencia de Maternidad', 'MAT', 'maternity', 'fixed', true, true, 84),
|
|
-- Paternidad (Mexico: 5 dias)
|
|
(tenant_id, company_id, 'Licencia de Paternidad', 'PAT', 'paternity', 'fixed', true, true, 5),
|
|
-- Luto
|
|
(tenant_id, company_id, 'Permiso por Luto', 'BER', 'bereavement', 'fixed', true, true, 5),
|
|
-- Personal sin goce
|
|
(tenant_id, company_id, 'Permiso Personal (Sin Goce)', 'UNPAID', 'unpaid', 'unlimited', true, false, NULL),
|
|
-- Personal con goce
|
|
(tenant_id, company_id, 'Permiso Personal (Con Goce)', 'PERS', 'personal', 'fixed', true, true, 3);
|
|
*/
|
|
|
|
-- =====================
|
|
-- COMENTARIOS
|
|
-- =====================
|
|
COMMENT ON SCHEMA hr IS 'Schema para gestion de Recursos Humanos';
|
|
COMMENT ON TABLE hr.departments IS 'Departamentos organizacionales con jerarquia';
|
|
COMMENT ON TABLE hr.job_positions IS 'Puestos de trabajo';
|
|
COMMENT ON TABLE hr.employees IS 'Empleados de la empresa';
|
|
COMMENT ON TABLE hr.contracts IS 'Contratos laborales';
|
|
COMMENT ON TABLE hr.leave_types IS 'Tipos de ausencia configurables';
|
|
COMMENT ON TABLE hr.leave_allocations IS 'Asignacion de dias por empleado y tipo';
|
|
COMMENT ON TABLE hr.leaves IS 'Solicitudes de ausencia';
|
|
COMMENT ON FUNCTION hr.get_direct_subordinates IS 'Obtiene subordinados directos de un manager';
|
|
COMMENT ON FUNCTION hr.get_leave_balance IS 'Obtiene saldo de dias de ausencia';
|
|
COMMENT ON FUNCTION hr.get_active_contract IS 'Obtiene contrato activo de un empleado';
|
|
COMMENT ON FUNCTION hr.check_leave_overlap IS 'Verifica solapamiento de ausencias';
|