erp-core/database/ddl/09-system.sql

854 lines
28 KiB
PL/PgSQL

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