-- ===================================================== -- SCHEMA: system -- PROPÓSITO: Mensajería, notificaciones, logs, reportes -- MÓDULOS: MGN-012 (Reportes), MGN-014 (Mensajería) -- FECHA: 2025-11-24 -- ===================================================== -- Crear schema CREATE SCHEMA IF NOT EXISTS system; -- ===================================================== -- TYPES (ENUMs) -- ===================================================== CREATE TYPE system.message_type AS ENUM ( 'comment', 'note', 'email', 'notification', 'system' ); CREATE TYPE system.notification_status AS ENUM ( 'pending', 'sent', 'read', 'failed' ); CREATE TYPE system.activity_type AS ENUM ( 'call', 'meeting', 'email', 'todo', 'follow_up', 'custom' ); CREATE TYPE system.activity_status AS ENUM ( 'planned', 'done', 'cancelled', 'overdue' ); CREATE TYPE system.email_status AS ENUM ( 'draft', 'queued', 'sending', 'sent', 'failed', 'bounced' ); CREATE TYPE system.log_level AS ENUM ( 'debug', 'info', 'warning', 'error', 'critical' ); CREATE TYPE system.report_format AS ENUM ( 'pdf', 'excel', 'csv', 'html' ); -- ===================================================== -- TABLES -- ===================================================== -- Tabla: messages (Chatter - mensajes en registros) CREATE TABLE system.messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Referencia polimórfica (a qué registro pertenece) model VARCHAR(100) NOT NULL, -- 'SaleOrder', 'Task', 'Invoice', etc. record_id UUID NOT NULL, -- Tipo y contenido message_type system.message_type NOT NULL DEFAULT 'comment', subject VARCHAR(255), body TEXT NOT NULL, -- Autor author_id UUID REFERENCES auth.users(id), author_name VARCHAR(255), author_email VARCHAR(255), -- Email tracking email_from VARCHAR(255), reply_to VARCHAR(255), message_id VARCHAR(500), -- Message-ID para threading -- Relación (respuesta a mensaje) parent_id UUID REFERENCES system.messages(id), -- Attachments attachment_ids UUID[] DEFAULT '{}', -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP ); -- Tabla: message_followers (Seguidores de registros) CREATE TABLE system.message_followers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Referencia polimórfica model VARCHAR(100) NOT NULL, record_id UUID NOT NULL, -- Seguidor partner_id UUID REFERENCES core.partners(id), user_id UUID REFERENCES auth.users(id), -- Configuración email_notifications BOOLEAN DEFAULT TRUE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_message_followers UNIQUE (model, record_id, COALESCE(user_id, partner_id)), CONSTRAINT chk_message_followers_user_or_partner CHECK ( (user_id IS NOT NULL AND partner_id IS NULL) OR (partner_id IS NOT NULL AND user_id IS NULL) ) ); -- Tabla: notifications (Notificaciones a usuarios) CREATE TABLE system.notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, -- Contenido title VARCHAR(255) NOT NULL, message TEXT NOT NULL, url VARCHAR(500), -- URL para acción (ej: /sales/orders/123) -- Referencia (opcional) model VARCHAR(100), record_id UUID, -- Estado status system.notification_status NOT NULL DEFAULT 'pending', read_at TIMESTAMP, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, sent_at TIMESTAMP ); -- Tabla: activities (Actividades programadas) CREATE TABLE system.activities ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Referencia polimórfica model VARCHAR(100) NOT NULL, record_id UUID NOT NULL, -- Actividad activity_type system.activity_type NOT NULL, summary VARCHAR(255) NOT NULL, description TEXT, -- Asignación assigned_to UUID REFERENCES auth.users(id), assigned_by UUID REFERENCES auth.users(id), -- Fechas due_date DATE NOT NULL, due_time TIME, -- Estado status system.activity_status NOT NULL DEFAULT 'planned', -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), completed_at TIMESTAMP, completed_by UUID REFERENCES auth.users(id) ); -- Tabla: message_templates (Plantillas de mensajes/emails) CREATE TABLE system.message_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, model VARCHAR(100), -- Para qué modelo se usa -- Contenido subject VARCHAR(255), body_html TEXT, body_text TEXT, -- Configuración email email_from VARCHAR(255), reply_to VARCHAR(255), cc VARCHAR(255), bcc VARCHAR(255), -- Control active BOOLEAN DEFAULT TRUE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), CONSTRAINT uq_message_templates_name_tenant UNIQUE (tenant_id, name) ); -- Tabla: email_queue (Cola de envío de emails) CREATE TABLE system.email_queue ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES auth.tenants(id), -- Destinatarios email_to VARCHAR(255) NOT NULL, email_cc VARCHAR(500), email_bcc VARCHAR(500), -- Contenido subject VARCHAR(255) NOT NULL, body_html TEXT, body_text TEXT, -- Remitente email_from VARCHAR(255) NOT NULL, reply_to VARCHAR(255), -- Attachments attachment_ids UUID[] DEFAULT '{}', -- Estado status system.email_status NOT NULL DEFAULT 'queued', attempts INTEGER DEFAULT 0, max_attempts INTEGER DEFAULT 3, error_message TEXT, -- Tracking message_id VARCHAR(500), opened_at TIMESTAMP, clicked_at TIMESTAMP, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, scheduled_at TIMESTAMP, sent_at TIMESTAMP, failed_at TIMESTAMP ); -- Tabla: logs (Logs del sistema) CREATE TABLE system.logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES auth.tenants(id), -- Nivel y fuente level system.log_level NOT NULL, logger VARCHAR(100), -- Módulo que genera el log -- Mensaje message TEXT NOT NULL, stack_trace TEXT, -- Contexto user_id UUID REFERENCES auth.users(id), ip_address INET, user_agent TEXT, request_id UUID, -- Referencia (opcional) model VARCHAR(100), record_id UUID, -- Metadata adicional metadata JSONB DEFAULT '{}', -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Tabla: reports (Definiciones de reportes) CREATE TABLE system.reports ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, code VARCHAR(50) NOT NULL, description TEXT, -- Tipo model VARCHAR(100), -- Para qué modelo es el reporte report_type VARCHAR(50), -- 'standard', 'custom', 'dashboard' -- Query/Template query_template TEXT, -- SQL template o JSON query template_file VARCHAR(255), -- Path al archivo de plantilla -- Configuración default_format system.report_format DEFAULT 'pdf', is_public BOOLEAN DEFAULT FALSE, -- Control active BOOLEAN DEFAULT TRUE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), CONSTRAINT uq_reports_code_tenant UNIQUE (tenant_id, code) ); -- Tabla: report_executions (Ejecuciones de reportes) CREATE TABLE system.report_executions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, report_id UUID NOT NULL REFERENCES system.reports(id) ON DELETE CASCADE, -- Parámetros de ejecución parameters JSONB DEFAULT '{}', format system.report_format NOT NULL, -- Resultado file_url VARCHAR(500), file_size BIGINT, error_message TEXT, -- Estado status VARCHAR(20) DEFAULT 'pending', -- pending, running, completed, failed -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), started_at TIMESTAMP, completed_at TIMESTAMP ); -- Tabla: dashboards (Dashboards configurables) CREATE TABLE system.dashboards ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, description TEXT, -- Configuración layout JSONB DEFAULT '{}', -- Grid layout configuration is_default BOOLEAN DEFAULT FALSE, -- Visibilidad user_id UUID REFERENCES auth.users(id), -- NULL = compartido is_public BOOLEAN DEFAULT FALSE, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMP, updated_by UUID REFERENCES auth.users(id), CONSTRAINT uq_dashboards_name_user UNIQUE (tenant_id, name, COALESCE(user_id, '00000000-0000-0000-0000-000000000000'::UUID)) ); -- Tabla: dashboard_widgets (Widgets en dashboards) CREATE TABLE system.dashboard_widgets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), dashboard_id UUID NOT NULL REFERENCES system.dashboards(id) ON DELETE CASCADE, -- Tipo de widget widget_type VARCHAR(50) NOT NULL, -- 'chart', 'kpi', 'table', 'calendar', etc. title VARCHAR(255), -- Configuración config JSONB NOT NULL DEFAULT '{}', -- Widget-specific configuration position JSONB DEFAULT '{}', -- {x, y, w, h} para grid -- Data source data_source VARCHAR(100), -- Model o query query_params JSONB DEFAULT '{}', -- Refresh refresh_interval INTEGER, -- Segundos -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP ); -- ===================================================== -- INDICES -- ===================================================== -- Messages CREATE INDEX idx_messages_tenant_id ON system.messages(tenant_id); CREATE INDEX idx_messages_model_record ON system.messages(model, record_id); CREATE INDEX idx_messages_author_id ON system.messages(author_id); CREATE INDEX idx_messages_parent_id ON system.messages(parent_id); CREATE INDEX idx_messages_created_at ON system.messages(created_at DESC); -- Message Followers CREATE INDEX idx_message_followers_model_record ON system.message_followers(model, record_id); CREATE INDEX idx_message_followers_user_id ON system.message_followers(user_id); CREATE INDEX idx_message_followers_partner_id ON system.message_followers(partner_id); -- Notifications CREATE INDEX idx_notifications_tenant_id ON system.notifications(tenant_id); CREATE INDEX idx_notifications_user_id ON system.notifications(user_id); CREATE INDEX idx_notifications_status ON system.notifications(status); CREATE INDEX idx_notifications_model_record ON system.notifications(model, record_id); CREATE INDEX idx_notifications_created_at ON system.notifications(created_at DESC); -- Activities CREATE INDEX idx_activities_tenant_id ON system.activities(tenant_id); CREATE INDEX idx_activities_model_record ON system.activities(model, record_id); CREATE INDEX idx_activities_assigned_to ON system.activities(assigned_to); CREATE INDEX idx_activities_due_date ON system.activities(due_date); CREATE INDEX idx_activities_status ON system.activities(status); -- Message Templates CREATE INDEX idx_message_templates_tenant_id ON system.message_templates(tenant_id); CREATE INDEX idx_message_templates_model ON system.message_templates(model); CREATE INDEX idx_message_templates_active ON system.message_templates(active) WHERE active = TRUE; -- Email Queue CREATE INDEX idx_email_queue_status ON system.email_queue(status); CREATE INDEX idx_email_queue_scheduled_at ON system.email_queue(scheduled_at); CREATE INDEX idx_email_queue_created_at ON system.email_queue(created_at); -- Logs CREATE INDEX idx_logs_tenant_id ON system.logs(tenant_id); CREATE INDEX idx_logs_level ON system.logs(level); CREATE INDEX idx_logs_logger ON system.logs(logger); CREATE INDEX idx_logs_user_id ON system.logs(user_id); CREATE INDEX idx_logs_created_at ON system.logs(created_at DESC); CREATE INDEX idx_logs_model_record ON system.logs(model, record_id); -- Reports CREATE INDEX idx_reports_tenant_id ON system.reports(tenant_id); CREATE INDEX idx_reports_code ON system.reports(code); CREATE INDEX idx_reports_active ON system.reports(active) WHERE active = TRUE; -- Report Executions CREATE INDEX idx_report_executions_tenant_id ON system.report_executions(tenant_id); CREATE INDEX idx_report_executions_report_id ON system.report_executions(report_id); CREATE INDEX idx_report_executions_created_by ON system.report_executions(created_by); CREATE INDEX idx_report_executions_created_at ON system.report_executions(created_at DESC); -- Dashboards CREATE INDEX idx_dashboards_tenant_id ON system.dashboards(tenant_id); CREATE INDEX idx_dashboards_user_id ON system.dashboards(user_id); CREATE INDEX idx_dashboards_is_public ON system.dashboards(is_public) WHERE is_public = TRUE; -- Dashboard Widgets CREATE INDEX idx_dashboard_widgets_dashboard_id ON system.dashboard_widgets(dashboard_id); CREATE INDEX idx_dashboard_widgets_type ON system.dashboard_widgets(widget_type); -- ===================================================== -- FUNCTIONS -- ===================================================== -- Función: notify_followers CREATE OR REPLACE FUNCTION system.notify_followers( p_model VARCHAR, p_record_id UUID, p_message_id UUID ) RETURNS VOID AS $$ BEGIN INSERT INTO system.notifications (tenant_id, user_id, title, message, model, record_id) SELECT get_current_tenant_id(), mf.user_id, 'New message in ' || p_model, m.body, p_model, p_record_id FROM system.message_followers mf JOIN system.messages m ON m.id = p_message_id WHERE mf.model = p_model AND mf.record_id = p_record_id AND mf.user_id IS NOT NULL AND mf.email_notifications = TRUE; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION system.notify_followers IS 'Notifica a los seguidores de un registro cuando hay un nuevo mensaje'; -- Función: mark_activity_as_overdue CREATE OR REPLACE FUNCTION system.mark_activities_as_overdue() RETURNS INTEGER AS $$ DECLARE v_updated_count INTEGER; BEGIN WITH updated AS ( UPDATE system.activities SET status = 'overdue' WHERE status = 'planned' AND due_date < CURRENT_DATE RETURNING id ) SELECT COUNT(*) INTO v_updated_count FROM updated; RETURN v_updated_count; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION system.mark_activities_as_overdue IS 'Marca actividades vencidas como overdue (ejecutar diariamente)'; -- Función: clean_old_logs CREATE OR REPLACE FUNCTION system.clean_old_logs(p_days_to_keep INTEGER DEFAULT 90) RETURNS INTEGER AS $$ DECLARE v_deleted_count INTEGER; BEGIN WITH deleted AS ( DELETE FROM system.logs WHERE created_at < CURRENT_TIMESTAMP - (p_days_to_keep || ' days')::INTERVAL AND level != 'critical' RETURNING id ) SELECT COUNT(*) INTO v_deleted_count FROM deleted; RETURN v_deleted_count; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION system.clean_old_logs IS 'Limpia logs antiguos (mantener solo críticos)'; -- ===================================================== -- TRIGGERS -- ===================================================== CREATE TRIGGER trg_messages_updated_at BEFORE UPDATE ON system.messages FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_message_templates_updated_at BEFORE UPDATE ON system.message_templates FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_reports_updated_at BEFORE UPDATE ON system.reports FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_dashboards_updated_at BEFORE UPDATE ON system.dashboards FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); CREATE TRIGGER trg_dashboard_widgets_updated_at BEFORE UPDATE ON system.dashboard_widgets FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); -- ===================================================== -- ROW LEVEL SECURITY (RLS) -- ===================================================== ALTER TABLE system.messages ENABLE ROW LEVEL SECURITY; ALTER TABLE system.notifications ENABLE ROW LEVEL SECURITY; ALTER TABLE system.activities ENABLE ROW LEVEL SECURITY; ALTER TABLE system.message_templates ENABLE ROW LEVEL SECURITY; ALTER TABLE system.logs ENABLE ROW LEVEL SECURITY; ALTER TABLE system.reports ENABLE ROW LEVEL SECURITY; ALTER TABLE system.report_executions ENABLE ROW LEVEL SECURITY; ALTER TABLE system.dashboards ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_messages ON system.messages USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_notifications ON system.notifications USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_activities ON system.activities USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_message_templates ON system.message_templates USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_logs ON system.logs USING (tenant_id = get_current_tenant_id() OR tenant_id IS NULL); CREATE POLICY tenant_isolation_reports ON system.reports USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_report_executions ON system.report_executions USING (tenant_id = get_current_tenant_id()); CREATE POLICY tenant_isolation_dashboards ON system.dashboards USING (tenant_id = get_current_tenant_id()); -- ===================================================== -- TRACKING AUTOMÁTICO (mail.thread pattern de Odoo) -- ===================================================== -- Tabla: field_tracking_config (Configuración de campos a trackear) CREATE TABLE system.field_tracking_config ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), table_schema VARCHAR(50) NOT NULL, table_name VARCHAR(100) NOT NULL, field_name VARCHAR(100) NOT NULL, track_changes BOOLEAN NOT NULL DEFAULT true, field_type VARCHAR(50) NOT NULL, -- 'text', 'integer', 'numeric', 'boolean', 'uuid', 'timestamp', 'json' display_label VARCHAR(255) NOT NULL, -- Para mostrar en UI: "Estado", "Monto", "Cliente", etc. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_field_tracking UNIQUE (table_schema, table_name, field_name) ); -- Índice para búsqueda rápida CREATE INDEX idx_field_tracking_config_table ON system.field_tracking_config(table_schema, table_name); -- Tabla: change_log (Historial de cambios en registros) CREATE TABLE system.change_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id), -- Referencia al registro modificado table_schema VARCHAR(50) NOT NULL, table_name VARCHAR(100) NOT NULL, record_id UUID NOT NULL, -- Usuario que hizo el cambio changed_by UUID NOT NULL REFERENCES auth.users(id), changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Tipo de cambio change_type VARCHAR(20) NOT NULL CHECK (change_type IN ('create', 'update', 'delete', 'state_change')), -- Campo modificado (NULL para create/delete) field_name VARCHAR(100), field_label VARCHAR(255), -- Para UI: "Estado", "Monto Total", etc. -- Valores anterior y nuevo old_value TEXT, new_value TEXT, -- Metadata adicional change_context JSONB, -- Info adicional: IP, user agent, módulo, etc. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Índices para performance del change_log CREATE INDEX idx_change_log_tenant_id ON system.change_log(tenant_id); CREATE INDEX idx_change_log_record ON system.change_log(table_schema, table_name, record_id); CREATE INDEX idx_change_log_changed_by ON system.change_log(changed_by); CREATE INDEX idx_change_log_changed_at ON system.change_log(changed_at DESC); CREATE INDEX idx_change_log_type ON system.change_log(change_type); -- Índice compuesto para queries comunes CREATE INDEX idx_change_log_record_date ON system.change_log(table_schema, table_name, record_id, changed_at DESC); -- RLS Policy para multi-tenancy ALTER TABLE system.change_log ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_change_log ON system.change_log USING (tenant_id = get_current_tenant_id()); -- ===================================================== -- FUNCIÓN DE TRACKING AUTOMÁTICO -- ===================================================== -- Función: track_field_changes -- Función genérica para trackear cambios automáticamente CREATE OR REPLACE FUNCTION system.track_field_changes() RETURNS TRIGGER AS $$ DECLARE v_tenant_id UUID; v_user_id UUID; v_field_name TEXT; v_field_label TEXT; v_old_value TEXT; v_new_value TEXT; v_field_config RECORD; BEGIN -- Obtener tenant_id y user_id del registro IF TG_OP = 'DELETE' THEN v_tenant_id := OLD.tenant_id; v_user_id := OLD.deleted_by; ELSE v_tenant_id := NEW.tenant_id; v_user_id := NEW.updated_by; END IF; -- Registrar creación IF TG_OP = 'INSERT' THEN INSERT INTO system.change_log ( tenant_id, table_schema, table_name, record_id, changed_by, change_type, change_context ) VALUES ( v_tenant_id, TG_TABLE_SCHEMA, TG_TABLE_NAME, NEW.id, NEW.created_by, 'create', jsonb_build_object('operation', 'INSERT') ); RETURN NEW; END IF; -- Registrar eliminación (soft delete) IF TG_OP = 'UPDATE' AND OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN INSERT INTO system.change_log ( tenant_id, table_schema, table_name, record_id, changed_by, change_type, change_context ) VALUES ( v_tenant_id, TG_TABLE_SCHEMA, TG_TABLE_NAME, NEW.id, NEW.deleted_by, 'delete', jsonb_build_object('operation', 'SOFT_DELETE', 'deleted_at', NEW.deleted_at) ); RETURN NEW; END IF; -- Registrar cambios en campos configurados IF TG_OP = 'UPDATE' THEN -- Iterar sobre campos configurados para esta tabla FOR v_field_config IN SELECT field_name, display_label, field_type FROM system.field_tracking_config WHERE table_schema = TG_TABLE_SCHEMA AND table_name = TG_TABLE_NAME AND track_changes = true LOOP v_field_name := v_field_config.field_name; v_field_label := v_field_config.display_label; -- Obtener valores antiguo y nuevo (usar EXECUTE para campos dinámicos) EXECUTE format('SELECT ($1).%I::TEXT, ($2).%I::TEXT', v_field_name, v_field_name) INTO v_old_value, v_new_value USING OLD, NEW; -- Si el valor cambió, registrarlo IF v_old_value IS DISTINCT FROM v_new_value THEN INSERT INTO system.change_log ( tenant_id, table_schema, table_name, record_id, changed_by, change_type, field_name, field_label, old_value, new_value, change_context ) VALUES ( v_tenant_id, TG_TABLE_SCHEMA, TG_TABLE_NAME, NEW.id, v_user_id, CASE WHEN v_field_name = 'status' OR v_field_name = 'state' THEN 'state_change' ELSE 'update' END, v_field_name, v_field_label, v_old_value, v_new_value, jsonb_build_object('operation', 'UPDATE', 'field_type', v_field_config.field_type) ); END IF; END LOOP; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; COMMENT ON FUNCTION system.track_field_changes IS 'Función trigger para trackear cambios automáticamente según configuración en field_tracking_config (patrón mail.thread de Odoo)'; -- ===================================================== -- SEED DATA: Configuración de campos a trackear -- ===================================================== -- FINANCIAL: Facturas INSERT INTO system.field_tracking_config (table_schema, table_name, field_name, field_type, display_label) VALUES ('financial', 'invoices', 'status', 'text', 'Estado'), ('financial', 'invoices', 'partner_id', 'uuid', 'Cliente/Proveedor'), ('financial', 'invoices', 'invoice_date', 'timestamp', 'Fecha de Factura'), ('financial', 'invoices', 'amount_total', 'numeric', 'Monto Total'), ('financial', 'invoices', 'payment_term_id', 'uuid', 'Término de Pago') ON CONFLICT (table_schema, table_name, field_name) DO NOTHING; -- FINANCIAL: Asientos contables INSERT INTO system.field_tracking_config (table_schema, table_name, field_name, field_type, display_label) VALUES ('financial', 'journal_entries', 'status', 'text', 'Estado'), ('financial', 'journal_entries', 'date', 'timestamp', 'Fecha del Asiento'), ('financial', 'journal_entries', 'journal_id', 'uuid', 'Diario Contable') ON CONFLICT (table_schema, table_name, field_name) DO NOTHING; -- PURCHASE: Órdenes de compra INSERT INTO system.field_tracking_config (table_schema, table_name, field_name, field_type, display_label) VALUES ('purchase', 'purchase_orders', 'status', 'text', 'Estado'), ('purchase', 'purchase_orders', 'partner_id', 'uuid', 'Proveedor'), ('purchase', 'purchase_orders', 'order_date', 'timestamp', 'Fecha de Orden'), ('purchase', 'purchase_orders', 'amount_total', 'numeric', 'Monto Total'), ('purchase', 'purchase_orders', 'receipt_status', 'text', 'Estado de Recepción') ON CONFLICT (table_schema, table_name, field_name) DO NOTHING; -- SALES: Órdenes de venta INSERT INTO system.field_tracking_config (table_schema, table_name, field_name, field_type, display_label) VALUES ('sales', 'sales_orders', 'status', 'text', 'Estado'), ('sales', 'sales_orders', 'partner_id', 'uuid', 'Cliente'), ('sales', 'sales_orders', 'order_date', 'timestamp', 'Fecha de Orden'), ('sales', 'sales_orders', 'amount_total', 'numeric', 'Monto Total'), ('sales', 'sales_orders', 'invoice_status', 'text', 'Estado de Facturación'), ('sales', 'sales_orders', 'delivery_status', 'text', 'Estado de Entrega') ON CONFLICT (table_schema, table_name, field_name) DO NOTHING; -- INVENTORY: Movimientos de stock INSERT INTO system.field_tracking_config (table_schema, table_name, field_name, field_type, display_label) VALUES ('inventory', 'stock_moves', 'status', 'text', 'Estado'), ('inventory', 'stock_moves', 'product_id', 'uuid', 'Producto'), ('inventory', 'stock_moves', 'product_qty', 'numeric', 'Cantidad'), ('inventory', 'stock_moves', 'location_id', 'uuid', 'Ubicación Origen'), ('inventory', 'stock_moves', 'location_dest_id', 'uuid', 'Ubicación Destino') ON CONFLICT (table_schema, table_name, field_name) DO NOTHING; -- PROJECTS: Proyectos INSERT INTO system.field_tracking_config (table_schema, table_name, field_name, field_type, display_label) VALUES ('projects', 'projects', 'status', 'text', 'Estado'), ('projects', 'projects', 'name', 'text', 'Nombre del Proyecto'), ('projects', 'projects', 'manager_id', 'uuid', 'Responsable'), ('projects', 'projects', 'date_start', 'timestamp', 'Fecha de Inicio'), ('projects', 'projects', 'date_end', 'timestamp', 'Fecha de Fin') ON CONFLICT (table_schema, table_name, field_name) DO NOTHING; -- ===================================================== -- COMENTARIOS -- ===================================================== COMMENT ON SCHEMA system IS 'Schema de mensajería, notificaciones, logs, reportes y tracking automático'; COMMENT ON TABLE system.messages IS 'Mensajes del chatter (comentarios, notas, emails)'; COMMENT ON TABLE system.message_followers IS 'Seguidores de registros para notificaciones'; COMMENT ON TABLE system.notifications IS 'Notificaciones a usuarios'; COMMENT ON TABLE system.activities IS 'Actividades programadas (llamadas, reuniones, tareas)'; COMMENT ON TABLE system.message_templates IS 'Plantillas de mensajes y emails'; COMMENT ON TABLE system.email_queue IS 'Cola de envío de emails'; COMMENT ON TABLE system.logs IS 'Logs del sistema y auditoría'; COMMENT ON TABLE system.reports IS 'Definiciones de reportes'; COMMENT ON TABLE system.report_executions IS 'Ejecuciones de reportes con resultados'; COMMENT ON TABLE system.dashboards IS 'Dashboards configurables por usuario'; COMMENT ON TABLE system.dashboard_widgets IS 'Widgets dentro de dashboards'; COMMENT ON TABLE system.field_tracking_config IS 'Configuración de campos a trackear automáticamente por tabla (patrón mail.thread de Odoo)'; COMMENT ON TABLE system.change_log IS 'Historial de cambios en registros (mail.thread pattern de Odoo). Registra automáticamente cambios de estado y campos críticos.'; -- ===================================================== -- FIN DEL SCHEMA SYSTEM -- =====================================================