[GAP-001,002,003] feat(ddl): Add KPIs, tool loans and depreciation tables

- Add 12-analytics-kpis-ddl.sql with kpis_config and kpis_values tables
- Add tool_loans table with loan_status enum (GAP-002)
- Add depreciation_schedule and depreciation_entries tables (GAP-003)
- Add depreciation_method enum and calculate_monthly_depreciation function

Implements 13 SP of critical gaps identified in EPIC-003.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-02-04 00:46:00 -06:00
parent 094cbe3ffb
commit 8a9db6f9d1
2 changed files with 558 additions and 0 deletions

View File

@ -941,6 +941,287 @@ CREATE TRIGGER trg_update_next_maintenance
AFTER INSERT OR UPDATE OR DELETE ON assets.maintenance_schedules
FOR EACH ROW EXECUTE FUNCTION assets.update_next_maintenance();
-- ============================================================================
-- GAP-002: PRESTAMOS DE HERRAMIENTAS (2026-02-04)
-- ============================================================================
-- Enum para estado de prestamo
CREATE TYPE assets.loan_status AS ENUM (
'active', -- Prestamo activo
'returned', -- Devuelto
'overdue', -- Vencido
'lost', -- Extraviado
'damaged' -- Danado
);
-- ----------------------------------------------------------------------------
-- 12. Prestamos de Herramientas
-- Registro de prestamos de herramientas entre obras o a empleados
-- ----------------------------------------------------------------------------
CREATE TABLE assets.tool_loans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Herramienta prestada
tool_id UUID NOT NULL REFERENCES assets.assets(id),
-- Quien recibe el prestamo
employee_id UUID NOT NULL,
employee_name VARCHAR(255),
-- Origen del prestamo
fraccionamiento_origen_id UUID,
fraccionamiento_origen_name VARCHAR(255),
-- Destino del prestamo
fraccionamiento_destino_id UUID,
fraccionamiento_destino_name VARCHAR(255),
-- Fechas
loan_date DATE NOT NULL,
expected_return_date DATE,
actual_return_date DATE,
-- Estado
status assets.loan_status NOT NULL DEFAULT 'active',
-- Condicion
condition_out TEXT,
condition_out_photos JSONB,
condition_in TEXT,
condition_in_photos JSONB,
-- Aprobacion
approved_by_id UUID,
approved_by_name VARCHAR(255),
approved_at TIMESTAMPTZ,
-- Devolucion
received_by_id UUID,
received_by_name VARCHAR(255),
-- Notas
notes TEXT,
metadata JSONB,
-- Auditoria
created_by UUID,
updated_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indices para tool_loans
CREATE INDEX idx_tool_loans_tenant ON assets.tool_loans(tenant_id);
CREATE INDEX idx_tool_loans_tool ON assets.tool_loans(tenant_id, tool_id);
CREATE INDEX idx_tool_loans_employee ON assets.tool_loans(tenant_id, employee_id);
CREATE INDEX idx_tool_loans_status ON assets.tool_loans(tenant_id, status);
CREATE INDEX idx_tool_loans_origen ON assets.tool_loans(tenant_id, fraccionamiento_origen_id);
CREATE INDEX idx_tool_loans_destino ON assets.tool_loans(tenant_id, fraccionamiento_destino_id);
CREATE INDEX idx_tool_loans_active ON assets.tool_loans(tenant_id, status) WHERE status = 'active';
CREATE INDEX idx_tool_loans_overdue ON assets.tool_loans(tenant_id, expected_return_date)
WHERE status = 'active' AND expected_return_date < CURRENT_DATE;
ALTER TABLE assets.tool_loans ENABLE ROW LEVEL SECURITY;
CREATE TRIGGER trg_tool_loans_updated_at
BEFORE UPDATE ON assets.tool_loans
FOR EACH ROW EXECUTE FUNCTION assets.set_updated_at();
-- ============================================================================
-- GAP-003: DEPRECIACION DE ACTIVOS (2026-02-04)
-- ============================================================================
-- Enum para metodo de depreciacion
CREATE TYPE assets.depreciation_method AS ENUM (
'straight_line', -- Linea recta
'declining_balance', -- Saldo decreciente
'double_declining', -- Doble saldo decreciente
'sum_of_years', -- Suma de digitos de los anos
'units_of_production' -- Unidades de produccion
);
-- ----------------------------------------------------------------------------
-- 13. Programacion de Depreciacion
-- Configuracion de depreciacion para cada activo
-- ----------------------------------------------------------------------------
CREATE TABLE assets.depreciation_schedule (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Activo
asset_id UUID NOT NULL REFERENCES assets.assets(id),
-- Tipo de activo
asset_type VARCHAR(20) NOT NULL, -- equipment, machinery, vehicle, tool
-- Metodo de depreciacion
method assets.depreciation_method NOT NULL DEFAULT 'straight_line',
-- Valores
original_value DECIMAL(18,2) NOT NULL,
salvage_value DECIMAL(18,2) DEFAULT 0,
depreciable_amount DECIMAL(18,2) GENERATED ALWAYS AS
(original_value - COALESCE(salvage_value, 0)) STORED,
-- Vida util
useful_life_months INTEGER NOT NULL,
useful_life_units INTEGER, -- Para units_of_production
-- Depreciacion mensual calculada (para straight_line)
monthly_depreciation DECIMAL(12,2) GENERATED ALWAYS AS
(CASE WHEN useful_life_months > 0
THEN (original_value - COALESCE(salvage_value, 0)) / useful_life_months
ELSE 0 END) STORED,
-- Fechas
depreciation_start_date DATE NOT NULL,
depreciation_end_date DATE,
-- Estado actual
accumulated_depreciation DECIMAL(18,2) DEFAULT 0,
current_book_value DECIMAL(18,2),
last_entry_date DATE,
-- Estado
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_fully_depreciated BOOLEAN NOT NULL DEFAULT FALSE,
-- Notas
notes TEXT,
metadata JSONB,
-- Auditoria
created_by UUID,
updated_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_depreciation_schedule_asset UNIQUE (tenant_id, asset_id)
);
-- ----------------------------------------------------------------------------
-- 14. Entradas de Depreciacion
-- Registro mensual de depreciacion aplicada
-- ----------------------------------------------------------------------------
CREATE TABLE assets.depreciation_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Programacion
schedule_id UUID NOT NULL REFERENCES assets.depreciation_schedule(id) ON DELETE CASCADE,
-- Periodo
period_date DATE NOT NULL, -- Primer dia del mes
fiscal_year INTEGER NOT NULL,
fiscal_month INTEGER NOT NULL,
-- Valores
depreciation_amount DECIMAL(12,2) NOT NULL,
accumulated_depreciation DECIMAL(18,2) NOT NULL,
book_value DECIMAL(18,2) NOT NULL,
-- Para units_of_production
units_used INTEGER,
-- Contabilidad
journal_entry_id UUID,
is_posted BOOLEAN DEFAULT FALSE,
posted_at TIMESTAMPTZ,
posted_by UUID,
-- Estado
status VARCHAR(20) NOT NULL DEFAULT 'draft', -- draft, posted, reversed
-- Notas
notes TEXT,
-- Auditoria
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_depreciation_entries_period UNIQUE (schedule_id, period_date)
);
-- Indices para depreciation_schedule
CREATE INDEX idx_depreciation_schedule_tenant ON assets.depreciation_schedule(tenant_id);
CREATE INDEX idx_depreciation_schedule_asset ON assets.depreciation_schedule(tenant_id, asset_id);
CREATE INDEX idx_depreciation_schedule_active ON assets.depreciation_schedule(tenant_id, is_active) WHERE is_active = TRUE;
CREATE INDEX idx_depreciation_schedule_not_depreciated ON assets.depreciation_schedule(tenant_id, is_fully_depreciated)
WHERE is_fully_depreciated = FALSE;
-- Indices para depreciation_entries
CREATE INDEX idx_depreciation_entries_tenant ON assets.depreciation_entries(tenant_id);
CREATE INDEX idx_depreciation_entries_schedule ON assets.depreciation_entries(schedule_id);
CREATE INDEX idx_depreciation_entries_period ON assets.depreciation_entries(tenant_id, period_date);
CREATE INDEX idx_depreciation_entries_fiscal ON assets.depreciation_entries(tenant_id, fiscal_year, fiscal_month);
CREATE INDEX idx_depreciation_entries_pending ON assets.depreciation_entries(tenant_id, status) WHERE status = 'draft';
ALTER TABLE assets.depreciation_schedule ENABLE ROW LEVEL SECURITY;
ALTER TABLE assets.depreciation_entries ENABLE ROW LEVEL SECURITY;
CREATE TRIGGER trg_depreciation_schedule_updated_at
BEFORE UPDATE ON assets.depreciation_schedule
FOR EACH ROW EXECUTE FUNCTION assets.set_updated_at();
CREATE TRIGGER trg_depreciation_entries_updated_at
BEFORE UPDATE ON assets.depreciation_entries
FOR EACH ROW EXECUTE FUNCTION assets.set_updated_at();
-- ============================================================================
-- FUNCION: Calcular depreciacion mensual
-- ============================================================================
CREATE OR REPLACE FUNCTION assets.calculate_monthly_depreciation(
p_schedule_id UUID,
p_period_date DATE
)
RETURNS DECIMAL(12,2) AS $$
DECLARE
v_schedule RECORD;
v_months_elapsed INTEGER;
v_depreciation DECIMAL(12,2);
BEGIN
SELECT * INTO v_schedule
FROM assets.depreciation_schedule
WHERE id = p_schedule_id AND is_active = TRUE;
IF NOT FOUND THEN
RETURN 0;
END IF;
-- Calcular meses transcurridos
v_months_elapsed := (
EXTRACT(YEAR FROM p_period_date) * 12 + EXTRACT(MONTH FROM p_period_date)
) - (
EXTRACT(YEAR FROM v_schedule.depreciation_start_date) * 12 + EXTRACT(MONTH FROM v_schedule.depreciation_start_date)
);
-- Si ya esta completamente depreciado
IF v_schedule.accumulated_depreciation >= v_schedule.depreciable_amount THEN
RETURN 0;
END IF;
-- Calcular segun metodo
CASE v_schedule.method
WHEN 'straight_line' THEN
v_depreciation := v_schedule.monthly_depreciation;
WHEN 'declining_balance' THEN
v_depreciation := (v_schedule.original_value - v_schedule.accumulated_depreciation) *
(1.0 / v_schedule.useful_life_months) * 2;
ELSE
v_depreciation := v_schedule.monthly_depreciation;
END CASE;
-- No depreciar mas del valor depreciable restante
IF (v_schedule.accumulated_depreciation + v_depreciation) > v_schedule.depreciable_amount THEN
v_depreciation := v_schedule.depreciable_amount - v_schedule.accumulated_depreciation;
END IF;
RETURN COALESCE(v_depreciation, 0);
END;
$$ LANGUAGE plpgsql;
-- ============================================================================
-- COMENTARIOS DE DOCUMENTACION
-- ============================================================================
@ -958,6 +1239,9 @@ COMMENT ON TABLE assets.maintenance_history IS 'Historial de mantenimientos real
COMMENT ON TABLE assets.asset_costs IS 'Costos de operacion y mantenimiento de activos';
COMMENT ON TABLE assets.asset_locations IS 'Historial de ubicaciones GPS de activos';
COMMENT ON TABLE assets.fuel_logs IS 'Registro de cargas de combustible';
COMMENT ON TABLE assets.tool_loans IS 'Prestamos de herramientas entre obras/empleados (GAP-002)';
COMMENT ON TABLE assets.depreciation_schedule IS 'Configuracion de depreciacion por activo (GAP-003)';
COMMENT ON TABLE assets.depreciation_entries IS 'Entradas mensuales de depreciacion (GAP-003)';
-- ============================================================================
-- FIN DEL SCRIPT

