465 lines
17 KiB
PL/PgSQL
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 $$;
|