erp-core-database-v2/migrations/20251212_003_financial_reports.sql

465 lines
17 KiB
PL/PgSQL

-- ============================================================================
-- MIGRACIÓN: Sistema de Reportes Financieros
-- Fecha: 2025-12-12
-- Descripción: Crea tablas para definición, ejecución y programación de reportes
-- Impacto: Módulo financiero y verticales que requieren reportes contables
-- Rollback: DROP TABLE y DROP FUNCTION incluidos al final
-- ============================================================================
-- ============================================================================
-- 1. TABLA DE DEFINICIONES DE REPORTES
-- ============================================================================
CREATE TABLE IF NOT EXISTS reports.report_definitions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Identificación
code VARCHAR(50) NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
-- Clasificación
report_type VARCHAR(50) NOT NULL DEFAULT 'financial',
-- financial, accounting, tax, management, custom
category VARCHAR(100),
-- balance_sheet, income_statement, cash_flow, trial_balance, ledger, etc.
-- Configuración de consulta
base_query TEXT, -- SQL base o referencia a función
query_function VARCHAR(255), -- Nombre de función PostgreSQL si usa función
-- Parámetros requeridos (JSON Schema)
parameters_schema JSONB DEFAULT '{}',
-- Ejemplo: {"date_from": {"type": "date", "required": true}, "company_id": {"type": "uuid"}}
-- Configuración de columnas
columns_config JSONB DEFAULT '[]',
-- Ejemplo: [{"name": "account", "label": "Cuenta", "type": "string"}, {"name": "debit", "label": "Debe", "type": "currency"}]
-- Agrupaciones disponibles
grouping_options JSONB DEFAULT '[]',
-- Ejemplo: ["account_type", "company", "period"]
-- Configuración de totales
totals_config JSONB DEFAULT '{}',
-- Ejemplo: {"show_totals": true, "total_columns": ["debit", "credit", "balance"]}
-- Plantillas de exportación
export_formats JSONB DEFAULT '["pdf", "xlsx", "csv"]',
pdf_template VARCHAR(255), -- Referencia a plantilla PDF
xlsx_template VARCHAR(255),
-- Estado y visibilidad
is_system BOOLEAN DEFAULT false, -- Reportes del sistema vs personalizados
is_active BOOLEAN DEFAULT true,
-- Permisos requeridos
required_permissions JSONB DEFAULT '[]',
-- Ejemplo: ["financial.reports.view", "financial.reports.balance_sheet"]
-- Metadata
version INTEGER DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT NOW(),
updated_by UUID REFERENCES auth.users(id),
-- Constraints
UNIQUE(tenant_id, code)
);
COMMENT ON TABLE reports.report_definitions IS
'Definiciones de reportes disponibles en el sistema. Incluye reportes predefinidos y personalizados.';
-- ============================================================================
-- 2. TABLA DE EJECUCIONES DE REPORTES
-- ============================================================================
CREATE TABLE IF NOT EXISTS reports.report_executions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
definition_id UUID NOT NULL REFERENCES reports.report_definitions(id) ON DELETE CASCADE,
-- Parámetros de ejecución
parameters JSONB NOT NULL DEFAULT '{}',
-- Los valores específicos usados para esta ejecución
-- Estado de ejecución
status VARCHAR(20) NOT NULL DEFAULT 'pending',
-- pending, running, completed, failed, cancelled
-- Tiempos
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
execution_time_ms INTEGER,
-- Resultados
row_count INTEGER,
result_data JSONB, -- Datos del reporte (puede ser grande)
result_summary JSONB, -- Resumen/totales
-- Archivos generados
output_files JSONB DEFAULT '[]',
-- Ejemplo: [{"format": "pdf", "path": "/reports/...", "size": 12345}]
-- Errores
error_message TEXT,
error_details JSONB,
-- Metadata
requested_by UUID NOT NULL REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE reports.report_executions IS
'Historial de ejecuciones de reportes con sus resultados y archivos generados.';
-- ============================================================================
-- 3. TABLA DE PROGRAMACIÓN DE REPORTES
-- ============================================================================
CREATE TABLE IF NOT EXISTS reports.report_schedules (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
definition_id UUID NOT NULL REFERENCES reports.report_definitions(id) ON DELETE CASCADE,
company_id UUID REFERENCES auth.companies(id) ON DELETE CASCADE,
-- Nombre del schedule
name VARCHAR(255) NOT NULL,
-- Parámetros predeterminados
default_parameters JSONB DEFAULT '{}',
-- Programación (cron expression)
cron_expression VARCHAR(100) NOT NULL,
-- Ejemplo: "0 8 1 * *" (primer día del mes a las 8am)
timezone VARCHAR(50) DEFAULT 'America/Mexico_City',
-- Estado
is_active BOOLEAN DEFAULT true,
-- Última ejecución
last_execution_id UUID REFERENCES reports.report_executions(id),
last_run_at TIMESTAMPTZ,
next_run_at TIMESTAMPTZ,
-- Destino de entrega
delivery_method VARCHAR(50) DEFAULT 'none',
-- none, email, storage, webhook
delivery_config JSONB DEFAULT '{}',
-- Para email: {"recipients": ["a@b.com"], "subject": "...", "format": "pdf"}
-- Para storage: {"path": "/reports/scheduled/", "retention_days": 30}
-- Metadata
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT NOW(),
updated_by UUID REFERENCES auth.users(id)
);
COMMENT ON TABLE reports.report_schedules IS
'Programación automática de reportes con opciones de entrega.';
-- ============================================================================
-- 4. TABLA DE PLANTILLAS DE REPORTES
-- ============================================================================
CREATE TABLE IF NOT EXISTS reports.report_templates (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Identificación
code VARCHAR(50) NOT NULL,
name VARCHAR(255) NOT NULL,
-- Tipo de plantilla
template_type VARCHAR(20) NOT NULL,
-- pdf, xlsx, html
-- Contenido de la plantilla
template_content BYTEA, -- Para plantillas binarias (XLSX)
template_html TEXT, -- Para plantillas HTML/PDF
-- Estilos CSS (para PDF/HTML)
styles TEXT,
-- Variables disponibles
available_variables JSONB DEFAULT '[]',
-- Estado
is_active BOOLEAN DEFAULT true,
-- Metadata
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, code)
);
COMMENT ON TABLE reports.report_templates IS
'Plantillas personalizables para la generación de reportes en diferentes formatos.';
-- ============================================================================
-- 5. ÍNDICES
-- ============================================================================
CREATE INDEX IF NOT EXISTS idx_report_definitions_tenant_type
ON reports.report_definitions(tenant_id, report_type);
CREATE INDEX IF NOT EXISTS idx_report_definitions_tenant_category
ON reports.report_definitions(tenant_id, category);
CREATE INDEX IF NOT EXISTS idx_report_executions_tenant_status
ON reports.report_executions(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_report_executions_definition
ON reports.report_executions(definition_id);
CREATE INDEX IF NOT EXISTS idx_report_executions_created
ON reports.report_executions(tenant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_report_schedules_next_run
ON reports.report_schedules(next_run_at)
WHERE is_active = true;
-- ============================================================================
-- 6. RLS (Row Level Security)
-- ============================================================================
ALTER TABLE reports.report_definitions ENABLE ROW LEVEL SECURITY;
ALTER TABLE reports.report_executions ENABLE ROW LEVEL SECURITY;
ALTER TABLE reports.report_schedules ENABLE ROW LEVEL SECURITY;
ALTER TABLE reports.report_templates ENABLE ROW LEVEL SECURITY;
-- Políticas para report_definitions
DROP POLICY IF EXISTS report_definitions_tenant_isolation ON reports.report_definitions;
CREATE POLICY report_definitions_tenant_isolation ON reports.report_definitions
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- Políticas para report_executions
DROP POLICY IF EXISTS report_executions_tenant_isolation ON reports.report_executions;
CREATE POLICY report_executions_tenant_isolation ON reports.report_executions
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- Políticas para report_schedules
DROP POLICY IF EXISTS report_schedules_tenant_isolation ON reports.report_schedules;
CREATE POLICY report_schedules_tenant_isolation ON reports.report_schedules
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- Políticas para report_templates
DROP POLICY IF EXISTS report_templates_tenant_isolation ON reports.report_templates;
CREATE POLICY report_templates_tenant_isolation ON reports.report_templates
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- ============================================================================
-- 7. FUNCIONES DE REPORTES PREDEFINIDOS
-- ============================================================================
-- Balance de Comprobación
CREATE OR REPLACE FUNCTION reports.generate_trial_balance(
p_tenant_id UUID,
p_company_id UUID,
p_date_from DATE,
p_date_to DATE,
p_include_zero_balance BOOLEAN DEFAULT false
)
RETURNS TABLE (
account_id UUID,
account_code VARCHAR(20),
account_name VARCHAR(255),
account_type VARCHAR(50),
initial_debit DECIMAL(16,2),
initial_credit DECIMAL(16,2),
period_debit DECIMAL(16,2),
period_credit DECIMAL(16,2),
final_debit DECIMAL(16,2),
final_credit DECIMAL(16,2)
) AS $$
BEGIN
RETURN QUERY
WITH account_balances AS (
-- Saldos iniciales (antes del período)
SELECT
a.id as account_id,
a.code as account_code,
a.name as account_name,
a.account_type,
COALESCE(SUM(CASE WHEN jel.transaction_date < p_date_from THEN jel.debit ELSE 0 END), 0) as initial_debit,
COALESCE(SUM(CASE WHEN jel.transaction_date < p_date_from THEN jel.credit ELSE 0 END), 0) as initial_credit,
COALESCE(SUM(CASE WHEN jel.transaction_date BETWEEN p_date_from AND p_date_to THEN jel.debit ELSE 0 END), 0) as period_debit,
COALESCE(SUM(CASE WHEN jel.transaction_date BETWEEN p_date_from AND p_date_to THEN jel.credit ELSE 0 END), 0) as period_credit
FROM financial.accounts a
LEFT JOIN financial.journal_entry_lines jel ON a.id = jel.account_id
LEFT JOIN financial.journal_entries je ON jel.journal_entry_id = je.id AND je.status = 'posted'
WHERE a.tenant_id = p_tenant_id
AND (p_company_id IS NULL OR a.company_id = p_company_id)
AND a.is_active = true
GROUP BY a.id, a.code, a.name, a.account_type
)
SELECT
ab.account_id,
ab.account_code,
ab.account_name,
ab.account_type,
ab.initial_debit,
ab.initial_credit,
ab.period_debit,
ab.period_credit,
ab.initial_debit + ab.period_debit as final_debit,
ab.initial_credit + ab.period_credit as final_credit
FROM account_balances ab
WHERE p_include_zero_balance = true
OR (ab.initial_debit + ab.period_debit) != 0
OR (ab.initial_credit + ab.period_credit) != 0
ORDER BY ab.account_code;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION reports.generate_trial_balance IS
'Genera el balance de comprobación para un período específico.';
-- Libro Mayor
CREATE OR REPLACE FUNCTION reports.generate_general_ledger(
p_tenant_id UUID,
p_company_id UUID,
p_account_id UUID,
p_date_from DATE,
p_date_to DATE
)
RETURNS TABLE (
entry_date DATE,
journal_entry_id UUID,
entry_number VARCHAR(50),
description TEXT,
partner_name VARCHAR(255),
debit DECIMAL(16,2),
credit DECIMAL(16,2),
running_balance DECIMAL(16,2)
) AS $$
BEGIN
RETURN QUERY
WITH movements AS (
SELECT
je.entry_date,
je.id as journal_entry_id,
je.entry_number,
je.description,
p.name as partner_name,
jel.debit,
jel.credit,
ROW_NUMBER() OVER (ORDER BY je.entry_date, je.id) as rn
FROM financial.journal_entry_lines jel
JOIN financial.journal_entries je ON jel.journal_entry_id = je.id
LEFT JOIN core.partners p ON je.partner_id = p.id
WHERE jel.account_id = p_account_id
AND jel.tenant_id = p_tenant_id
AND je.status = 'posted'
AND je.entry_date BETWEEN p_date_from AND p_date_to
AND (p_company_id IS NULL OR je.company_id = p_company_id)
ORDER BY je.entry_date, je.id
)
SELECT
m.entry_date,
m.journal_entry_id,
m.entry_number,
m.description,
m.partner_name,
m.debit,
m.credit,
SUM(m.debit - m.credit) OVER (ORDER BY m.rn) as running_balance
FROM movements m;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION reports.generate_general_ledger IS
'Genera el libro mayor para una cuenta específica.';
-- ============================================================================
-- 8. DATOS SEMILLA: REPORTES PREDEFINIDOS DEL SISTEMA
-- ============================================================================
-- Nota: Los reportes del sistema se insertan con is_system = true
-- y se insertan solo si no existen (usando ON CONFLICT)
DO $$
DECLARE
v_system_tenant_id UUID;
BEGIN
-- Obtener el tenant del sistema (si existe)
SELECT id INTO v_system_tenant_id
FROM auth.tenants
WHERE code = 'system' OR is_system = true
LIMIT 1;
-- Solo insertar si hay un tenant sistema
IF v_system_tenant_id IS NOT NULL THEN
-- Balance de Comprobación
INSERT INTO reports.report_definitions (
tenant_id, code, name, description, report_type, category,
query_function, parameters_schema, columns_config, is_system
) VALUES (
v_system_tenant_id,
'TRIAL_BALANCE',
'Balance de Comprobación',
'Reporte de balance de comprobación con saldos iniciales, movimientos y saldos finales',
'financial',
'trial_balance',
'reports.generate_trial_balance',
'{"company_id": {"type": "uuid", "required": false}, "date_from": {"type": "date", "required": true}, "date_to": {"type": "date", "required": true}, "include_zero": {"type": "boolean", "default": false}}',
'[{"name": "account_code", "label": "Código", "type": "string"}, {"name": "account_name", "label": "Cuenta", "type": "string"}, {"name": "initial_debit", "label": "Debe Inicial", "type": "currency"}, {"name": "initial_credit", "label": "Haber Inicial", "type": "currency"}, {"name": "period_debit", "label": "Debe Período", "type": "currency"}, {"name": "period_credit", "label": "Haber Período", "type": "currency"}, {"name": "final_debit", "label": "Debe Final", "type": "currency"}, {"name": "final_credit", "label": "Haber Final", "type": "currency"}]',
true
) ON CONFLICT (tenant_id, code) DO NOTHING;
-- Libro Mayor
INSERT INTO reports.report_definitions (
tenant_id, code, name, description, report_type, category,
query_function, parameters_schema, columns_config, is_system
) VALUES (
v_system_tenant_id,
'GENERAL_LEDGER',
'Libro Mayor',
'Detalle de movimientos por cuenta con saldo acumulado',
'financial',
'ledger',
'reports.generate_general_ledger',
'{"company_id": {"type": "uuid", "required": false}, "account_id": {"type": "uuid", "required": true}, "date_from": {"type": "date", "required": true}, "date_to": {"type": "date", "required": true}}',
'[{"name": "entry_date", "label": "Fecha", "type": "date"}, {"name": "entry_number", "label": "Número", "type": "string"}, {"name": "description", "label": "Descripción", "type": "string"}, {"name": "partner_name", "label": "Tercero", "type": "string"}, {"name": "debit", "label": "Debe", "type": "currency"}, {"name": "credit", "label": "Haber", "type": "currency"}, {"name": "running_balance", "label": "Saldo", "type": "currency"}]',
true
) ON CONFLICT (tenant_id, code) DO NOTHING;
RAISE NOTICE 'Reportes del sistema insertados correctamente';
END IF;
END $$;
-- ============================================================================
-- ROLLBACK SCRIPT
-- ============================================================================
/*
DROP FUNCTION IF EXISTS reports.generate_general_ledger(UUID, UUID, UUID, DATE, DATE);
DROP FUNCTION IF EXISTS reports.generate_trial_balance(UUID, UUID, DATE, DATE, BOOLEAN);
DROP TABLE IF EXISTS reports.report_templates;
DROP TABLE IF EXISTS reports.report_schedules;
DROP TABLE IF EXISTS reports.report_executions;
DROP TABLE IF EXISTS reports.report_definitions;
*/
-- ============================================================================
-- VERIFICACIÓN
-- ============================================================================
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'reports' AND tablename = 'report_definitions') THEN
RAISE EXCEPTION 'Error: Tabla report_definitions no fue creada';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'reports' AND tablename = 'report_executions') THEN
RAISE EXCEPTION 'Error: Tabla report_executions no fue creada';
END IF;
RAISE NOTICE 'Migración completada exitosamente: Reportes Financieros';
END $$;