From 8a9db6f9d161ea7ac6cf0155d90124473b530c33 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Wed, 4 Feb 2026 00:46:00 -0600 Subject: [PATCH] [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 --- schemas/09-assets-schema-ddl.sql | 284 ++++++++++++++++++++++++++++++ schemas/12-analytics-kpis-ddl.sql | 274 ++++++++++++++++++++++++++++ 2 files changed, 558 insertions(+) create mode 100644 schemas/12-analytics-kpis-ddl.sql diff --git a/schemas/09-assets-schema-ddl.sql b/schemas/09-assets-schema-ddl.sql index bd36dce..a4235e8 100644 --- a/schemas/09-assets-schema-ddl.sql +++ b/schemas/09-assets-schema-ddl.sql @@ -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 diff --git a/schemas/12-analytics-kpis-ddl.sql b/schemas/12-analytics-kpis-ddl.sql new file mode 100644 index 0000000..b743a80 --- /dev/null +++ b/schemas/12-analytics-kpis-ddl.sql @@ -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 +-- ============================================================================