-- ============================================================================ -- 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 $$;