View File

@ -0,0 +1,274 @@
-- ============================================================================
-- 12-analytics-kpis-ddl.sql
-- Schema: reports (extension)
-- ERP Construccion - KPIs Configurables (GAP-001)
-- ============================================================================
-- Descripcion: Configuracion dinamica de KPIs incluyendo:
-- - Definicion de KPIs con formulas configurables
-- - Valores calculados historicos
-- - Umbrales y semaforizacion
-- ============================================================================
-- Autor: Claude-Especialista-BD
-- Fecha: 2026-02-04
-- Version: 1.0.0
-- Tarea: TASK-2026-02-03-ANALISIS-MODELADO-INTEGRAL / GAP-001
-- ============================================================================
-- Usar schema reports existente
-- CREATE SCHEMA IF NOT EXISTS reports;
-- ============================================================================
-- TABLAS
-- ============================================================================
-- ----------------------------------------------------------------------------
-- 1. Configuracion de KPIs
-- Permite definir KPIs dinamicos con formulas configurables
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS reports.kpis_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Identificacion
code VARCHAR(50) NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
-- Clasificacion
category VARCHAR(50) NOT NULL, -- financial, progress, quality, hse, hr, inventory, operational
module VARCHAR(50) NOT NULL, -- MAI-006, MAE-014, etc.
-- Formula de calculo
formula TEXT NOT NULL, -- SQL o expresion matematica
formula_type VARCHAR(20) NOT NULL DEFAULT 'sql', -- sql, expression, function
query_function VARCHAR(255), -- Nombre de funcion PL/pgSQL si aplica
-- Parametros de la formula
parameters_schema JSONB DEFAULT '{}',
-- Unidad y formato
unit VARCHAR(20), -- %, $, hrs, dias, etc.
decimal_places INTEGER DEFAULT 2,
format_pattern VARCHAR(50), -- Patron de formato para display
-- Umbrales de semaforizacion
target_value DECIMAL(18,4),
threshold_green DECIMAL(18,4), -- Valor >= este es verde
threshold_yellow DECIMAL(18,4), -- Valor >= este es amarillo, < es rojo
invert_colors BOOLEAN DEFAULT FALSE, -- TRUE si menor es mejor
-- Frecuencia de calculo
calculation_frequency VARCHAR(20) DEFAULT 'daily', -- realtime, hourly, daily, weekly, monthly
-- Visualizacion
display_order INTEGER DEFAULT 0,
icon VARCHAR(50),
color VARCHAR(20),
-- Estado
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_system BOOLEAN NOT NULL DEFAULT FALSE, -- TRUE = no editable por usuario
-- Metadatos
metadata JSONB,
-- Auditoria
created_by UUID,
updated_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uq_kpis_config_tenant_code UNIQUE (tenant_id, code)
);
-- ----------------------------------------------------------------------------
-- 2. Valores Calculados de KPIs
-- Almacena los valores calculados periodicamente
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS reports.kpis_values (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- KPI
kpi_id UUID NOT NULL REFERENCES reports.kpis_config(id) ON DELETE CASCADE,
-- Periodo
period_start DATE NOT NULL,
period_end DATE NOT NULL,
period_type VARCHAR(20) NOT NULL DEFAULT 'daily', -- daily, weekly, monthly, quarterly, yearly
-- Contexto opcional
project_id UUID, -- Fraccionamiento/Obra especifica
department_id UUID, -- Departamento especifico
-- Valor calculado
value DECIMAL(18,4) NOT NULL,
previous_value DECIMAL(18,4),
-- Comparacion con objetivo
target_value DECIMAL(18,4),
variance_value DECIMAL(18,4), -- value - target_value
variance_percentage DECIMAL(8,2), -- ((value - target) / target) * 100
-- Semaforizacion calculada
status VARCHAR(10), -- green, yellow, red
is_on_target BOOLEAN,
-- Tendencia
trend_direction VARCHAR(10), -- up, down, stable
change_percentage DECIMAL(8,2),
-- Desglose
breakdown JSONB, -- Datos adicionales de calculo
-- Calculo
calculated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
calculation_duration_ms INTEGER,
calculation_error TEXT,
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================================================
-- INDICES
-- ============================================================================
-- KPIs Config
CREATE INDEX IF NOT EXISTS idx_kpis_config_tenant ON reports.kpis_config(tenant_id);
CREATE INDEX IF NOT EXISTS idx_kpis_config_tenant_category ON reports.kpis_config(tenant_id, category);
CREATE INDEX IF NOT EXISTS idx_kpis_config_tenant_module ON reports.kpis_config(tenant_id, module);
CREATE INDEX IF NOT EXISTS idx_kpis_config_active ON reports.kpis_config(tenant_id, is_active) WHERE is_active = TRUE;
-- KPIs Values
CREATE INDEX IF NOT EXISTS idx_kpis_values_tenant ON reports.kpis_values(tenant_id);
CREATE INDEX IF NOT EXISTS idx_kpis_values_kpi ON reports.kpis_values(kpi_id);
CREATE INDEX IF NOT EXISTS idx_kpis_values_period ON reports.kpis_values(tenant_id, period_start, period_end);
CREATE INDEX IF NOT EXISTS idx_kpis_values_kpi_period ON reports.kpis_values(kpi_id, period_start DESC);
CREATE INDEX IF NOT EXISTS idx_kpis_values_project ON reports.kpis_values(tenant_id, project_id) WHERE project_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_kpis_values_calculated ON reports.kpis_values(calculated_at DESC);
-- ============================================================================
-- ROW LEVEL SECURITY (RLS)
-- ============================================================================
ALTER TABLE reports.kpis_config ENABLE ROW LEVEL SECURITY;
ALTER TABLE reports.kpis_values ENABLE ROW LEVEL SECURITY;
-- ============================================================================
-- TRIGGERS DE AUDITORIA
-- ============================================================================
CREATE OR REPLACE FUNCTION reports.set_kpis_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_kpis_config_updated_at ON reports.kpis_config;
CREATE TRIGGER trg_kpis_config_updated_at
BEFORE UPDATE ON reports.kpis_config
FOR EACH ROW EXECUTE FUNCTION reports.set_kpis_updated_at();
-- ============================================================================
-- FUNCIONES AUXILIARES
-- ============================================================================
-- Funcion para calcular un KPI especifico
CREATE OR REPLACE FUNCTION reports.calculate_kpi(
p_kpi_id UUID,
p_tenant_id UUID,
p_period_start DATE,
p_period_end DATE,
p_project_id UUID DEFAULT NULL
)
RETURNS DECIMAL(18,4) AS $$
DECLARE
v_kpi RECORD;
v_result DECIMAL(18,4);
BEGIN
-- Obtener configuracion del KPI
SELECT * INTO v_kpi
FROM reports.kpis_config
WHERE id = p_kpi_id AND tenant_id = p_tenant_id AND is_active = TRUE;
IF NOT FOUND THEN
RAISE EXCEPTION 'KPI not found or inactive: %', p_kpi_id;
END IF;
-- Si es una funcion, ejecutarla
IF v_kpi.formula_type = 'function' AND v_kpi.query_function IS NOT NULL THEN
EXECUTE format('SELECT %s($1, $2, $3, $4)', v_kpi.query_function)
INTO v_result
USING p_tenant_id, p_period_start, p_period_end, p_project_id;
ELSE
-- Ejecutar formula SQL directamente (con cuidado de seguridad)
-- En produccion esto debe ser mas restrictivo
EXECUTE v_kpi.formula
INTO v_result
USING p_tenant_id, p_period_start, p_period_end, p_project_id;
END IF;
RETURN v_result;
EXCEPTION
WHEN OTHERS THEN
RAISE WARNING 'Error calculating KPI %: %', p_kpi_id, SQLERRM;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Funcion para determinar color de semaforo
CREATE OR REPLACE FUNCTION reports.get_kpi_status(
p_value DECIMAL(18,4),
p_threshold_green DECIMAL(18,4),
p_threshold_yellow DECIMAL(18,4),
p_invert_colors BOOLEAN DEFAULT FALSE
)
RETURNS VARCHAR(10) AS $$
BEGIN
IF p_threshold_green IS NULL OR p_threshold_yellow IS NULL THEN
RETURN NULL;
END IF;
IF p_invert_colors THEN
-- Menor es mejor
IF p_value <= p_threshold_green THEN
RETURN 'green';
ELSIF p_value <= p_threshold_yellow THEN
RETURN 'yellow';
ELSE
RETURN 'red';
END IF;
ELSE
-- Mayor es mejor
IF p_value >= p_threshold_green THEN
RETURN 'green';
ELSIF p_value >= p_threshold_yellow THEN
RETURN 'yellow';
ELSE
RETURN 'red';
END IF;
END IF;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- ============================================================================
-- COMENTARIOS DE DOCUMENTACION
-- ============================================================================
COMMENT ON TABLE reports.kpis_config IS 'Configuracion de KPIs dinamicos con formulas (GAP-001)';
COMMENT ON COLUMN reports.kpis_config.formula IS 'Formula SQL o expresion para calcular el KPI';
COMMENT ON COLUMN reports.kpis_config.threshold_green IS 'Umbral para status verde';
COMMENT ON COLUMN reports.kpis_config.threshold_yellow IS 'Umbral para status amarillo';
COMMENT ON COLUMN reports.kpis_config.invert_colors IS 'TRUE si valores menores son mejores';
COMMENT ON TABLE reports.kpis_values IS 'Valores calculados historicos de KPIs (GAP-001)';
COMMENT ON COLUMN reports.kpis_values.variance_value IS 'Diferencia absoluta: value - target';
COMMENT ON COLUMN reports.kpis_values.variance_percentage IS 'Diferencia porcentual respecto al target';
-- ============================================================================
-- FIN DEL SCRIPT
-- ============================================================================