-- ===================================================== -- SISTEMA DE TRAZABILIDAD DE LOTES - ESQUEMA PRINCIPAL -- ===================================================== -- Este esquema implementa un modelo de grafo para trazabilidad -- de café desde ingreso de uva hasta secado final. -- Permite rastrear divisiones, combinaciones y transformaciones. -- Extensiones necesarias CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ===================================================== -- TABLA: lotes -- ===================================================== -- Representa cualquier estado físico del café en un momento dado. -- Ejemplos: uva ingresada, café despulpado, café oreado, café secado, etc. CREATE TABLE lotes ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), codigo TEXT UNIQUE, -- Código legible: UVA-001, SEC-042, etc. tipo TEXT NOT NULL, -- uva, despulpado_primera, despulpado_segunda, despulpado_rechazos, oreado, presecado, reposo, secado fecha_creado TIMESTAMPTZ NOT NULL DEFAULT NOW(), lugar_id INTEGER, -- Referencia opcional a lugares (patio 1, pila 2, etc.) cantidad_kg NUMERIC(10,2), -- Cantidad en kilogramos meta JSONB, -- Información adicional (humedad, notas, etc.) CONSTRAINT lotes_cantidad_positiva CHECK (cantidad_kg IS NULL OR cantidad_kg >= 0), CONSTRAINT lotes_tipo_valido CHECK (tipo IN ( 'uva', 'despulpado_primera', 'despulpado_segunda', 'despulpado_rechazos', 'oreado', 'presecado', 'reposo', 'secado' )) ); -- Índices para búsquedas frecuentes CREATE INDEX idx_lotes_tipo ON lotes(tipo); CREATE INDEX idx_lotes_fecha_creado ON lotes(fecha_creado DESC); CREATE INDEX idx_lotes_codigo ON lotes(codigo) WHERE codigo IS NOT NULL; -- Comentarios COMMENT ON TABLE lotes IS 'Representa cualquier estado físico del café en un momento dado'; COMMENT ON COLUMN lotes.codigo IS 'Código legible opcional para identificar el lote (ej: UVA-001, SEC-042)'; COMMENT ON COLUMN lotes.tipo IS 'Tipo de lote: uva, despulpado_*, oreado, presecado, reposo, secado'; COMMENT ON COLUMN lotes.meta IS 'Datos adicionales en formato JSON (ej: {humedad: 12.5, notas: "café especial"})'; -- ===================================================== -- TABLA: operaciones -- ===================================================== -- Representa un evento donde lotes se transforman, combinan o dividen. -- Ejemplos: ingreso de uva, despulpado, oreado, ajuste de merma, etc. CREATE TABLE operaciones ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tipo TEXT NOT NULL, -- Tipo de operación fecha TIMESTAMPTZ NOT NULL DEFAULT NOW(), lugar_id INTEGER, -- Referencia opcional a lugares meta JSONB, -- Información adicional específica del tipo CONSTRAINT operaciones_tipo_valido CHECK (tipo IN ( -- Operaciones de proceso normal 'ingreso', 'despulpado', 'oreado', 'presecado', 'reposo', 'secado', 'traslado', 'mezcla', -- Operaciones de ajuste/corrección 'ajuste_merma', 'ajuste_cantidad', 'ajuste_tipo', 'correccion_asignacion', 'fusion_manual', 'division_manual' )) ); -- Índices para búsquedas frecuentes CREATE INDEX idx_operaciones_tipo ON operaciones(tipo); CREATE INDEX idx_operaciones_fecha ON operaciones(fecha DESC); -- Comentarios COMMENT ON TABLE operaciones IS 'Eventos donde lotes se transforman, combinan o dividen'; COMMENT ON COLUMN operaciones.tipo IS 'Tipo de operación: ingreso, despulpado, oreado, ajuste_merma, etc.'; COMMENT ON COLUMN operaciones.meta IS 'Datos adicionales específicos del tipo de operación en formato JSON'; -- ===================================================== -- TABLA: operacion_lotes -- ===================================================== -- Relación muchos a muchos entre operaciones y lotes. -- Define qué lotes entran (input) y salen (output) de cada operación. CREATE TABLE operacion_lotes ( operacion_id UUID NOT NULL REFERENCES operaciones(id) ON DELETE CASCADE, lote_id UUID NOT NULL REFERENCES lotes(id) ON DELETE CASCADE, rol TEXT NOT NULL, -- 'input' o 'output' cantidad_kg NUMERIC(10,2), -- Cantidad específica usada/producida PRIMARY KEY (operacion_id, lote_id, rol), CONSTRAINT operacion_lotes_rol_valido CHECK (rol IN ('input', 'output')), CONSTRAINT operacion_lotes_cantidad_positiva CHECK (cantidad_kg IS NULL OR cantidad_kg > 0) ); -- Índices para navegación del grafo CREATE INDEX idx_operacion_lotes_operacion ON operacion_lotes(operacion_id); CREATE INDEX idx_operacion_lotes_lote ON operacion_lotes(lote_id); CREATE INDEX idx_operacion_lotes_rol ON operacion_lotes(rol); -- Comentarios COMMENT ON TABLE operacion_lotes IS 'Define qué lotes entran y salen de cada operación (grafo de trazabilidad)'; COMMENT ON COLUMN operacion_lotes.rol IS 'input: lote usado en la operación | output: lote producido por la operación'; COMMENT ON COLUMN operacion_lotes.cantidad_kg IS 'Cantidad en kg que participó en esta relación específica'; -- ===================================================== -- FUNCIÓN: get_trazabilidad -- ===================================================== -- Obtiene el historial completo de un lote caminando el grafo hacia atrás. -- Retorna todos los lotes ancestros hasta llegar a los ingresos iniciales. CREATE OR REPLACE FUNCTION get_trazabilidad(lote_id_inicial UUID) RETURNS TABLE ( lote_id UUID, codigo TEXT, tipo TEXT, cantidad_kg NUMERIC, operacion_id UUID, operacion_tipo TEXT, profundidad INTEGER ) AS $$ BEGIN RETURN QUERY WITH RECURSIVE trazabilidad AS ( -- Punto de partida: el lote final SELECT l.id AS lote_id, l.codigo, l.tipo, l.cantidad_kg, ol.operacion_id, o.tipo AS operacion_tipo, 0 AS profundidad FROM lotes l LEFT JOIN operacion_lotes ol ON ol.lote_id = l.id AND ol.rol = 'output' LEFT JOIN operaciones o ON o.id = ol.operacion_id WHERE l.id = lote_id_inicial UNION ALL -- Caminar hacia atrás: buscar lotes que fueron inputs SELECT l2.id AS lote_id, l2.codigo, l2.tipo, l2.cantidad_kg, ol2.operacion_id, o2.tipo AS operacion_tipo, t.profundidad + 1 FROM trazabilidad t JOIN operacion_lotes ol_in ON ol_in.operacion_id = t.operacion_id AND ol_in.rol = 'input' JOIN lotes l2 ON l2.id = ol_in.lote_id LEFT JOIN operacion_lotes ol2 ON ol2.lote_id = l2.id AND ol2.rol = 'output' LEFT JOIN operaciones o2 ON o2.id = ol2.operacion_id WHERE t.operacion_id IS NOT NULL -- Solo continuar si hay operación ) SELECT * FROM trazabilidad ORDER BY profundidad, tipo, codigo; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION get_trazabilidad IS 'Obtiene el historial completo de un lote caminando el grafo hacia atrás'; -- ===================================================== -- VISTA: vista_lotes_con_origen -- ===================================================== -- Vista útil que muestra cada lote con información de la operación que lo creó. CREATE OR REPLACE VIEW vista_lotes_con_origen AS SELECT l.id, l.codigo, l.tipo, l.fecha_creado, l.cantidad_kg, l.meta, o.id AS operacion_id, o.tipo AS operacion_tipo, o.fecha AS operacion_fecha FROM lotes l LEFT JOIN operacion_lotes ol ON ol.lote_id = l.id AND ol.rol = 'output' LEFT JOIN operaciones o ON o.id = ol.operacion_id; COMMENT ON VIEW vista_lotes_con_origen IS 'Muestra lotes con información de la operación que los creó'; -- ===================================================== -- MENSAJES DE ÉXITO -- ===================================================== DO $$ BEGIN RAISE NOTICE '✓ Esquema de trazabilidad creado exitosamente'; RAISE NOTICE ' - Tabla lotes creada'; RAISE NOTICE ' - Tabla operaciones creada'; RAISE NOTICE ' - Tabla operacion_lotes creada'; RAISE NOTICE ' - Función get_trazabilidad() creada'; RAISE NOTICE ' - Vista vista_lotes_con_origen creada'; END $$;